/* ═══ app-panel.css — THE PANEL PRIMITIVE ═════════════════════════════════════════════════
 *
 * docs/design/PANEL_PRIMITIVE.md is the contract; docs/design/references/ is the taste it was
 * read off. Owner, 2026-09-05, handing over four pictures: *"look at how they turn boxes and
 * shapes into visually appealing designs. very boxy, yet still beautiful … they figure out how
 * to turn data into visuals."*
 *
 * WHAT THIS IS. The admin dashboard's panel grammar — the hairline box with corner brackets, a
 * tab label on its top edge, a meta slot, cells separated by hairlines, gauges, lozenges, log
 * rows, the hazard stripe, the ghost numeral — lifted out of web/app-admin.css so that EVERY
 * screen can wear it. It was written for one screen and immediately wanted by three; a second
 * copy of an eight-layer corner-bracket background is a night nobody gets back.
 *
 * ⚖ THIS IS THE SHAPE, NEVER THE SCREEN. A rule belongs here when a panel on any surface would
 * want it, and in the screen's own sheet when it would not. `.pn-cell` knows it is a cell in a
 * hairline grid; it does not know that the admin dashboard's tiles centre their contents or
 * that a member's wallet cell does not. ⛔ Nothing in this file may name a screen, a route or
 * a feature, and nothing in another sheet may write a `.pn-*` selector — the whole point of a
 * primitive is that it has ONE definition, and a screen reaching in to bend one is the DUP
 * this sheet exists to prevent. panelprimitive.test.js gates both directions.
 *
 * ⚖ WHERE IT SITS IN THE CASCADE. Second, immediately after app.css and above every other part
 * (server/tests/cssparts.js CSS_ORDER, web/shell/head.html). It is a BASE — a screen's own
 * sheet must be able to override any of it at equal specificity, which only works if this one
 * comes first. ⛔ Never move it below a screen sheet to win an argument; the screen is right.
 *
 * ⚖ TOKENS ONLY, BECAUSE THERE ARE SEVEN SKINS. No literal colour appears in this file. The
 * filled pairs are --accent on --void, which sit at opposite ends of every skin's ramp (amber
 * on near-black in the five dark ones, dark red / dark navy on cream in the two light ones) —
 * that pairing is what makes one rule legible in all seven. ⚠ `--bg` is a phantom: it has never
 * been defined anywhere in this client and `var(--bg)` resolves to nothing.
 *
 * ⚖ ONE ACCENT, THEN MEANING. `--accent` is the interface's own mark (a tab label, a lit cell,
 * a count, "you are here"). `--good` / `--bad` / `--info` appear only where they MEAN
 * something. ⛔ Never a red badge for an ordinary count: red spent on a queue with three things
 * in it is red that says nothing when it lands on something that matters.
 *
 * ⚖ THE TONE VOCABULARY IS `.is-*`, AND IT IS THE SAME EVERYWHERE. `.is-good` --good,
 * `.is-warn` --accent, `.is-bad` --bad, `.is-info` --info, `.is-accent` FILLED --accent. A
 * screen that invents `.hot` or `.warning` for one of these has forked the vocabulary.
 *
 * ⚖ ONE BREAKPOINT, AND IT IS THE HOUSE'S. 1000px — BURGER_BP's min-width complement
 * (app.css's ladder, mobilebreakpoints.test.js). This file states it once, as the phone default
 * for `--pn-cols`. There is no second rung here.
 *
 * ⚖ NO MOTION AT ALL, WHICH IS HOW IT RESPECTS `prefers-reduced-motion`. Not one `transition`
 * and not one `animation`: a panel is furniture and none of it needs to move. ⚠ IF THAT EVER
 * CHANGES, the same edit adds the `@media(prefers-reduced-motion:reduce)` block that turns it
 * into an instant state change — never into no feedback. panelprimitive.test.js holds the pair
 * together so the guard cannot be forgotten by the person adding the motion.
 *
 * ⛔ THE ONE INLINE ALLOWANCE, AND IT IS EXACTLY ONE. `.pn-gauge-fill` and `.pn-bar-seg` read
 * their length from `--pn-fill`, set as `style="--pn-fill:42%"`. A bar's LENGTH IS DATA — the
 * number is the picture — and a custom property is the one way to hand a datum to the
 * stylesheet without inventing a style rule at runtime. Nothing else in this file may be
 * written from script.
 */

/* ═══ THE PANEL ═══════════════════════════════════════════════════════════════════════════
   SCiPNET's "SITE-XX SITE CONTROL" and TERMINAL_UI's RESOURCE_MONITOR: a hairline box on
   --raised, NO RADIUS, four corner brackets, a tab label sitting ON the top edge and an
   optional meta slot at the top right.

   ⚖ THE BRACKETS ARE BACKGROUND LAYERS, NOT DOM AND NOT PSEUDO-ELEMENTS. An L needs two
   rectangles, so four corners are EIGHT one-pixel gradient layers in ONE `background-image`.
   Pseudo-elements were the obvious spelling and they are wrong: a panel wants both of its own
   (`::before` for a ground, `::after` for an overlay), and a corner that costs four spans is
   four spans on every panel on the site. They sit 3px inside the padding box so they read as a
   bracket rather than as a thicker border.

   ⚑ `--pn-pad` IS THE PADDING, SO A SCREEN CAN MOVE IT. The default is the dashboard's. A
   panel whose meta slot must escape the padding (the admin suite's does, below 1000px) sets
   `--pn-pad:0` and pads an inner box instead.

   ⚑ DENSITY, OWNER 2026-09-05 (forage-pia1.7): THE DEFAULT WAS `22px 16px 16px` AND IS NOW
   `18px 16px 14px` — 4px off the top, 2px off the bottom, on every panel that has not asked for
   something else. ⚖ IT IS THE FALLBACK THAT MOVED, NOT A PROPERTY, and that is what makes it
   safe on a surface four other lanes were in: every consumer that sets `--pn-pad` keeps exactly
   the number it set (`.pns-up-store` 26/16/22, `.as-panel` 0, clubs.css's `.pns-clubs-ev`
   14/6/6, its composer, its grid and `#p-event`), so this cannot reach a panel whose spacing
   somebody measured. ⚠ 18 IS A FLOOR, NOT A TASTE: `.pn-tab` hangs at `top:-8px` and its box is
   17px, so it ends 10px inside the panel's border box — the first thing under it may not start
   above that. 18 + 1px border leaves NINE pixels of clearance under the label, which is more
   than the clubs session card has shipped with all along (14px pad, 5px clearance). ⛔ Never a
   smaller TYPE size: the owner's other standing complaint about this UI is that it is already
   *"too small, too cluttered"*, so density is bought with chrome and never with points.
   Pinned, with the clearance, by server/tests/paneldensity.test.js. */
.pn{position:relative;padding:var(--pn-pad,18px 16px 14px);
  border:1px solid var(--hair);background-color:var(--raised);
  background-repeat:no-repeat;
  background-image:
    linear-gradient(var(--hair-hi),var(--hair-hi)),linear-gradient(var(--hair-hi),var(--hair-hi)),
    linear-gradient(var(--hair-hi),var(--hair-hi)),linear-gradient(var(--hair-hi),var(--hair-hi)),
    linear-gradient(var(--hair-hi),var(--hair-hi)),linear-gradient(var(--hair-hi),var(--hair-hi)),
    linear-gradient(var(--hair-hi),var(--hair-hi)),linear-gradient(var(--hair-hi),var(--hair-hi));
  background-size:11px 1px,1px 11px,11px 1px,1px 11px,11px 1px,1px 11px,11px 1px,1px 11px;
  background-position:
    3px 3px,3px 3px,
    calc(100% - 3px) 3px,calc(100% - 3px) 3px,
    3px calc(100% - 3px),3px calc(100% - 3px),
    calc(100% - 3px) calc(100% - 3px),calc(100% - 3px) calc(100% - 3px)}

/* THE TAB LABEL — the panel's name, ON the edge, overlapping the border by half its height.
   ⚖ IT IS THE HEADING. The real `<h2>` stays in the DOM for the screen reader and leaves the
   screen (app-admin.css's 1px clip is the house spelling); what a sighted reader gets is this,
   which is a label doing a label's job instead of a row of same-weight uppercase words. */
/* ⚑ OWNER 2026-09-05: *"that upload page is a bit too heavy on the console design. having the
   constant yellow boxes is a bit much."* The tab was a SOLID --accent chip with --void text —
   fine on a two-panel screen, a wall of gold on /upload's five stacked panels. ⚖ IT IS STILL
   THE HEADING and still the accent, but it is the accent as a WORD, not a filled box: --accent
   text on the panel's own --raised ground, which is exactly how `.pn-meta` interrupts the border
   cleanly on the other edge. ⛔ NOT transparent — the panel's 1px --hair border runs under the
   label, and a bare tab would have the line strike through the text; the raised ground is what
   breaks the border for it, the meta slot's own trick. The wider tracking carries the label
   weight the fill used to. */
.pn > .pn-tab{position:absolute;top:-8px;left:14px;z-index:2;
  background:var(--raised);color:var(--accent);
  font-size:11px;line-height:1;padding:3px 6px;white-space:nowrap;
  text-transform:var(--caps);letter-spacing:calc(.16em * var(--track))}

/* THE META SLOT — `30 DAYS · UTC`, `6 OF 41`, a chart's readout. Tabular, quiet, and at ≥1000px
   it wears the panel's own ground so it interrupts the border cleanly.
   ⚠ BELOW 1000px IT COMES OFF THE EDGE and becomes a right-aligned line inside the panel. An
   absolutely-positioned readout that wraps is a readout hanging over the tab label, which is a
   misalignment the owner has already photographed once in another shape.

   ⚑ DENSITY, OWNER 2026-09-05 (forage-pia1): *"this pretty UI went from being really info dense
   to being really pretty and not info dense."* MEASURED ON THE REAL APP AT 390 BEFORE ANY EDIT:
   the slot's first word sat 45px into the panel and the first row of DATA 60px, on twelve member
   panels; 20 of those pixels were this rule's blank top padding, paying for a ONE-LINE readout
   ("20 posts", "0 rows", "1 collection"). At ≥1000px the same readout costs nothing at all — it
   is absolute, on the border, beside the tab — so the phone was paying for a layout the desktop
   does not have. ⚖ THE LINE IS NOT SHRUNK: same 11px, same words, same right edge. What goes is
   twenty pixels of nothing, and the readout now opens the panel's content instead of floating in
   a band above it — the same anatomy clubs.css already gives its own metas at ≤999px.

   ⛔ AND IT IS A PADDING CHANGE AND NOT A LIFT, WHICH IS THE WHOLE SAFETY ARGUMENT. The first
   attempt (8a0daa47, MERGED AND REVERTED 2026-09-07) bought seven more pixels with
   `margin-top:-13px`. A negative margin on a SHARED class is measured against the CONSUMER's top
   padding, not this sheet's: `.pns-clubs-ev` is `--pn-pad:14px 6px 6px`, so a session card's
   readout — which already overrides this rule's `padding` and therefore kept every pixel of the
   lift — rose 6px ABOVE the tab it must sit under, and `clubs_desktop_e2e` §16c caught it at 390
   (`the tab (178) is still above the meta's words (172)`). PADDING CANNOT DO THAT: every consumer
   that wants a different slot writes its own `padding` (clubs.css ≤999px, `.pns-up-storemeta`,
   `.pns-search-recapmeta`, `.pns-alerts-meta`, `.pns-data-meta`) and is therefore untouched by
   this line, and a slot that is not lifted can never rise above the panel's own content edge on
   any surface. Budgeted, with the tab clearance, by server/tests/paneldensity.test.js. */
.pn > .pn-meta{display:block;padding:0 16px 0;text-align:right;
  font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;
  text-transform:var(--caps);letter-spacing:calc(.08em * var(--track))}
@media(min-width:1000px){
  .pn > .pn-meta{position:absolute;top:-8px;right:14px;z-index:2;padding:0 8px;
    background:var(--raised);max-width:calc(100% - 200px);overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap}
}

/* THE GROUND — SCiPNET's faint dot grid, on the COLUMN the panels stack in, never on a panel
   and never on the masthead. 1px dots on a 24px cell at --hair-hi 35%, drawn on a pseudo-
   element rather than on the column itself so the alpha belongs to the dots and not to
   everything stacked above them. The panels carry their own opaque --raised ground and sit on
   top of it. ⚠ A CHILD THAT MUST SIT ABOVE THE GRID NEEDS `position` AND `z-index` OF ITS OWN;
   the grid is at z-index 0 and an untouched static child paints under nothing. */
.pn-ground{position:relative}
.pn-ground::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background-image:radial-gradient(circle at 1px 1px,
    color-mix(in srgb,var(--hair-hi) 35%,transparent) 1px,transparent 0);
  background-size:24px 24px}

/* ═══ CELLS: HAIRLINE-SEPARATED, IN ONE BOX ═══════════════════════════════════════════════
   TERMINAL_UI's RESOURCE_MONITOR — cells inside ONE box, never six boxes floating in a grid.
   ⚖ THE 1px GAP OVER A --hair GROUND *IS* THE HAIRLINE. The separators are the container's own
   background showing through, so they can never disagree with its border, never double up at a
   corner and never need a `:last-child` exception.
   ⚠ AN EMPTY TRACK IS A HOLE, NOT A GAP. Because the separators are the ground, a grid cell
   with nothing in it renders as a SOLID BLOCK of --hair — photographed at 390px when five
   tiles landed in a two-column grid. A screen with an odd count either spans the last cell
   across the row or uses `auto-fit`; ⛔ never `auto-fill`, which invents the empty track. */
.pn-cells{display:grid;gap:1px;background:var(--hair);border:1px solid var(--hair);
  grid-template-columns:repeat(var(--pn-cols,3),minmax(0,1fr))}
/* the phone default, at BURGER_BP's rung. A screen states its own `--pn-cols` and that wins
   here too: this file loads first, so an equal-specificity rule in a screen sheet outranks it
   at every width, which is exactly the relationship a base should have. */
@media(max-width:999px){.pn-cells{--pn-cols:2}}
.pn-cell{background:var(--sunk);padding:12px 14px 14px}
/* ⚖ THE LIT EDGE IS THE WHOLE DIFFERENCE BETWEEN A TASK AND A DOOR, and the number carries it
   with the edge. A cell at zero is --faint and says "a destination"; a cell with something in
   it takes the accent, twice, in the two places the eye lands. ⚠ A cell that can be lit must
   reserve the 2px at rest (`border-top:2px solid transparent` in the screen's own rule) or
   lighting one MOVES it. */
.pn-cell.is-lit{border-top:2px solid var(--accent)}
.pn-cell.is-lit .pn-num{color:var(--accent)}
.pn-cell.is-faint .pn-num{color:var(--faint)}

/* ═══ TYPE: THE NUMBER, THE LABEL, THE WORD ═══════════════════════════════════════════════
   ⚖ TABULAR NUMERALS WHEREVER THE EYE SCANS A COLUMN, and six cells in a row ARE a column:
   `1,284` over `981` in proportional figures shifts the hundreds digit sideways between
   neighbours and the row stops reading as a table. */
.pn-num{font-size:28px;line-height:1;color:var(--bright);font-variant-numeric:tabular-nums}
/* the headline of a panel that has exactly one number in it. */
.pn-num.is-big{font-size:36px}
.pn-label{font-size:11px;color:var(--dim);text-transform:var(--caps);
  letter-spacing:calc(.12em * var(--track))}

/* THE STATUS WORD (SCiPNET's mint `SAFE`) — one word answering the only question its panel
   asks. ⚖ IT IS CENTRED IN ITS OWN CONTAINER: centring a word-plus-meter pair together leaves
   the word itself sitting left of centre, which is the "uncentred content in a container" the
   references never do. The meter is a SIBLING beside it, not a passenger inside it. */
.pn-word{font-size:26px;line-height:1.15;text-align:center;text-transform:var(--caps);
  letter-spacing:calc(.08em * var(--track));font-variant-numeric:tabular-nums}
.pn-word.is-good{color:var(--good)}
.pn-word.is-warn{color:var(--accent)}
.pn-word.is-bad{color:var(--bad)}
/* the SCiPNET bar beside the word: a vertical stripe, 8px, in the state's own colour. It is
   the only striped ornament that is not the hazard — the angle is the same, the colour is not. */
.pn-meter{flex:0 0 8px;width:8px;align-self:stretch;min-height:46px}
.pn-meter.is-good{border:1px solid var(--good);
  background:repeating-linear-gradient(-45deg,var(--good) 0 3px,transparent 3px 6px)}
.pn-meter.is-warn{border:1px solid var(--accent);
  background:repeating-linear-gradient(-45deg,var(--accent) 0 3px,transparent 3px 6px)}

/* ═══ BARS: EVERY PERCENTAGE DRAWS ═════════════════════════════════════════════════════════
   ⚖ A PERCENTAGE IS NEVER JUST A NUMBER ON THIS SITE (the references' whole argument: *"they
   figure out how to turn data into visuals"*). Every one gets a 6px track of --hair with its
   fill in the state's colour, under the number it measures.
   ⛔ `--pn-fill` IS THE ONLY THING SCRIPT MAY WRITE (see the header). `width:var(--pn-fill)`,
   and the element carries `style="--pn-fill:42%"` — the length is the datum. */
.pn-gauge{display:block;height:6px;background:var(--hair)}
.pn-gauge-fill{display:block;height:100%;width:var(--pn-fill,0)}
.pn-gauge-fill.is-good{background:var(--good)}
.pn-gauge-fill.is-warn{background:var(--accent)}
.pn-gauge-fill.is-bad{background:var(--bad)}
/* THE TWO-SEGMENT BAR — one track, n segments laid end to end, for a split that is a whole:
   earned against spent, used against reserved. ⚠ THE SEGMENTS MUST SUM TO THE WHOLE or the
   remainder reads as a third, unlabelled category; a bar whose parts do not add up is a lie
   with a legend. */
.pn-bar{display:flex;height:6px;background:var(--hair)}
.pn-bar-seg{height:100%;width:var(--pn-fill,0)}
.pn-bar-seg.is-accent{background:var(--accent)}
.pn-bar-seg.is-good{background:var(--good)}
.pn-bar-seg.is-warn{background:var(--accent)}
.pn-bar-seg.is-bad{background:var(--bad)}
.pn-bar-seg.is-info{background:var(--info)}

/* ═══ THE LOZENGE ═════════════════════════════════════════════════════════════════════════
   A 10px chip with a hairline border. `.is-accent` is the FILLED one — a count, and the only
   filled variant there is; the rest are text tints with the border following the text, because
   a wall of filled chips is a wall of badges and none of them means anything.
   ⛔ NEVER `.is-bad` FOR AN ORDINARY COUNT. A count is `.is-accent`; red is reserved for the
   one or two states on a screen that actually mean "stop and look". */
.pn-loz{flex:0 0 auto;display:inline-block;font-size:10px;line-height:1.5;padding:1px 6px;
  color:var(--dim);border:1px solid var(--hair);white-space:nowrap;
  text-transform:var(--caps);letter-spacing:calc(.1em * var(--track));
  font-variant-numeric:tabular-nums}
.pn-loz.is-accent{background:var(--accent);color:var(--void);border-color:var(--accent);
  font-weight:700}
.pn-loz.is-warn{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
.pn-loz.is-good{color:var(--good);border-color:color-mix(in srgb,var(--good) 55%,transparent)}
.pn-loz.is-bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 55%,transparent)}
.pn-loz.is-info{color:var(--info);border-color:color-mix(in srgb,var(--info) 55%,transparent)}

/* ═══ THE LOG ROW (TERMINAL_UI's KERNEL_LOG) ══════════════════════════════════════════════
   ⛔ NO `overflow-x`, NO FIXED HEIGHT, EVER. This box was a scroller once and the owner's
   scrolling screenshot caught it TWICE — a scroller inside the page's scroller is two surfaces
   moving under one thumb. The row is a WRAPPING flex line of tokens, so a long path costs a
   second line and never a millimetre of document width.
   ⚖ THE TIME READS FIRST AND THE DOM ORDER DOES NOT MOVE. The reading order a screen reader
   announces is the DOM sequence and screens pin it; `order` moves the pixel and leaves the
   announcement alone. That is what `order` is for. */
.pn-log{border:1px solid var(--hair);background:var(--sunk)}
.pn-log > .pn-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;padding:9px 12px;
  font-size:11.5px;line-height:1.5;min-width:0}
.pn-log > .pn-row + .pn-row{border-top:1px solid var(--hair)}
/* ⚠ THE UNREAD EDGE IS AN INSET SHADOW, NOT A BORDER, AND THAT IS THE WHOLE POINT. A real
   2px border appears and disappears with the state, so marking a row read would shift every
   line of text in it sideways — a list that twitches as you read it. An inset shadow paints
   the same 2px accent edge and costs no layout at all, so the rail's `.pn-mark` (a border,
   because every rail row reserves one at rest) and this are deliberately different spellings
   of the same mark. */
.pn-log > .pn-row.is-unread{box-shadow:inset 2px 0 0 var(--accent)}
/* THE TIME LEADS, in a fixed gutter, so a column of rows reads as a column of instants. */
.pn-time{order:-1;flex:0 0 3.6em;color:var(--dim);font-size:10.5px;white-space:nowrap;
  font-variant-numeric:tabular-nums}
.pn-actor{color:var(--accent);text-decoration:none;overflow-wrap:anywhere}
a.pn-actor:hover{text-decoration:underline}
.pn-verb{color:var(--ink)}
.pn-path{color:var(--dim);text-decoration:none;overflow-wrap:anywhere;min-width:0}
a.pn-path:hover{color:var(--bright)}

/* ⛔ NO TOKEN MAY REARRANGE THE ROW AROUND IT (forage-xa68.1, the CSS half of the fix).
   server/audit.js `visibleOrder` makes the bidi FORMAT CONTROLS visible, so no value can flip
   its own text any more. This answers the other half: a value that is right-to-left by virtue
   of its LETTERS — an Arabic word typed at the login door, a Hebrew club name — carries no
   control character to escape and is legitimately RTL, and an audit row is ONE bidi paragraph.
   Without isolation that run resolves the neutral characters beside it (the brackets, the `·`,
   the dotted quad of an address) into its own direction and drags them across the token
   boundary — the honest name reads correctly and the ROW around it stops being true.
   `isolate` gives every token its own bidi context: محمد still reads محمد, right to left,
   inside its own box, and the Latin scaffolding does not move.

   ⚖ THE PRIMITIVES, NOT THE ROW, BECAUSE THAT IS WHAT MAKES IT ONE PLACE. `audit.segments`
   has THREE presentations — /admin/audit's grid (`.as-auditrow`), the dashboard's RECENT
   AUTHORITY list (a bare `.pn-row`, page-admin.js:1716, which never gets the grid's class) and
   the per-account dossier — and all three draw the same tokens through these five classes.
   A rule keyed on any one row wrapper lands on two surfaces out of three, which is the DUP
   this repo keeps paying for. Gated by server/tests/auditbidi.test.js. */
.pn-time,.pn-loz,.pn-actor,.pn-verb,.pn-path{unicode-bidi:isolate}

/* ═══ ORNAMENT WITH INTENT ════════════════════════════════════════════════════════════════
   ⚖ THE HAZARD STRIPE IS SPENT ONCE PER SCREEN, ON THE ONE STATE THAT MEANS "STOP AND LOOK".
   SCiPNET uses it as emphasis and it only works because it is rare. ⛔ Never on a warning,
   never on a count, never as a divider — a striped page is a page with no emphasis left. */
.pn-hazard{background-repeat:no-repeat;background-size:100% 6px;background-position:0 0;
  background-image:repeating-linear-gradient(-45deg,var(--bad) 0 8px,transparent 8px 16px)}
/* THE GHOST NUMERAL (TERMINAL_UI's "01") — the number the panel is ABOUT, once, at the size of
   a watermark, `aria-hidden` because the panel already says it and announcing it twice is a
   screen reader reading the wallpaper.
   ⚠ IT NEEDS A BAND TO STAND IN. Cells carry an opaque ground, so a numeral "behind" them is a
   numeral you cannot see — the first cut showed a 20px grey stub below the last row and read
   as a rendering fault. The panel keeps a clear strip at its foot and clips the overflow. */
.pn-ghost{position:absolute;right:16px;bottom:-4px;z-index:0;pointer-events:none;
  font-size:72px;line-height:.75;color:var(--hair-hi);font-variant-numeric:tabular-nums;
  letter-spacing:calc(.04em * var(--track))}

/* ═══ THE RAIL ════════════════════════════════════════════════════════════════════════════
   TERMINAL_UI's sidebar: a group header with a hairline under it, 36px rows, and the CURRENT
   row lit. ⚖ THE MARK IS THE ACCENT, because "you are here" is navigation, not an alarm.
   ⚖ THE 44px FLOOR IS A POINTER QUESTION, NOT A WIDTH ONE. A rail usually lives at desktop
   widths, which is NOT the same sentence as "lives under a mouse" — a touch tablet in landscape
   gets the wide lane and a 36px row. The rows tile in a column with no neighbour to steal a tap
   from, so the floor costs the rail's height and nothing else. */
.pn-rail{display:flex;align-items:center;gap:8px;min-height:36px;padding:0 14px;
  border-left:2px solid transparent;color:var(--dim);font-size:12px;text-decoration:none}
@media(pointer:coarse){.pn-rail{min-height:var(--tap-min)}}
.pn-rail:hover{color:var(--bright);background:var(--panel)}
/* ⚠ THE CURRENT ROW OUTRANKS HOVER, and that needs saying twice. `.pn-mark` alone loses the
   tie to `.pn-rail:hover`, so the row you are ON would flip to the plain hover ground as the
   pointer crossed it — "you are here" blinking off is worse than no mark at all. */
.pn-mark{border-left:2px solid var(--accent);color:var(--bright);
  background:color-mix(in srgb,var(--accent) 10%,transparent)}
.pn-rail.pn-mark:hover{color:var(--bright);
  background:color-mix(in srgb,var(--accent) 10%,transparent)}
.pn-group{font-size:11px;color:var(--dim);text-transform:var(--caps);
  letter-spacing:calc(.12em * var(--track));padding:0 14px 6px;margin:0 0 5px;
  border-bottom:1px solid var(--hair)}

/* ═══ THE BUTTON ══════════════════════════════════════════════════════════════════════════
   A hairline box with a spaced uppercase label — the references have no rounded rectangle
   anywhere, and one among a dozen bracketed boxes reads as the odd one out.
   ⚖ THE 44px FLOOR IS ON COARSE POINTERS. On a mouse the floor is a button three times the
   size of its own label; on a thumb it is the difference between a control and a dare. */
.pn-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;
  padding:6px 12px;font:inherit;font-size:11px;line-height:1.4;cursor:pointer;
  background:var(--sunk);color:var(--ink);border:1px solid var(--hair);text-decoration:none;
  text-transform:var(--caps);letter-spacing:calc(.12em * var(--track))}
.pn-btn:hover{color:var(--bright);border-color:var(--hair-hi)}
.pn-btn.is-primary{background:var(--accent);color:var(--void);border-color:var(--accent);
  font-weight:700}
@media(pointer:coarse){.pn-btn{min-height:var(--tap-min)}}

/* THE EMPTY STATE — one --dim sentence, centred, inside the panel that has nothing to show.
   ⚖ A PANEL WITH NOTHING IN IT STILL HAS ITS TAB LABEL, so the reader knows what is empty. ⛔
   Never delete the panel instead: a section that vanishes when it is empty teaches the reader
   that the page's shape depends on the data, and then a real outage looks like a quiet day. */
.pn-empty{padding:22px 12px;text-align:center;color:var(--dim);font-size:11.5px;line-height:1.6}
