/* app-screens.css — THE MEMBER SCREENS' OWN RULES, one fenced region per screen.
   docs/design/DESIGN.md is the spec; docs/design/references/ is the taste.

   ⚖ WHY A SHEET OF ITS OWN, AND WHY FENCED. Lane F repaints five member screens in five
   commits (2026-09-05, forage-qw2w.6) and the owner's revert law for that night is that a
   screen that reads wrong comes off ALONE. A region here plus that screen's own page-*.js is
   the whole of one screen's paint, so `git revert <sha>` takes exactly one screen with it and
   leaves the other four wearing the grammar. ⛔ Do not merge two regions because they share a
   rule — the shared rule belongs in web/app-panel.css, for everybody, on purpose.

   ⚑ THE PREFIX IS `.pns-<screen>-*`, NOT `.pn-<screen>-*`, AND THAT IS NOT A TYPO.
   panelprimitive.test.js asserts that NO sheet but app-panel.css writes a selector matching
   `.pn` followed by a non-alphanumeric — the DUP gate that stops a screen from growing a
   second copy of the panel. `.pn-wallet-big` matches it; `.pns-wallet-big` does not, because
   the `s` closes the lookahead. So every class here reads "panel screen", it sits BESIDE a
   primitive class on the same element rather than redefining one, and the gate stays a gate.
   ⛔ A rule in this file may never name a `.pn-*` class, not even as an ancestor or a
   descendant: the gate scans the whole prelude, so `.pns-wallet-flow > .pn-bar` is an offence
   and `.pn-bar.pns-wallet-flow` is the spelling that works. Put the class on the element.

   ⚖ CASCADE SLOT: 2, directly after app-panel.css (server/tests/cssparts.js, web/shell/head.html).
   The primitive is the base and this is the first sheet allowed to bend it, so a screen wins its
   tie against the primitive here. It deliberately sits ABOVE app-rank.css / app-profile.css /
   app-feeds.css, which still hold the screens' PRE-repaint rules: nothing in this file fights
   them, because every class here is new. A screen stops using an old class rather than
   out-specifying it, and the old rule goes with the old markup or not at all.

   TOKENS ONLY — seven skins read this (tools/uishot.sh). No hex, no rgb(). No inline style but
   `--pn-fill`. No motion without its @media(prefers-reduced-motion:reduce) in the same edit. */

/* ════════════════════════════════════════════════════════════════════════════════════════
   SHARED BY EVERY REGION BELOW — the column the panels stack in, and nothing else.
   ⚠ This is the ONE un-fenced block. It is three declarations and it is what makes a screen a
   stack of panels rather than a stack of boxes; a second copy per region would be the DUP the
   sheet above argues against. It carries no colour and no screen's opinion. */
.pns-col{display:flex;flex-direction:column;gap:22px;padding:18px 0 40px}

/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /wallet — THE COIN PURSE (page-wallet.js) ▼▼▼  forage-qw2w.6.1
   docs/design/DESIGN.md §/wallet. Two panels: BALANCE (the status cell + the earned/spent
   split drawn as one bar) and LEDGER (log rows). Every figure still comes from
   GET /api/me/coins and is printed, never recomputed (docs/surfaces/wallet.md).
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* THE SPLIT. Status cell left, the flow bar right; one column on a phone so the balance is
   never a 36px number squeezed into half of 390px. */
.pns-wallet-split{display:grid;gap:14px;grid-template-columns:minmax(0,1fr)}
@media(min-width:640px){
  .pns-wallet-split{grid-template-columns:minmax(0,220px) minmax(0,1fr);align-items:stretch}
}

/* THE STATUS CELL (SCiPNET's "OVERALL STATUS / SAFE"): a bordered cell with a label strip and
   the one number the member opened this page for, CENTRED in its own container. */
.pns-wallet-status{display:flex;border:1px solid var(--hair);background:var(--sunk)}
/* ⚠ CENTRED IN ITS OWN CONTAINER, both axes. An off-centre datum in its own box is one of the
   named fails, and the cell is taller than its number whenever the flow beside it is taller. */
.pns-wallet-face{flex:1;display:flex;align-items:center;justify-content:center;gap:10px;
  padding:22px 12px}
/* ⚖ THE BALANCE IS THE ACCENT AND IT IS THE ONLY 36px THING ON THE SCREEN. It sits beside
   `.pn-num.is-big` rather than restyling it — the primitive keeps the size and the tabular
   figures, this says whose colour it wears. */
.pns-wallet-big{color:var(--accent)}
/* ⚠ THE COIN GLYPH IS SIZED HERE AND NOWHERE ELSE. coinMark() (common-icons.js) is the ONE
   copy of the mark on every money surface (navicons.test.js); this scales the instance and
   never re-draws it. */
.pns-wallet-coin{width:26px;height:26px;flex:0 0 auto}

/* THE FLOW — the earned/spent split as ONE bar over the gauge track, the two figures under
   their own ends of it. ⚖ A NUMBER WITH A CEILING IS A BAR, and "earned" is exactly that:
   the ceiling is what has been earned plus what has been spent. */
.pns-wallet-flow{display:flex;flex-direction:column;justify-content:center;gap:10px;
  padding:16px 2px}
/* the SPENT segment. --dim, not a tone: coins leaving is not a warning, and the primitive
   deliberately ships no dim segment (a dim bar means nothing on the dashboard). */
.pns-wallet-spent{background:var(--dim)}
.pns-wallet-legend{display:flex;justify-content:space-between;gap:12px}
.pns-wallet-leg{display:flex;flex-direction:column;gap:3px;min-width:0}
.pns-wallet-leg.is-right{text-align:right}
.pns-wallet-fig{font-size:20px;color:var(--ink)}
.pns-wallet-leg.is-earned .pns-wallet-fig{color:var(--accent)}
/* a positive `adjust` is a ledger GAIN, not spending, and its figure wears the segment's own
   tone so the number and the bar cannot be read as saying different things. */
.pns-wallet-leg.is-ledger .pns-wallet-fig{color:var(--info)}

/* DEBT IS EXPLAINED, NOT JUST COLOURED (docs/surfaces/wallet.md, SPEC_coins.md "Debt is
   real"). It is the one --bad thing on this screen and it is a genuine alarm: a number the
   member did not cause, below zero. It keeps its whole sentence and it stays inside the
   panel that shows the number it explains. */
.pns-wallet-debt{margin:14px 0 0;padding:9px 12px;border:1px solid var(--hair);
  border-top:2px solid var(--bad);background:var(--sunk);
  color:var(--bad);font-size:11.5px;line-height:1.6}

/* THE LEDGER ROW's amount, flush right.
   ⚖ THE COLOUR LAW IS docs/surfaces/wallet.md's, NOT THIS REPAINT'S, AND THAT IS A SPEC LINE
   OVERRULED ON PURPOSE. docs/design/DESIGN.md §/wallet asked for "`+` in --accent, `−` in
   --ink". The surface doc had already ruled the same three rows — `delta > 0` --good, `< 0`
   --dim, `=== 0` the --dim base — and that table is not a paint preference: it is the record
   of defect #1/#5 (*"a zero rendered in the positive colour"*, forage-wzm, the same repair as
   `.sbstanding`), and tools/wallet_e2e.sh measures each of the three IN A BROWSER because
   "what colour is this element" is a cascade question. Shipping accent/ink meant either a
   money gate rewritten to match whatever the paint happened to be, or a documented defect law
   with nothing behind it. A one-line paint preference does not outrank either.
   ⚠ THREE STATES, NOT TWO. A zero delta earns NEITHER class and falls through to this --dim
   base rather than borrowing the earned green. `.is-out` restates --dim rather than
   inheriting it, exactly as app-rank.css's original did, so the neutral case cannot become
   green by an accident two sheets away. */
.pns-wallet-amt{margin-left:auto;flex:0 0 auto;color:var(--dim);
  font-variant-numeric:tabular-nums}
.pns-wallet-amt.is-in{color:var(--good)}
.pns-wallet-amt.is-out{color:var(--dim)}
.pns-wallet-what{color:var(--ink);min-width:0;overflow-wrap:anywhere}
.pns-wallet-subj{color:var(--dim);min-width:0;overflow-wrap:anywhere}

/* ⚠ EVERY LEDGER ROW HAS THE SAME ANATOMY, AND ON A PHONE THAT COSTS TWO LINES. Left to the
   log row's own wrapping, a short description keeps its amount on line one and a long one
   pushes it to line two — so a column of six rows shows two different shapes and the eye
   reads the difference as meaning. Photographed at 390 on the first pass of this repaint.
   Below the primitive's own phone rung the amount is pinned to the END of line one, beside the
   time and the lozenge, and the description always takes line two. Same DOM, same order for a
   screen reader (`order` moves the pixel and leaves the announcement alone), one shape. */
@media(max-width:640px){
  .pns-wallet-amt{order:1}
  .pns-wallet-what{order:2;flex:1 1 100%}
  .pns-wallet-subj{order:3;flex:1 1 100%;margin-top:-2px}
}
/* ▲▲▲ /wallet ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /alerts — THE NOTIFICATION ARCHIVE (page-alerts.js) ▼▼▼  forage-qw2w.6.2
   docs/design/DESIGN.md §/alerts. ONE panel; the h1 is its tab label, the relative/exact
   switch is a lozenge in its meta slot, and the fresh/viewed split stays page structure as
   the primitive's group header.

   ⚠ web/alerts.css IS A PAGE SHEET AND IT LOADS AFTER THIS ONE (alerts.html links it below
   the shell fragment, the casino.css precedent). So a rule here loses every tie to it, and
   every rule below either names a class alerts.css does not write or out-specifies it on
   purpose. ⛔ Do not "fix" that by moving alerts.css into CSS_ORDER — cssparts.test.js pins
   that list against all 18 pages in both directions and an eleventh shared sheet would have
   to load on every screen in the archive.

   ⚖ WHAT WAS NOT TOUCHED: the row's three tracks (avatar with its verb badge · the sentence,
   excerpt and time · the subject thumbnail), the aggregate collapse, the dead-plate state for
   an unavailable subject, and the per-row time buttons. docs/surfaces/alerts.md pins all of
   it, and docs/design/DESIGN.md's §/alerts was written from the EMPTY screen — it describes
   a screen with a title, a toggle and a sentence on it. Applying its row description literally
   would have deleted the avatar, the badge and the thumbnail, which is a regression wearing a
   spec's clothes. The spec's own first rule wins: every datum shown today is shown after.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* the h1 as the tab label. `.pn > .pn-tab` positions it; this only takes the heading margin
   off, because a browser's default h1 margin on an absolutely-positioned box still offsets it. */
/* ⚠ THE CHILD COMBINATOR IS NOT DECORATION HERE. app-panel.css writes `.pn > .pn-tab` and
   `.pn > .pn-meta` — TWO classes each — so a bare `.pns-alerts-meta` loses the tie and its
   `display:flex` never lands. That is a silent loss: the slot still renders, the two children
   just sit against each other with no gap, which is exactly what the 1440 picture showed on
   the first pass. Matching the specificity with this screen's own two classes is what wins it,
   and app-screens.css is read after app-panel.css so the tie goes to this file.
   ⛔ `.pn.pns-alerts > …` would be the obvious spelling and it is FORBIDDEN — the DUP gate
   scans the whole prelude for `.pn` and would call this sheet a second panel definition. */
.pns-alerts > .pns-alerts-tab{margin:0;font-size:11px;font-weight:400}

/* THE META SLOT holds two things, so it is a row rather than a line of text.
   ⚠ overflow:visible ON PURPOSE. The primitive clips its meta at ≥1000px because a readout
   that wraps hangs over the tab label; here the slot holds a bordered button that is taller
   than the 11px line, and the clip would shave its top and bottom hairline off. */
.pns-alerts > .pns-alerts-meta{display:flex;align-items:center;justify-content:flex-end;
  gap:12px;overflow:visible}
.pns-alerts-count{white-space:nowrap}
/* ⚑ DENSITY, forage-pia1.5 — THE READOUT AND THE FIRST GROUP'S HEADER SHARE ONE LINE BELOW THE
   RUNG. Measured at 390 on the real app: the slot is a 21px line and `FRESH 2` is a 24px line
   directly under it, 45px of panel spent on two facts of one word each before the first alert.
   ⚖ ONE LINE, BOTH FACTS, NEITHER SHRUNK: `FRESH 2` keeps its size, its rule and its place at
   the panel's left edge, the readout and its switch keep the right edge and the 44px target,
   and the panel gives back 24px. Above the rung the slot is absolutely positioned on the
   border and there is nothing to share, so this is scoped to the phone.
   ⛔ A FLOAT, AND NOT A NEGATIVE MARGIN, AND THE 320px COLUMN IS WHY. Measured: at 320 the
   readout's own ink (`2 UNREAD` + the switch, 205px) starts 70px into a 258px content box and
   `FRESH 2`'s ink ends at 89 — a lift would print one over the other on the narrowest phone
   anybody holds. `.al-sec-h` is a flex container, so it establishes its own formatting context
   and is placed BESIDE the float with its width shortened rather than under it; when there is
   no room left it drops below the float on its own, which is last week's layout and not a
   defect. Nothing after it needs a `clear`: the float is 21px and the header it sits beside is
   24px, so the log already starts below it.
   ⚠ `margin-left` IS THE GUTTER BETWEEN THE TWO, not decoration — without it a long group name
   would touch the readout's first character. */
@media(max-width:999px){
  .pns-alerts > .pns-alerts-meta{float:right;margin-left:16px}
}
/* ⚠ THE SLOT IS ABSOLUTELY POSITIONED AT ≥1000px AND ITS RIGHT EDGE IS THE PANEL'S. Without
   this the button's own border sits flush against the panel border and reads as part of it —
   photographed at 1440 on the first pass. */
@media(min-width:1000px){ .pns-alerts > .pns-alerts-meta{padding-right:4px} }
/* THE SWITCH, drawn as a lozenge WITHOUT wearing `.pn-loz` — see page-alerts.js for why the
   shared chip may not be hung on a <button>. Same shape, this screen's class. */
.pns-alerts-toggle{position:relative;font:inherit;font-size:10px;line-height:1.5;
  padding:2px 8px;color:var(--dim);border:1px solid var(--hair);background:var(--sunk);
  cursor:pointer;white-space:nowrap;text-transform:var(--caps);
  letter-spacing:calc(.1em * var(--track))}
.pns-alerts-toggle:hover{color:var(--ink);border-color:var(--hair-hi)}
.pns-alerts-toggle.is-on{color:var(--accent);
  border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
/* ⚖ THE 44px FLOOR IS SPENT AS AN OVERHANG, NOT AS A FATTER BOX (app.css `.btn`, the house
   shape; docs/archive/DECISIONS.md "The floor may not change what a member can see" and the
   owner's *"Smaller controls are fine I hate the massive boxes that rule has made"*). The chip
   stays 19px on the panel's edge and the TARGET is 44px tall. ⚠ `left:0;right:0` and never a
   width: a horizontal halo would overhang the unread count beside it and eat its taps — and it
   is why `.pns-alerts-meta` above sets `overflow:visible`, or the slot clips this away. */
@media(pointer:coarse){
  .pns-alerts-toggle::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}

/* ⚠ RESERVE WHAT LIGHTS (the primitive's fourth law). `.al-row.fresh` takes a 2px accent
   border-left; a viewed row with none is 2px wider, and now that both groups live in ONE
   panel the two edges sit in one column and the step is visible. Every row reserves it. The
   two-class selector is what beats the bare `.al-row` rule in the later sheet, and
   `.al-row.fresh` is two classes too, so the accent still wins where it should. */
.pns-alerts-log .al-row{border-left:2px solid transparent}

/* THE GROUP HEADER, at the primitive's weight. ⚠ alerts.css draws `.al-sec-h` at 12px BOLD
   with the count in --faint — right when it was a free-standing section title on the page,
   wrong now that the panel's tab label is the heading and this is a rule ACROSS it. Two
   headings competing for the same job is the row-of-same-weight-words fail with two rows.
   The two-class selectors are what beat the later sheet. */
.pns-alerts .al-sec-h{font-size:11px;font-weight:400;color:var(--dim)}
.pns-alerts .al-sec-t{color:var(--dim)}
.pns-alerts .al-sec-n{color:var(--faint);font-variant-numeric:tabular-nums}
/* a group that FOLLOWS a list of rows needs air above it, or "viewed" reads as the last row's
   caption. The first group needs none — the panel's own padding is above it. */
.pns-alerts-log + .al-sec-h{margin-top:22px}

/* the load error, in the empty slot's position but not in its colour. It is the one --bad
   thing on this screen and it is a real alarm: paging has STOPPED, and a --dim sentence
   saying so reads as an ordinary empty state (docs/surfaces/alerts.md's own defect list). */
.pns-alerts-err{color:var(--bad)}

/* the footer — "older alerts", or the sentence that says the history really ended. */
.pns-alerts .al-more{display:flex;justify-content:center;padding:14px 0 2px;margin:0}
/* ▲▲▲ /alerts ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /leaderboard — THE ASCENT (page-leaderboard.js) ▼▼▼  forage-qw2w.6.3 + forage-qw2w.2.5
   docs/design/DESIGN.md §/leaderboard. Two panels: THE CLIMB (a ranked list where the score
   is a bar against the leader's as well as a number) and CROWNS (thirteen cells in ONE box,
   not thirteen dashed boxes floating on the page).

   ⚠ web/app-rank.css HOLDS `.roster-*` AND `.apex-frame` AND IT LOADS AFTER THIS SHEET. Ties
   go to it, so the two overrides below are written with this screen's own class in front —
   `.pns-lb-crown .roster-row` (0,2,0) beats `.roster-row` (0,1,0) wherever the cascade cannot.
   ⛔ `.roster-row.vacant`'s dashed border is 0,2,0 in the LATER sheet and is NOT overridden on
   purpose: the spec asks a vacant crown for "a dashed inner border", which is exactly what it
   already draws, and the cell around it is the outer box it was always missing.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* the `#count` result bar. Offscreen, not absent — app-admin.css's `.as-h` idiom, the house
   spelling of "reachable, not shown". The words are on the CLIMB panel's edge now. */
.pns-lb-count{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}

/* ── THE CLIMB ROW ──────────────────────────────────────────────────────────────────────── */
.pns-lb-place{flex:0 0 2.2em;color:var(--dim);font-size:12px;
  font-variant-numeric:tabular-nums}
/* the podium is the only place a rank NUMBER takes the accent — three rows out of fifty. */
.pns-lb-row.is-podium .pns-lb-place{color:var(--accent)}
/* ⛔ NO COLOUR HERE, AND THAT IS THE POINT. `.ulink{color:var(--accent)}` lives in
   app-profile.css, later in the cascade and also (0,1,0), so a one-class colour written on a
   username in THIS sheet ties it and loses on source order — spelled correctly, painting
   nothing (ulinkcascade.test.js; the forage-fk5 class, which has shipped twice, once on this
   very screen). A username on the ascent wears whatever a username wears everywhere else in
   the archive, which is what it wore before this repaint. All this rule does is let the name
   yield its width to the bar beside it — `.lb-name` already truncates with an ellipsis. */
.pns-lb-name{min-width:0}
/* ⚖ THE 44px FLOOR, SPENT AS AN OVERHANG (forage-qw2w.18.1). uxprobe measured this link
   DRAWN AT 20px on 32 rows at 390x844 — the only link in the row, and the way to a
   climber's profile. ⛔ `mobiletaps.test.js` is structurally blind to it: its "the author
   owns this box" branch fires on a rule stating a padding AND a font-size, and the rule
   above states neither, so the census never asks. The harness that measures DRAWN boxes is.
   ⚠ AN OVERHANG, NOT A FATTER ROW (app.css `.btn`, the house shape; the owner's *"Smaller
   controls are fine I hate the massive boxes that rule has made"*). The name stays 20px and
   the TARGET is 44px.
   ⚠ `left:0;right:0` AND NEVER A WIDTH — a horizontal halo would overhang the tier and the
   score beside it and eat their taps, which is the note `.pns-alerts-toggle` carries.
   ⚠ AND THE VERTICAL REACH IS SAFE HERE, MEASURED: the row pitch at 390 is ~48px (two
   lines, the bar taking the second), so a 44px halo centred on the name clears the halo of
   the row above and the row below by ~4px. It overhangs this row's own gauge, which is a
   `<span>`, not a control. */
.pns-lb-name{position:relative}
@media(pointer:coarse){
  .pns-lb-name::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
.pns-lb-tier{flex:0 0 auto;font-size:10px}
.pns-lb-score{flex:0 0 auto;color:var(--bright);font-variant-numeric:tabular-nums}
/* the bar takes whatever the row has left. ⚠ `min-width` is what stops a long name from
   collapsing it to nothing — a gauge drawn 3px wide is a smear, not a comparison. */
.pns-lb-gauge{flex:1 1 90px;min-width:56px;align-self:center}
/* everyone who is not the leader. The gap above you is the point, so the bars below the top
   are the quiet --hair-hi and only the leader's is lit. */
.pns-lb-fill{background:var(--hair-hi)}

/* ⚠ ONE ROW ANATOMY AT 390, TWO LINES. Five tokens do not fit on a phone line, and left to
   wrap the gauge lands in a different place on every row — the same mixed-anatomy fail the
   wallet's ledger had. Below the phone rung line one is place · name · tier · score and the
   BAR takes line two, full width, on every row without exception. `order` moves pixels and
   leaves the announced order alone. */
@media(max-width:640px){
  .pns-lb-tier{order:1}
  .pns-lb-score{order:2;margin-left:auto}
  .pns-lb-gauge{order:3;flex:1 1 100%;min-width:0}
}

/* ── THE CROWNS GRID ────────────────────────────────────────────────────────────────────── */
/* ⚠ THE LAST CELL SPANS THE ROW, AND THAT IS A HOLE FIX, NOT A FLOURISH. `.pn-cells` draws its
   separators as a 1px gap over the container's --hair ground, so a grid TRACK with no cell in
   it renders as a SOLID BLOCK of --hair — the primitive's own header says so. The roster is
   thirteen crowns and thirteen is prime: it leaves a remainder at two columns and at three.
   `grid-column:1/-1` on the last child fills whatever the last row has left at any column
   count and any breakpoint, without the sheet having to know either. */
.pns-lb-crown:last-child{grid-column:1/-1}

/* ⚑ THE 2px SIDEWAYS SCROLL IS FIXED BY THIS LAYOUT, AND BY NOTHING ELSE IN THIS SHEET
   (forage-qw2w.2.5, found by monkey.js at 390x844 — five hits in an 800-action run, only ever
   after tapping a crown, which is why the resting scan never saw it). The reigning crown's
   corona is `.apex-frame > .prismglow{inset:-16px}`: sixteen pixels of deliberate bleed
   outside the row, in a `.roster-cell` nothing clipped, sitting in a grid that ran to the edge
   of `.wrap`. At the right-hand column that bleed widened the DOCUMENT.
   The crowns are cells inside a PANEL now, so the nearest cell edge is the panel's 16px plus
   the cell's own padding inside the wrap's — the corona has more than sixteen pixels of room
   before it can reach the viewport, at both column counts.

   ⚠ MEASURED, WITH A CONTROL, on the real app at 390x844 with two crowns actually held
   (.scratch/laneF/drive.js --screen leaderboard):
       rest                                   scrollWidth 390 / client 390
       with a crown's panel open              390 / 390
       CONTROL: same page, padding stripped   395 / 390
   The control is the point — a reading of 390 from an instrument that cannot see 395 would
   mean nothing.
   ⛔ AND `overflow:clip` ON THIS GRID WAS TRIED AND REMOVED. It measured 395/390 in the
   control too: `overflow-clip-margin` has to be wide enough for the corona to paint (owner
   ruling 2026-07-20i is that the glow ESCAPES — clipping it away is reversing a ruling), and
   a clip margin the corona fits in is a clip margin the overflow fits in. It looked like a
   belt-and-braces guarantee and it was a comment. The padding is the fix; do not re-add it. */

/* the crown row inside its cell. The cell is the box now, so the row stops being one: no
   radius (the references have no rounded rectangle anywhere), no padding of its own. */
.pns-lb-crown .roster-row{border-radius:0;padding:0;background:none}
/* ⚠ AND THE DASH COMES OFF — THE "FOUR NESTED FRAMES" HALF OF forage-qw2w.36, WHICH SURVIVED
   THE FIRST PASS AND WAS STILL ON THE PHOTOGRAPH THREE DAYS LATER. Owner review 2026-09-05:
   *"every cell is a DASHED box inside a solid cell inside a solid grid inside a bracket panel
   — four nested frames per crown."* The rule above took the row's radius, its padding and its
   ground and simply did not name its BORDER, so `.roster-row.vacant{border:1px dashed}`
   (app-rank.css) kept painting — same specificity, and app-rank.css loads AFTER this sheet
   (server/tests/cssparts.js), so the dash won every tie. Measured on master at 390x844 and
   1440x900: thirteen dashed rectangles, one inside each of thirteen solid cells.
   ⚖ `.vacant` IS IN THE SELECTOR ON PURPOSE — 0,0,3,0 beats it wherever either sheet sits, so
   this cannot be undone by a future reordering the way the first attempt was.
   ⚠ NOTHING IS LOST WITH IT: vacancy is still said twice inside the cell, by the title dropping
   to --faint and by the holder line reading `vacant` in italic --faint. The dash was carrying
   no information, only a frame.
   ⚑ SCOPED TO THIS SCREEN, and that is load-bearing. `.roster-row.vacant` is also the PROFILE
   crown wall (`.crown-g`, app-profile.css), where the row IS the box and the dash is the only
   thing drawing it — a bare `.roster-row.vacant{border:0}` would erase that wall's grid. */
.pns-lb-crown .roster-row.vacant{border:0}
/* ⚠ 390 IS THE PRIMARY PICTURE AND TWO COLUMNS OF IT ARE 160px EACH. `.roster-title` clips
   with an ellipsis rather than wrapping (app-rank.css, so every card stays the same two-line
   height), so every pixel the cell's own padding takes comes straight off the crown's NAME —
   "the archivist" was losing its tail. The cell keeps the primitive's padding everywhere else. */
@media(max-width:640px){
  .pns-lb-crown{padding:10px 9px 11px}
  .pns-lb-crown .roster-row{gap:8px}
  .pns-lb-crown .roster-ic{font-size:17px}
}
/* ═══ ⛔ THE CROWNS ARE EXEMPT FROM THE MONOCHROME PALETTE — OWNER RULING 2026-09-08 ═══════════
   *"the crowns are special, they have a unique theme."* They keep the vendor's own colours, on
   every surface, and that is a RULING and not an oversight to tidy up later.

   ⚑ THIS PARAGRAPH REPLACES ONE THAT ARGUED THE OPPOSITE, so read the history before you
   "fix" this again. forage-qw2w.36 recorded the owner counting thirteen glossy multi-coloured
   vendor glyphs in a hairline-and-corner-bracket design whose palette is amber on near-black,
   and a lane put them in the palette with a filter chain
   (`grayscale sepia saturate hue-rotate brightness contrast`). The reasoning was sound and the
   conclusion was wrong: the review said the crowns did not MATCH, and the answer to that is
   not always to make them match. Their being outside the palette is the point of them.
   ⚠ AND THE FLATTENING WAS THE REAL LOSS — `contrast(.85)` was there deliberately to kill the
   vendor's moulded shading, which is exactly the prismatic read the owner is protecting.

   ⛔ DO NOT re-tint these glyphs, on this surface or any other, without a new owner ruling:
   /leaderboard (`.pns-lb-crown .roster-ic`), the profile crown wall (`.crown-g`) and the mark
   before a name (`.apex-crown`, app-rank.css) are one decision, not three. */
.pns-lb-crown .roster-title{color:var(--dim)}
.pns-lb-crown.is-faint .roster-title{color:var(--faint)}
/* ▲▲▲ /leaderboard ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /collections — THE MIXTAPE SHELF (page-collections.js, renderList) ▼▼▼  forage-qw2w.6.4
   docs/design/DESIGN.md §/collections. ONE panel: the create row as a single hairline
   control, then every collection as a cell in one box. The ?id= detail view is untouched.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* THE CREATE ROW — the input and its button are ONE box. The button is the input's right cap,
   so there is no gap to explain and no second border to align. `.nag-addform` (app-admin.css,
   the layout this reused) still supplies nothing this needs, so everything is stated here. */
.pns-coll-new{display:flex;align-items:stretch;gap:0;margin:0 0 14px}
.pns-coll-input{flex:1 1 auto;min-width:0;font:inherit;font-size:12px;
  padding:0 12px;min-height:var(--tap-min);
  color:var(--ink);background:var(--sunk);border:1px solid var(--hair);border-right:0}
.pns-coll-input::placeholder{color:var(--faint)}
.pns-coll-input:focus{outline:none;border-color:var(--hair-hi)}
/* the cap. ⚠ `border-left:0` would leave the seam invisible at the join; the input drops ITS
   right border instead, so the two share exactly one hairline.
   ⚖ AND IT IS NOT THE FILLED PRIMARY ANY MORE (forage-qw2w.37). A solid amber block made
   "make another one" the loudest object on a screen whose actual primary action is OPENING a
   collection — and the destructive control was the neutral one, which is the same complaint
   read from the other end. The card is the primary affordance now; this is the hairline cap
   of the field it belongs to, and the accent on this screen is spent on the counts and the
   panel's tab label. `.is-primary` stays OFF the element rather than being overridden here,
   so the primitive's filled button is still exactly one thing. */
.pns-coll-add{flex:0 0 auto;min-height:var(--tap-min);border-radius:0}
.pns-coll-msg{display:block;margin:-8px 0 12px;color:var(--bad)}

/* ⚠ THE GRID'S SEPARATORS ARE PER-CELL, NOT THE CONTAINER'S GROUND, AND THAT IS A HOLE FIX.
   `.pn-cells` paints the container --hair and lets a 1px gap show it through — perfect for a
   FIXED set of cells, which is why the crowns region above uses it as it ships. A collection
   count is whatever the member has made, so any partial last row would render as a solid
   --hair block (the primitive's own header calls this out). Here the container wears the
   panel's own --sunk and each cell casts a 1px --hair ring instead: two neighbours' rings
   share the one-pixel gap and read as a single hairline, the outer ring reads as the box, and
   a track with no cell in it is simply the panel — no hole at any count or breakpoint.
   ⛔ Not `border` on the cell: adjacent borders would double to 2px in the gap. */
.pns-coll-grid{background:var(--sunk);border:0;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
/* ⚑ ONE ALIGNMENT FOR THE WHOLE CARD (forage-qw2w.37). The first build centred the count in
   its block while the name, the date and the button below it all sat left — three alignments
   inside one 400px card, and the centred element is the one that reads as broken. Everything
   in the cell is flush with the cell's own padding edge, top to bottom, and the cell says so
   ONCE, here, rather than each child deciding for itself.
   ⚠ `position:relative` IS LOAD-BEARING, NOT TIDINESS. It is what the name's stretched-link
   ::after resolves `inset:0` against — the whole point being that the CARD is the tap target.
   Put `position` on the link instead and the ::after covers the word it came from, which
   looks identical in a screenshot and leaves the card dead everywhere else. */
.pns-coll-cell{position:relative;display:flex;flex-direction:column;gap:8px;
  align-items:stretch;text-align:left;box-shadow:0 0 0 1px var(--hair)}

/* THE PLATE — the mosaic's slot, standing in with the count until there are covers to draw.
   ⚠ IT IS NOT THE CARD'S HEADLINE ANY MORE. It was a 24px accent number in a box twice the
   height of everything else, above a 12.5px name — the hierarchy inverted against the only
   reason the card exists. The number lost six pixels, the box lost half its padding, and the
   NAME took the size and the --bright it should always have had. */
.pns-coll-plate{display:flex;align-items:baseline;gap:7px;
  padding:8px 10px;background:var(--void);border:1px solid var(--hair)}
/* ⚠ NEVER LARGER THAN THE NAME. The finding was that the name is "rendered smaller and
   quieter than its post count"; leaving the count the bigger NUMBER answers half of it and
   leaves the half a ruler can measure. They are the same size, and what separates them is
   role: --bright prose against an --accent figure with a caption. */
.pns-coll-n{font-size:16px;line-height:1.1;color:var(--accent);
  font-variant-numeric:tabular-nums}
.pns-coll-unit{font-size:10px;color:var(--dim);text-transform:var(--caps);
  letter-spacing:calc(.12em * var(--track))}
/* ⚠ AN EMPTY COLLECTION GOES QUIET, AND IT HAS TO SAY SO ITSELF NOW. `.pn-cell.is-faint
   .pn-num` was doing this while the count wore the primitive's class; the count is this
   screen's own element since the alignment fix, so the faint case moved with it — otherwise
   a collection holding nothing shouts the same accent zero as a full one holds its seven. */
.pns-coll-cell.is-faint .pns-coll-n{color:var(--faint)}
.pns-coll-cell.is-faint .pns-coll-name{color:var(--ink)}

/* ⚖ THE NAME IS THE HEADLINE AND THE WHOLE CARD IS ITS TAP TARGET. The ::after is the
   stretched-link idiom: one real <a> in the accessibility tree, valid markup, keyboard
   reachable, and every pixel of the cell opens the collection. ⛔ It must stay UNPAINTED —
   a background or a border here would sit over the card's own contents. */
.pns-coll-name{color:var(--bright);font-size:16px;line-height:1.35;
  text-decoration:none;overflow-wrap:anywhere}
.pns-coll-name::after{content:"";position:absolute;inset:0;z-index:1}
.pns-coll-cell:hover .pns-coll-name,
.pns-coll-name:focus-visible{color:var(--accent)}
/* the cell answers the hover too, so the target reads as the whole card and not as the word. */
.pns-coll-cell:hover{background:var(--panel)}

/* ⚠ NOT UPPERCASE. It is a date, not a label, and the card already carries a POSTS caption. */
.pns-coll-age{font-size:10.5px;color:var(--dim)}

/* the controls, at the foot, on the same edge as everything above them.
   ⚠ `position:relative` + a z-index ABOVE the stretched link, or the ::after covering the
   card would swallow every tap meant for delete. That is the one thing the stretched-link
   idiom gets wrong when it is copied without its second half.
   ⚖ DESTRUCTIVE IS SMALL AND LAST. Opening the collection is the card itself now, so these
   are hairline controls rather than the only thing there is to do here; `.is-del` is the
   marker, and it takes --bad only on hover — a wall of red delete buttons at rest is the
   opposite of reserving the colour for an alarm. */
.pns-coll-acts{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:4px;
  position:relative;z-index:2}
.pns-coll-act{flex:0 0 auto;border-radius:0}
.pns-coll-act.is-del:hover{color:var(--bad);border-color:var(--bad)}

/* ▲▲▲ /collections ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /data — THE ARCHIVE ASKING (page-data.js) ▼▼▼  forage-qw2w.6.5
   docs/design/DESIGN.md §/data. The deck's card sits IN a panel: the tab names it, the
   backlog count is the meta, and the card stops drawing a second box inside the first.

   ⛔ web/tagpoll.js AND web/tagpoll.css ARE NOT TOUCHED. That card is the component /queue
   and /collective interleave, and its answer path is the one that pays the arcade
   (docs/surfaces/data.md; docs/archive/DECISIONS.md "The feedback screen pays the ARCADE").
   Every rule below that names a `.tp-*` or `.q*` class is scoped under this screen's own
   `.pns-data*`, so the other two surfaces drawing the same card see none of it. That scoping
   is also what wins the tie: tagpoll.css is a PAGE sheet and loads after this one, so a bare
   `.tp-card` here would be spelled correctly and paint nothing.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* the head above the panel: the back link alone now, because the backlog count moved to the
   panel's meta slot and two readouts facing each other across a bare row was the row of
   same-weight words this repaint is against. */
/* ⚖ 12px WAS 12px OF ROOM FOR A LINK WITH NO BOX, AND THE LINK HAS A BOX NOW (forage-qw2w.34).
   `.qback` (app-feeds.css) is a hairline control on all three deck surfaces since that bead —
   26px drawn instead of 17 — so this row pays 6 of the 9 new pixels back out of its own gap
   rather than charging them to the stage. /data's head: 17+12 = 29px before, 26+6 = 32px after.
   ⚠ THE DECK STILL DOES NOT SCROLL (tagpoll.css, page-data.js's own header, owner 2026-08-23),
   so this number is a budget and not a preference. ⛔ Do not spend it on a drawn 44px control:
   that is 15px off a card that has none to give. The 44px TARGET is the coarse-pointer overhang,
   which moved to `.qback` with the box and covers /queue and /collective too — they had none.
   ⚰ `.pns-data-back` HAD ITS OWN `position:relative` AND ITS OWN HALO HERE (forage-qw2w.18.1)
   and both are gone, not lost: `.qback` declares them, and `.pns-data-back` was never anything
   but the /data-scoped name for the same element. Keeping the pair would have been the DUP the
   bead above is about, one file apart. */
.pns-data-top{padding:0 0 6px}

/* ⚠ ONE BOX, NOT A BOX IN A BOX. `.tp-card` carries its own border, radius and --sunk ground —
   right when it is a card in a scrolling feed, wrong inside a panel that already is one. The
   panel is the box; the card keeps every other thing about itself, including its own layout,
   its media sizing and its answer row. */
.pns-data-host .tp-card{border:0;border-radius:0;background:none;padding:0}
.pns-data-host{margin:0}

/* ⚑ AND THE PANEL MUST FILL THE DECK, OR A TALL CARD BECOMES UNREACHABLE. tagpoll.css builds
   the deck out of one flex chain — `html.datadeck body` → `.datawrap` → `.qpollhost` (which is
   the only thing on the page allowed to scroll) → `.tp-card` — because the DOCUMENT does not
   scroll here at all (page-data.js's own header: `overflow:hidden`, owner 2026-08-23 *"why can
   I scroll only to show dead space"*). Dropping a panel between `.datawrap` and `.qpollhost`
   broke that chain: the panel sized to its content, the host stopped being the flex child that
   grows, and a confirm card with a description AND six example thumbnails had its bottom — the
   ANSWER BUTTONS — below a viewport that cannot scroll. Photographed as "the dead space is
   gone", which is what it looks like right up until a card is taller than a phone.
   ⚠ `min-height:0` ON EVERY LINK OF THE CHAIN. A flex item's default `min-height:auto` refuses
   to shrink below its content, which is how one forgotten declaration turns an internal
   scroller back into an overflowing page. */
.pns-data{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}

/* the meta slot holds a sentence rather than a figure, so it may not be clipped to the
   primitive's ellipsis at ≥1000px — the number is the half that would go. */
.pns-data > .pns-data-meta{max-width:none;overflow:visible}

/* THE CAUGHT-UP SCREEN — the one status word, in the grammar's own weight. */
.pns-data-caught{padding:26px 8px 8px;text-align:center}
.pns-data-caught .qcaught-mark{color:var(--good)}
.pns-data-caught .qcaught-sub{max-width:44ch;margin-inline:auto}
.pns-data-caught .qcaught-stats{margin-top:14px}
.pns-data-caught .qcaught-acts{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;
  margin-top:18px}
/* ⛔ NOT `.pns-data-caught .qcaught-acts .pn-btn`, WHICH IS WHAT THIS FIRST SAID AND WHAT
   panelprimitive.test.js caught. The DUP gate reads the WHOLE prelude, so naming a primitive
   class as a descendant is an offence even three classes deep and even scoped to one screen —
   the rule this file's own header states, broken by its author four regions later. The
   deviation goes on the element. */
.pns-data-act{border-radius:0}

/* ═══ THE TWO TABS AND THE ANSWERS TAB (owner ruling 2026-09-13, page-data.js `dtaTabs`) ═══
   ⚖ TWO SEPARATE TABS, OPPOSITE THE BACK LINK, IN THE PROFILE TAB SET'S GRAMMAR (`.pns-u-tabs .prof-tab`):
   hairline `.pn-btn`s, the current one `.is-primary` — filled accent — and each 44px on a thumb, which
   `.pn-btn` already is on a coarse pointer. ⛔ The first cut put both words in the panel's one tab label
   and it read as a single run-on heading. */
.pns-data-tabs{display:flex;gap:6px;flex:0 0 auto}
.pns-data-tab{border-radius:0;white-space:nowrap}
/* the answers tab scrolls INSIDE the panel — the document never scrolls on /data (the deck's chain above) */
/* ⚖ DENSE ON PURPOSE (coordinator review 2026-09-13: the populated tab was three tall cards). A row is
   one or two lines beside a 32px thumbnail, the standing is one quiet line, and a group header costs a
   single 11px line — so most of a member's record is on the first screen at 390. */
.pns-data-rec{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:4px}
.pns-data-standing{margin:0 0 2px;font-size:12px;line-height:1.45;color:var(--dim)}
.pns-data-grp{padding:8px 0 2px;margin:0;border-bottom:0}
/* ⚖ A ROW IS ONE LINK: the post's thumbnail (or a `#` mark for a tag), the sentence, the date. The row
   primitive's own paint (`.pn-log > .pn-row`) stays; this only makes the anchor look like a row. */
/* ⚠ ITS OWN CLASS, NOT `.pn-row`: a row here is a LINK, and the log row primitive is a line of text —
   borrowing it made mobiletaps.test.js count every log row on the site as an undersized control. The
   paint is the primitive's (hairline between rows, 11.5px), and on a thumb the row is 44px tall. */
.pns-data-row{display:flex;align-items:center;gap:10px;padding:5px 10px 5px 6px;min-width:0;
  font-size:11.5px;line-height:1.35;color:var(--ink);text-decoration:none}
.pns-data-row + .pns-data-row{border-top:1px solid var(--hair)}
.pns-data-row:hover{background:var(--panel)}
@media(pointer:coarse){.pns-data-row{min-height:var(--tap-min)}}
.pns-data-thumb{flex:0 0 32px;width:32px;height:32px;overflow:hidden;background:var(--raised);
  display:inline-flex;align-items:center;justify-content:center;color:var(--dim);font-size:13px}
.pns-data-thumb img{width:100%;height:100%;object-fit:cover;display:block}
/* two lines at most: a long description is a link to the tag, where it reads in full */
.pns-data-txt{flex:1 1 auto;min-width:0;display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:2;overflow:hidden}
.pns-data-rec .pns-data-when{order:0;flex:0 0 auto;font-size:10px}
.pns-data-reclink{display:inline-block;margin-top:14px;color:var(--dim);font-size:12px;text-decoration:none}
.pns-data-reclink:hover{color:var(--bright)}

/* ⛔ THE THREE VERDICT BUTTONS STAY EQUAL, AND THIS IS A REFUSAL OF A SPEC LINE.
   docs/design/DESIGN.md §/data asks the answer controls for "--accent on the primary". There
   is no primary here: YES, NO and NOT SURE are three readings of one piece of evidence, and
   the answer is what releases an AI tag across the archive and what pays the arcade. Making
   one of them the filled accent button is a thumb on the scale of the evidence path the lane
   was told to dress and not to touch — a member in a hurry answers the bright one. So this
   region says NOTHING about `.tp-btn`. It is written down here because a rule that is absent
   on purpose looks exactly like one nobody got to. */
/* ▲▲▲ /data ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /u/:username + /profile — THE MEMBER CARD (page-user.js) ▼▼▼  forage-qw2w.9.1
   docs/design/DESIGN.md §/u. The panels are THE MEMBER (identity + the fact sentence as
   cells), SCORE (the three bars as gauges), and then whatever the tab renders — the crowns,
   the cabinet, the record, THE WALL, RECENT and STEAM — each wearing its own old heading as
   a tab label.

   ⚠ app-profile.css AND app-rank.css HOLD `.prof-*`, `.plaque-*`, `.spine-*` AND `.dsr-ev*`
   AND THEY LOAD AFTER THIS SHEET. Ties go to them, so every override below leads with this
   screen's own class — `.pns-u-score .spine-track` (0,2,0) beats `.spine-track` (0,1,0)
   wherever the cascade alone cannot. ⛔ Do not "simplify" one back to a bare selector.

   ⚖ NOTHING IS DELETED FROM EITHER SHEET. The plaque, the spine, the sections and the event
   rows are all still rendered by web/common-profile.js / web/common-profiletabs.js — files
   this screen may not edit, because the dossier rail and the hover card draw from the same
   builders. A revert of this commit drops the dressing in page-user.js and the old rules find
   their old markup again, untouched.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* "reachable, not shown" — a heading whose words now live on a panel's edge, or a medallion
   whose number is still in front of the username. The element and its text stay in the reading
   order; the pixels go. app-admin.css's `.as-h` is the house original. */
.pns-u-off{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}

/* ── THE MEMBER PANEL — GONE, BY OWNER RULING ───────────────────────────────────────────
   ⚖ 2026-09-05, on his phone: *"I like the overall changes, but not the top of the profile
   page. you should revert this portion of it."* Every rule that dressed the identity block is
   deleted rather than commented out — the bracketed avatar cell, the fact-sentence cells with
   their trust gauge, and the marks strip that gave the award glyphs their names. The plaque
   renders as common-profile.js's profPlaque() has always rendered it, which is what the ruling
   asked for, and page-user.js no longer touches it.
   ⛔ AND `.pns-col` / `.pn-ground` CAME OFF `.profile-view` WITH THEM. The column's 22px gap and
   the dot grid both sat on the root, whose FIRST child is the plaque, so either one left behind
   would have moved the identity block or painted a grid behind it. The ground moved to the tab
   CONTENT, where the panels that kept the new look actually live.
   ⚠ WHAT STAYS BELOW THE TAB ROW IS UNTOUCHED: the tab buttons, SCORE with its gauges, THE
   WALL, RECENT's log rows and the STEAM panel with its 16px padding edge (clubs_writes_e2e
   §12). This region is now everything from the tab row down. */

/* ── THE SCORE PANEL ────────────────────────────────────────────────────────────────────── */
/* the spine loses its own top margin: the panel's padding is the spacing now. */
.pns-u-score .prof-spine{margin-top:0;gap:14px}
/* ⚑ THE BARS BECOME GAUGES, AND THE COMMUNITY GRADIENT SURVIVES INTACT. Owner ruling
   2026-07-21h is three bars with three palettes and this repaint does not reverse it — what
   changes is the SHAPE: the rounded 10px pill on a sunk ground becomes the site's 6px square
   track on --hair, so a bar here and a bar on /wallet or /leaderboard are the same object.
   The fill's own background (the three gradients) is not named anywhere in this region. */
.pns-u-score .spine-track{height:6px;border:0;border-radius:0;background:var(--hair)}
.pns-u-score .spine-fill{border-radius:0}
/* the figure and its share, flush right and tabular, so three rows read as a column. */
.pns-u-score .spine-add{color:var(--bright);font-variant-numeric:tabular-nums}
.pns-u-score .spine-pct{margin-left:10px;color:var(--dim);font-variant-numeric:tabular-nums}
.pns-u-score .spine-head{gap:8px}
.pns-u-score .spine-lbl{margin-right:auto}
/* ⚖ THE TOTAL RIDES THE TAB LINE, in the accent, tabular. ⚠ THE SPEC SAID 36px AND IT IS 13:
   above 1000px the meta slot is ABSOLUTELY POSITIONED ON THE PANEL'S TOP BORDER (app-panel.css)
   and a 36px numeral there does not sit on the line, it sits over the panel and under the
   masthead. The figure is the accent and it is the largest thing on that edge; the 36px
   headline treatment belongs to a panel whose ONLY content is one number, which this is not. */
.pns-u-score > .pns-u-scoremeta{color:var(--accent);font-size:13px}

/* ── EVERY OTHER SECTION ────────────────────────────────────────────────────────────────── */
/* ⚑ THE TAB CONTENT IS THE COLUMN THE SECTION PANELS STACK IN, and it has to say so itself.
   `.pns-col` is on the view ROOT, whose children are three things — the member panel, the tab
   row and this box — so its 22px gap never reaches the panels INSIDE this box. Without this
   rule SCORE, THE WALL, RECENT and STEAM tile edge to edge and each tab label lands on the
   panel above it, which is exactly what the first 1440 picture showed. */
/* ⚠ TWO CLASSES, for the fourth time in this region: `.prof-tabcontent{display:block}` is
   (0,1,0) in the LATER app-profile.css and a one-class rule here loses the tie silently. */
.pns-u .pns-u-content{display:flex;flex-direction:column;gap:22px}
/* ⚠ AND THE OLD MARGIN GOES WITH IT: `.prof-sec{margin-top:22px}` (app-profile.css, a LATER
   sheet, so this needs two classes to win) on top of the gap is 44px between panels — twice
   what every other repainted screen has, and enough that the page stops reading as one stack. */
.pns-u .pns-u-sec{margin-top:0}
/* ⚑ THE STEAM CARD KEEPS ITS 16px PADDING, AND THAT IS A HARNESS INVARIANT, NOT A TASTE CALL.
   `tools/clubs_writes_e2e.js` §12's ALIGNMENT leg is a GEOMETRY assertion: it reads
   `getBoundingClientRect().left` off the header tile and off every visible child of
   `.prof-steam-body` in all three states, requires ONE value, and compares it to the card's own
   computed content edge — `card.left + borderLeftWidth + paddingLeft` — then pins that padding
   at 16. It exists because the owner measured three left edges on this card (2026-09-02: *"the
   steam card layout is pretty trash. why is nothing aligned?"*), and `pad` is in the expected
   string so the fix cannot rot into "aligned at zero".
   ⚠ THE FIRST CUT OF THIS REGION SAID `padding:0` AND BROKE THE DEPLOY. Alignment still held —
   `spread=0.00 | offpad=0.00` — because every row moved together; what changed was the value the
   harness pins, and it went RED on `pad=0` exactly as it was written to.
   ⚖ SO THE PANEL GIVES ITS HORIZONTAL PADDING TO THE CARD INSTEAD OF TAKING THE CARD'S. The
   card's 16px IS this panel's inset now, which is the same 16px every other panel on the screen
   uses, so the card reads as the panel's content rather than as a box inside a box — and the
   card's own edge, the thing the harness measures, is untouched. Only the paint that made it a
   second BOX comes off: the border, the ground, the radius and the margin the column owns. */
.pns-u-steam{--pn-pad:8px 0 0}
.pns-u-steam .prof-steam-card{border:0;background:none;border-radius:0;margin:0}
/* ⚑ AND THE CARD'S OWN WORD GOES, IN CSS AND NOT IN SCRIPT. common-profiletabs.js states the
   ruling — the card carries Valve's mark and its own name, and the archive does not put a
   second label above somebody else's product — and this panel keeps the promise by saying
   "steam" ONCE, on the panel's edge where every other section says its name. ⚠ It has to be a
   rule rather than a class added by the dressing pass: the card paints ASYNCHRONOUSLY, after
   PUT/GET /api/sched/me/steam answers, so the name element does not exist yet when the dressing
   runs and does not exist again after any press that repaints it. Photographed as "STEAM" on
   the edge and "Steam" in the card. */
.pns-u .prof-steam-name{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}

/* ── RECENT ─────────────────────────────────────────────────────────────────────────────── */
/* ⚠ `display:flex;flex-direction:column` FROM app-rank.css WOULD WIN THE TIE (later sheet,
   equal specificity), and a log whose rows are flex items has no hairline between them and no
   row padding — the whole anatomy the primitive draws. This screen's class takes it back. */
.pns-u .pns-u-events{display:block}
/* ⚠ THE ROW IS NOT THE LINK — `post 7` is (page-user.js says why, and the primitive's own
   example is an `a.pn-path` inside a `.pn-row`). So there is no row hover rule here: a whole
   row that lights under the pointer and is not clickable is a lie the cursor tells. */
.pns-u-ev{color:var(--dim)}
/* the delta, flush right, tabular. Up is the accent — earning is the ordinary case on this
   list. ⛔ DOWN IS NOT RED: a negative delta on your own ledger is a fact, not an alarm, and
   red spent here is red that says nothing when a real one lands. */
.pns-u-delta{margin-left:auto;color:var(--accent);font-variant-numeric:tabular-nums}
.pns-u-delta.is-down{color:var(--dim)}
/* the one lozenge tone the primitive has no name for — a comment is ordinary text weight. */
.pns-u-kind-ink{color:var(--ink)}

/* ── THE TAB ROW ────────────────────────────────────────────────────────────────────────── */
/* the row keeps its element and its handlers; the buttons wear the panel's button. ⚠ THE
   CURRENT ONE IS PAINTED FROM `.on` AND NOT FROM `.is-primary`, because paintActive()
   (common-profiletabs.js, a file this screen may not edit) toggles `.on` and nothing else — a
   class added once here would stay on the tab that was current when the page loaded. */
/* ⚠ THE ROW KEEPS ITS OWN MARGIN AGAIN. `.pns-col`'s 22px gap used to space this off the
   plaque above it and the content below; the ruling took that column off the root, so the row
   carries the spacing `.prof-tabs` always carried — which is also what makes the plaque above
   it land where it landed before the repaint. */
.pns-u .pns-u-tabs{border-bottom:0;padding-bottom:0;margin:16px 0 22px;gap:6px}
/* ⚑ PROTOTYPE forage-pia1.1 — THE BAND, NOT THE BUTTONS. At 390 seven tabs wrap to two rows of
   29px, and around those 64px the row spends 38 more in margin: 102px of a phone screen for a
   tab set. ⛔ THE BUTTONS ARE NOT TOUCHED — same size, same padding, same two rows, same tap
   floor (the pointer:coarse rule below is untouched); what closes is the air, to 22px. The row
   still reads as its own band because it is the only thing between two hairlines. */
@media(max-width:640px){ .pns-u .pns-u-tabs{margin:10px 0 12px} }
/* ⚠ AND THE BUTTON ITSELF NEEDS TWO CLASSES TOO. `.sortopt` (app-feeds.css, a LATER sheet)
   carries this button's whole look at (0,1,0), so `.pn-btn` alone loses the tie and the row
   rendered as .sortopt boxes with the panel button's padding fighting them — photographed at
   390 as a row of overlapping, clipped labels. */
.pns-u .pns-u-tabs .prof-tab{display:inline-flex;align-items:center;justify-content:center;
  background:var(--sunk);color:var(--ink);border:1px solid var(--hair);border-radius:0;
  padding:6px 12px;font-size:11px;line-height:1.4}
/* ⚑ AND THE `.sortopt` BAND IDIOM HAS TO BE UNDONE ON A THUMB, WHICH IS WHAT THE 390 PICTURE
   CAUGHT. app.css draws a coarse-pointer `.sortopt` as a 44px BOX whose visible 26px chrome is
   an absolutely-positioned `::after` band, and hands the row the growth back with
   `margin-block:-9px`. Give that element a real border and a real background as well and you
   get TWO boxes stacked and rows that overlap by nine pixels — photographed as a tab row with
   its labels clipped by the row above. The band goes and the margin goes with it; the panel
   button's own answer to the same question is simply to BE 44px on a thumb, which is what
   every other button in this grammar does. */
@media(pointer:coarse){
  .pns-u .pns-u-tabs .prof-tab{min-height:var(--tap-min);margin-block:0}
  .pns-u .pns-u-tabs .prof-tab::after{display:none}
}
.pns-u .pns-u-tabs .prof-tab:hover{color:var(--bright);border-color:var(--hair-hi)}
/* the current tab is the filled one — `.is-primary`'s paint, keyed off the class the renderer
   actually toggles. */
.pns-u .pns-u-tabs .prof-tab.on{background:var(--accent);color:var(--void);
  border-color:var(--accent);font-weight:700}
/* ▲▲▲ /u/:username ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /upload — THE INTAKE (page-upload.js) ▼▼▼  forage-qw2w.9.2
   docs/design/DESIGN.md §/upload. Five doors, five panels: DROP · PRIVACY · STORAGE ·
   IMPORT FROM A LINK · INSTAGRAM. Same controls, same handlers, same routing — addFiles()
   still decides bulk by COUNT and nothing here has an opinion about it.

   ⚠ app.css, intake.css AND app-moderation.css ALL HOLD THIS SCREEN'S CLASSES AND ALL LOAD
   AFTER THIS SHEET, and `#privrow` is written as an ID in intake.css. Ties go to them, so
   every override below leads with `.pns-up*` — and the private row's lead with the ID's own
   panel class, which makes it (1,1,0) against intake.css's (1,0,0).
   ⛔ Nothing is deleted from any of the three. Every element they style is still rendered by
   the same code; a revert of this commit drops the dressing and they find their markup again.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* the column the doors stack in. `#app` is the page's own `main.wrap.narrow`, which had no
   opinion about the gaps between its children — they were each carrying their own margin. */
.pns-up{display:flex;flex-direction:column;gap:22px;padding-bottom:40px}
.pns-up-off{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}

/* ── DROP ───────────────────────────────────────────────────────────────────────────────── */
/* ⚠ THE FORMAT LIST IS SEVEN TOKENS AND IT WRAPS AT 390. Below 1000px the meta slot is a
   right-aligned LINE inside the panel (app-panel.css), and the primitive's 20px of top padding
   over a two-line readout left a band of nothing between the tab and the door. The slot keeps
   its alignment — that is the house shape — and gives the padding back. (0,2,0), because
   `.pn > .pn-meta` is two classes and a one-class rule here would lose the tie silently. */
/* ⚑ AND THE FORMAT LIST WANTS ITS WHOLE LINE (forage-qw2w.33's third leg). Below 1000px the meta
   slot is a right-aligned LINE with 16px of side padding of its own, INSIDE the panel's 16px — so
   at 390 it had 292px for a 286px string and `WEBM` fell to a second line on its own, orphaned
   and right-aligned under the rest. The slot gives its side padding back (the panel already
   pads) and the seven tokens sit on one line. `text-wrap:balance` is the belt: if a skin's
   metrics or a longer list ever push it over again, it breaks 4/3 rather than 6/1. */
.pns-up-drop > .pns-up-dropmeta{padding:4px 0 0;line-height:1.7;
  letter-spacing:calc(.02em * var(--track));text-wrap:balance}
/* ⚖ THE DASHED BORDER STAYS AND THAT IS THE SPEC'S CALL: the door keeps its dashed inner edge
   INSIDE the panel, because a dashed rectangle is the one universal "put something here" and
   the panel's hairline is the box, not the invitation. What goes is its margin — the column
   owns the spacing now, and 14px of margin inside 16px of panel padding is 30px of nothing. */
.pns-up-drop .drop{margin:0}
/* ⚑ ONE SENTENCE PER DEVICE (forage-qw2w.33 leg 2, second cut). page-upload.js wraps
   upload.html's "drop files here, click to choose, or paste (⌘/ctrl-V)." in `.pns-up-mouse`
   and puts "tap to choose files." beside it in `.pns-up-touch`; this is the switch. The rung is
   the DEVICE, not a width — the same query the paste span below already answers to — so a
   narrow desktop window keeps its keyboard's sentence and a phone in landscape keeps its thumb's.
   The paste span's own rule stays: it is the rung 409065c5 shipped, and it still governs the
   mouse sentence on a touch laptop that reports hover. */
.pns-up-drop .pns-up-touch{display:none}
@media (hover:none) and (pointer:coarse){
  .pns-up-drop .pns-up-mouse{display:none}
  .pns-up-drop .pns-up-touch{display:inline}
}

/* ── PRIVACY ────────────────────────────────────────────────────────────────────────────── */
/* two switches, two rows, a hairline between them — and the admin sentence as the panel's last
   row. ⚠ (1,1,0): intake.css writes `#privrow` as an ID, which no plain class can outrank. */
/* ⚠ `align-items:stretch` IS NOT THE DEFAULT HERE. `.upload-flags{align-items:center}` (app.css)
   still applies — this rule states `display` and `flex-direction` and nothing else — so the two
   rows were content-width and the hairline between them stopped two thirds of the way across
   the panel, which reads as a rule somebody drew rather than as a seam between two rows. */
.pns-up-priv #privrow{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:0}
/* ⛔ THIS ROW STATES NEITHER `display` NOR `white-space`, AND BOTH ABSENCES ARE MEASURED. The
   first cut said `display:flex;white-space:normal` and the picture showed "keep new uploads"
   broken into three words in a 90px column and "keep imported links PRIVATE" running 68px off
   the right edge of the phone — the scanner caught the second as OVERFLOW-DOC.
   · `.nsfwcheck` is already `display:inline-flex` (app.css) and a flex item is blockified, so
     the column has made it a full-width row without this rule saying anything.
   · `white-space:nowrap` comes from `.upload-flags.privateflags .nsfwcheck` at (0,3,0), and
     this selector is (1,1,0) — the ID wins, the label wraps, and a two-word control wearing a
     checkbox is what you get. The labels fit on one line at 390 and always did.
   All this row needs is its own height and the hairline under it. */
.pns-up-priv #privrow > .nsfwcheck{padding:11px 2px}
/* ⚑ AND THE CHECKBOX HAS TO BE TOLD IT IS A CHECKBOX. app.css:358 gives EVERY `input` on the
   site `width:100%` — the house form idiom — and it never showed here because `.nsfwcheck` was
   `inline-flex` and shrink-to-fit, so 100% resolved against a box the label's own text had
   already sized. Stretch the label to full width for its hairline and the same declaration
   resolves against 694px: MEASURED in the real app at 1440, the box drew 501px wide with the
   13px tick floating in the middle of it, which is what put a checkbox in the centre of the
   panel and its label against the right-hand edge. `flex:0 0 auto` pins the basis as well, so
   the row cannot shrink it back to a smear either. */
.pns-up-priv #privrow .nsfwcheck input{width:auto;flex:0 0 auto}
@media(pointer:coarse){.pns-up-priv #privrow > .nsfwcheck{min-height:var(--tap-min)}}
.pns-up-priv #privrow > .nsfwcheck + .nsfwcheck{border-top:1px solid var(--hair)}
/* ⚖ THE RULING, AS A FOOTNOTE AND NOT AS THE PANEL'S CONTENT. Owner ruling 2026-08-25 puts
   this sentence beside the switch, at rest, spelled out — never in a tooltip or a <details>,
   all of which are invisible on touch — so it cannot be shortened or shut. What CAN change is
   its weight: ruled off from the controls, dimmed, and small enough that the two switches
   above it are what the eye lands on first. `--dim` is the token the empty state uses. */
.pns-up-priv #privrow > .hint{flex:0 0 auto;margin:0;padding:10px 2px 0;
  border-top:1px solid var(--hair);color:var(--dim);line-height:1.6}

/* ── STORAGE ────────────────────────────────────────────────────────────────────────────── */
/* the readout sits directly under the tab with the bar beneath it — three lines, no band. */
.pns-up-store > .pns-up-storemeta{padding-top:2px}
/* ⚠ AND THE PANEL NEEDS ITS OWN PADDING, because above 1000px its ONLY content is a 6px bar —
   the meta slot goes absolute onto the top border there, so the box came out 44px tall and the
   tab chip collided with its own corner brackets. `--pn-pad` is the primitive's hook for
   exactly this (PANEL_PRIMITIVE.md), so the panel is adjusted rather than the gauge. */
.pns-up-store{--pn-pad:26px 16px 22px}
/* ⚑ THE FIGURES ARE ON THE EDGE AND THE ROW IS REDUNDANT, NOT HIDDEN. privateQuotaMeter()
   (common-chrome.js — one builder, three surfaces, deliberately) prints "STORAGE · 0 of 10 MB
   used · level 1 · 10 MB per level", and on THIS panel every part of it is said somewhere
   better: "STORAGE" is the tab, the two figures are the meta slot in privateMb()'s own
   spelling, and `level 1 · 10 MB per level` is how the allowance is EARNED — a different
   question, asked in the settings drawer, and intake.css has hidden it on this screen since
   2026-08-29 for exactly that reason. So the row stays in the DOM, in the reading order, and
   the panel shows the one thing it never had: the bar.
   ⚠ IT IS A RULE AND NOT A CLASS ADDED BY SCRIPT because intakeQuotaMount() REBUILDS this node
   after every private upload (intakeQuotaCharged), so anything written onto it by hand is
   gone the first time the member uploads something. */
.pns-up-store #privmeter{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}
.pns-up-gauge{margin:0}

/* ── IMPORT ─────────────────────────────────────────────────────────────────────────────── */
/* the link door was a bordered card; the panel is the card now. */
.pns-up-import .importurl{border:0;background:none;padding:0;margin:0}
/* ⚑ ONE CAPPED ROW — the input and its button share an edge instead of floating 8px apart.
   ⚠ THE -1px IS THE SHARED HAIRLINE, not a nudge: two 1px borders meeting draw 2px, which
   reads as a seam between two controls rather than as one control with a cap. */
/* ⚑ THE PLACEHOLDER ELLIPSES RATHER THAN BEING CUT (forage-qw2w.33). At 390 the field read
   "https://…  paste a post li" — sheared dead in the middle of "link" at the input's right edge,
   because the string was written for a wider box and nothing was truncating it. ⛔ THE COPY IS
   NOT SHORTENED: an ellipsis says "there is more of this word" and a clean cut says "this is the
   word", and only one of those is true. ⚠ BOTH SELECTORS ARE NEEDED — `text-overflow` on the
   input governs its VALUE, and the placeholder is a pseudo-element with its own box, so the
   input's rule never reaches it. */
.pns-up-import .importinput{text-overflow:ellipsis}
.pns-up-import .importinput::placeholder{overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.pns-up-import .importrow{gap:0;margin:0}
.pns-up-import .importrow .btn{margin-left:-1px;border-color:var(--hair);background:var(--sunk)}
.pns-up-import .importrow .btn:hover{border-color:var(--accent)}
/* the disclosure keeps its own shape (intake.css took its box off deliberately in 2026-08-21)
   and only loses the margin the column now owns. */
.pns-up-import .importhelp{margin-top:10px}
/* ⚑ TWO DISCLOSURES, ONE ANATOMY (forage-qw2w.33 leg 4). "▸which links work?" and
   "▸ INSTAGRAM IMPORTS (OPTIONAL)" sat 140px apart on one screen wearing two looks: one 11px
   lowercase, one the house `.lbl` (10px, .16em, caps) — and the first lost the space after its
   marker because app-moderation.css's coarse block makes that summary `display:flex`, and a
   flex item drops the trailing space in `content:"▸ "`. DESIGN.md prescribes no disclosure
   anatomy of its own, so the uppercase spaced one is the one they share: both summaries are
   flex rows with the marker as its own item and a `gap` that cannot collapse, in `.lbl`'s type.
   ⚠ The `::after` tap overhang app-moderation.css draws on the IG summary is `position:absolute`
   and is untouched by the display change. (0,2,1)/(0,3,1) against that sheet's (0,1,1)/(0,2,1),
   which loads later — specificity, not order, is what wins here. */
.pns-up-import .importhelp > summary,
.pns-up-ig .igcookie > summary{display:flex;align-items:center;gap:.5em;
  font-size:10px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);
  color:var(--dim)}
.pns-up-import .importhelp > summary::before,
.pns-up-ig .igcookie > summary::before{content:"▸"}
.pns-up-import .importhelp[open] > summary::before,
.pns-up-ig .igcookie[open] > summary::before{content:"▾"}
.pns-up-import .importhelp > summary:hover,
.pns-up-ig .igcookie > summary:hover{color:var(--bright)}

/* ── INSTAGRAM ──────────────────────────────────────────────────────────────────────────── */
/* the <details> was a bordered card too, and it is the panel's content now. Its summary keeps
   every word: a panel tab is a label, a summary is a CONTROL, and a control with its text
   taken away to avoid saying "instagram" twice is an unlabelled control. */
.pns-up-ig .igcookie{border:0;background:none;padding:0;margin:0}
/* ⚑ forage-qw2w.33 leg 2 — A KEYBOARD SHORTCUT IS NOT TOLD TO A THUMB. The dropzone's ", or
   paste (⌘/ctrl-V)" is one span now, and it is hidden on a device with no hover and a coarse
   pointer — the device rung, not a width: a phone in landscape is still a phone, and a narrow
   desktop window still has a keyboard. No words change; only who is told them. */
@media (hover:none) and (pointer:coarse){ .pns-up-drop .pns-up-kbd{display:none} }
/* ▲▲▲ /upload ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /hot — THE BOARD (page-hot.js) ▼▼▼  forage-qw2w.9.3
   docs/design/DESIGN.md §/hot. ONE panel, tab HOT, meta = the post count; each post a row
   in its log box; the comments under it as QUOTED REPLIES rather than bordered paragraph
   boxes.

   ⚖ THE BOARD-INDEX RULING IS UNTOUCHED (owner 2026-07-19, docs/surfaces/hot.md): full-width
   rows, the thumb far left in its natural aspect and top-aligned, the header line plus the
   comments flowing in all the remaining width, the WHOLE ROW one tap target, authors as plain
   dim text because a nested anchor would swallow it. Every one of those is still true. This
   region changes what a row is made OF, never what it is.

   ⚠ app-feeds.css HOLDS `.hot*` AND LOADS AFTER THIS SHEET, so every override leads with this
   screen's own class. ⛔ Nothing is deleted from it: reverting this change drops the classes in
   page-hot.js and app-feeds.css finds its markup again exactly as it was.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* ⚠ THE PANEL CARRIES THE MARGIN, NOT THE PAGE. `.hotwrap{padding:0 18px}` is app-feeds.css's
   and it is a LATER sheet, so a padding written on `#app` here would lose the tie in silence.
   `.pn` has no margin of its own, so there is nothing to fight. */
.pns-hot-panel{margin:18px 0 40px}

/* ── THE ROW ────────────────────────────────────────────────────────────────────────────── */
/* the log box is the primitive's; the SEPARATOR is this screen's, because the row cannot wear
   the primitive's row class (page-hot.js says why: it would tell the tap census that every log
   row on the site is a control). ⚠ NO `font-size` IN THIS RULE, DELIBERATELY —
   mobiletaps.test.js estimates a control's height from `padding` + `font-size` in ONE rule, and
   this row is a 200px thumbnail whose height that estimate would get wrong by an order of
   magnitude. */
.pns-hot-list .pns-hot-row{border-bottom:0;padding:14px 14px}
.pns-hot-list .pns-hot-row + .pns-hot-row{border-top:1px solid var(--hair)}
.pns-hot-list .pns-hot-row:hover{background:var(--panel)}

/* ── THE HEADER LINE ────────────────────────────────────────────────────────────────────── */
/* ⚖ THREE SHAPES, NOT THREE WORDS. The line was title · count · age at one weight with two
   middots holding them apart — the "row of same-weight words" the references name as the thing
   they never do. It is a bright title, a filled chip and a dim tabular instant now, so the eye
   sorts them without reading them, and the separators went with the sameness. */
/* ⚑ TWO ROWS, NOT ONE WRAPPING LINE (forage-qw2w.31). One flex line meant the layout was decided
   by how long the title happened to be: short, and the chip sat beside it with the time wrapped
   alone underneath; long, and both dropped to line two together. Two peer cards one scroll apart
   drew their meta two different ways, and the row jumped under the thumb as the feed scrolled.
   The title takes the first row, the meta takes the second, at every length and every width. */
.pns-hot-row .hothead{display:flex;flex-direction:column;align-items:stretch;gap:6px;
  width:100%;line-height:1.4}
/* ⚠ AND THE COLUMN THE HEAD SITS IN IS STATED TOO. MEASURED at 390 in the real app: the three
   heads came out 186px, 240px and 274px wide — each shrink-to-fit around its own title — so a
   meta row that fills its head still put the instant at three different x. The row is only one
   shape if the box it lives in is one width. */
.pns-hot-row .hotdisc{width:100%;align-self:stretch}
.pns-hot-title{min-width:0}
/* [chip] … [time]: one shape, the chip at the left edge and the instant at the right.
   ⚠ `width:100%` AND `space-between` RATHER THAN `margin-left:auto` ALONE. The first cut relied
   on the row stretching and on the time pushing itself to the end, and the picture had the two
   cards' timestamps at two different x — which is the same complaint this bead makes, moved
   eight pixels. Stating the width and the distribution leaves nothing for a parent's
   `align-items` to decide. */
.pns-hot-meta{display:flex;align-items:baseline;justify-content:space-between;
  gap:10px;width:100%;min-width:0}
.pns-hot-age{font-size:11px;white-space:nowrap}

/* ── THE QUOTED REPLY ───────────────────────────────────────────────────────────────────── */
/* ⚑ THE BRACKET IS ONE PSEUDO-ELEMENT AND NOT A CHARACTER IN THE DOM (SCiPNET's quoted reply,
   docs/design/references/03-scipnet-activity.png). A rail down the left turning right at the
   top: `border-left` + `border-top` on an 11px box with one rounded corner, so the arc is the
   corner radius doing its job rather than a glyph somebody could select, search or hear read
   out. ⛔ Never `content:"┌"` — a screen reader announces box-drawing characters. */
.pns-hot-quotes{gap:14px}
/* ⚠ TWO CLASSES: `.hotc` carries the card — the raised ground, the hairline, the padding and a
   180px min-width — at (0,1,0) in the LATER app-feeds.css, so a one-class rule here loses the
   tie in silence and the first 1440 picture still had five bordered boxes in it. */
.pns-hot-quotes .pns-hot-quote{position:relative;background:none;border:0;
  padding:0 0 0 18px;min-width:0}
.pns-hot-quotes .pns-hot-quote::before{content:"";position:absolute;left:0;top:0;bottom:0;width:11px;
  border-left:2px solid var(--hair-hi);border-top:2px solid var(--hair-hi);
  border-top-left-radius:6px;pointer-events:none}
/* the author takes the accent — it is the one name in the row, and the row's own title is the
   post's, not a person's. The body stays --ink and keeps its four-line clamp. */
.pns-hot-quotes .pns-hot-quote .hotc-a{color:var(--accent);margin-bottom:3px}
/* ▲▲▲ /hot ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /tags — THE TAG DESK (page-tags.js) ▼▼▼  forage-qw2w.9.4
   docs/design/DESIGN.md §/tags. One enormous box holding nine unrelated strips becomes
   four panels that each answer one question — the tag, its links, its posts, its tree — with
   the type rail as its own panel at the right of the wide lane.

   ⚠ app-tagrows.css AND app-viewer.css HOLD `.tagi-*` AND `.tag-*` AND BOTH LOAD AFTER THIS
   SHEET, so every override leads with this screen's own class. ⛔ Nothing is deleted from
   either: every element they style is still built by the same code in the same order, so
   reverting this change drops the classes in page-tags.js and they find their markup again.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* the enormous box loses its box — the panels inside it are the boxes now. */
.pns-tags-box{border:0;background:none;padding:0;margin:0}
.pns-tags-off{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap;border:0;padding:0;margin:-1px}

/* ⚖ THE RAIL IS A RAIL ONLY WHERE THERE IS A LANE FOR IT — one column below the house rung
   (1000px; app-panel.css states it once, mobilebreakpoints.test.js), a 230px right-hand lane
   above it, sticky, because a taxonomy is walked while the tag stays on screen.
   ⚠ ON A PHONE THE TYPES COME LAST, AND THAT IS A CHOICE. The old selector sat at `order:-1`
   inside `.tagi-top`, where it was a wrapped row of seven chips above a textarea; here it is a
   whole PANEL, and leading a 390px screen with a seven-row control would put the answer to
   "what KIND is this" ahead of the tag the member came to read. The panels the rail governs go
   first; the rail is the last thing on the page, where a tool belongs. */
/* ⚠ THE PADDING IS ON THE GRID AND `.pns-col`'s IS TAKEN OFF THE COLUMN INSIDE IT. A tab label
   sits NINE PIXELS ABOVE its panel's top border, so a first panel flush against the masthead
   has its own name clipped by it — photographed at both viewports. */
/* ⚑ DENSITY, forage-pia1.2 — THE TOP PAD IS THE TAB'S CLEARANCE AND IS TRIMMED TO IT, NOT
   BELOW IT. The paragraph above is why this padding exists at all: the first panel's own name
   overhangs its border by NINE pixels and the masthead is directly above. 14 leaves five clear
   and the name still lands on ground — measured at 390 and 1440. The FOOT was answering nothing:
   `#count` under it opens with its own 10px and wears no rule, so 24 is a band and not a seam,
   and the tag cloud — the thing this screen is an index of — comes up 22px on a phone. */
.pns-tags-grid{display:grid;gap:22px;grid-template-columns:minmax(0,1fr);padding:14px 0 24px}
.pns-tags-main{padding:0}
@media(min-width:1000px){
  .pns-tags-grid{grid-template-columns:minmax(0,1fr) 230px;align-items:start}
  .pns-tags-types{position:sticky;top:74px}
}

/* ── THE TAG PANEL ──────────────────────────────────────────────────────────────────────── */
/* the tab wears the tag's own type colour rather than the interface accent — it is the one
   label on this screen that is a piece of DATA. ⚠ (0,2,0): `.pn > .pn-tab` is two classes, so
   a one-class rule here loses the tie and the tab stays amber. The `k-*` token is
   app-tagrows.css's and is not restated. */
/* ⚑ `-webkit-text-fill-color` IS THE ONE THING THAT MAKES THIS WORK, and it is not a hack for
   a browser. The tag's type token is spent on `color` (app-tagrows.css's `.k-*` rules, one per
   type), so `background:currentColor` fills the tab with the RIGHT colour — and then paints the
   label in it too, which is a tab you cannot read. Photographed at both viewports as a blank
   green block. Setting `color` would fill the tab with whatever the label needs. `text-fill`
   colours the GLYPHS alone and leaves `currentColor` carrying the type, so one node can be a
   filled chip in the tag's colour with the panel's own --void label on it. Both are tokens. */
.pns-tags-tag > .pns-tags-name{background:currentColor;
  -webkit-text-fill-color:var(--void);text-fill-color:var(--void)}

/* the panel's own body: the search row, the description and its save, then the cells. */
/* ⚑ DENSITY, forage-pia1.2 — the column's gap is 8, and every row inside it keeps its size.
   Three gaps at 390 (the find row, the label, the box, the save), so this is six pixels of
   nothing between four controls that are all still exactly as tall as they were. */
.pns-tags-tag .tagi-left{display:flex;flex-direction:column;gap:8px;padding:0;border:0;
  background:none;min-width:0}
/* ⚑ ONE CAPPED ROW. The find-a-tag box and its `+` and lens buttons shared a border once and
   then drifted 8px apart; they are one control with two caps again. The -1px is the SHARED
   hairline — two 1px borders meeting draw 2px, which reads as a seam. */
.pns-tags-tag .tagi-search{display:flex;align-items:stretch;gap:0;flex-wrap:wrap;margin:0}
.pns-tags-tag .tagi-search > input{min-width:0;flex:1 1 160px}
/* ⚑ ONE ANATOMY, ONE TINT (forage-qw2w.24). This strip was four cells and four species: a green
   outlined word, an input, a GREEN `+` and a WHITE lens — two of them unlabelled icons, in two
   colours, with nothing on the screen saying why one was green and one was white. Both buttons
   are the panel's own button now, both --ink on --sunk, each a mark and its word (page-tags.js
   carries the words and the reason the lens keeps its drawing). ⛔ `.tagi-plus{color:var(--good)}`
   is NOT deleted from app.css — it simply has this rule above it in the cascade, so a revert of
   this region takes the green `+` back with it. */
.pns-tags-tag .tagi-search > .tagi-plus,
.pns-tags-tag .tagi-search > .tagi-mag{margin-left:-1px;border-radius:0;min-width:0;
  padding:6px 12px;font-size:11px;line-height:1.4;
  background:var(--sunk);color:var(--ink);border:1px solid var(--hair)}
.pns-tags-tag .tagi-search > .tagi-plus:hover,
.pns-tags-tag .tagi-search > .tagi-mag:hover{color:var(--bright);border-color:var(--hair-hi)}
/* ⚠ THE FLOOR IS RESTATED FOR THESE TWO SELECTORS, not inherited. Giving each cap the panel
   button's 6px/12px padding beside its 11px type makes these rules state the pair
   mobiletaps.test.js reads as "this element's height is its type plus its padding" — it measures
   29px here and does not care that app-tagrows.css already floors `.tagi-mag` on a coarse
   pointer, because it matches by SELECTOR and a grant it cannot see is a grant it cannot see
   revoked either. Same floor, same token, said where this rule can be read. */
@media(pointer:coarse){
  .pns-tags-tag .tagi-search > .tagi-plus,
  .pns-tags-tag .tagi-search > .tagi-mag{min-height:var(--tap-min)}
}
/* the `+` is a MARK inside its cap, not the cap. It keeps the size it had so it still reads as
   a plus rather than as punctuation, and the word carries the meaning. */
.pns-tags-capmark{font-size:15px;line-height:1}
/* ⚖ THE CURRENT TAG IS THE INPUT'S LEADING LOZENGE. It was a 14px outlined word standing apart
   from the box beside it; it shares the row's edge now — same height, one hairline between them
   — so the strip reads as one control that already has something in it, which is what it is.
   The type colour stays on the TEXT: this cell is the one piece of data in the row. */
.pns-tags-tag .tagi-search > .tagi-name{align-self:stretch;display:flex;align-items:center;
  margin-right:-1px;font-size:11px;padding:4px 11px;border:1px solid var(--hair);
  letter-spacing:calc(.08em * var(--track));text-transform:var(--caps)}
/* ⚠ AT 390 THE TWO CAPS TAKE THEIR OWN LINE AND SPLIT IT. Four cells do not fit a 324px panel,
   and left to wrap on `auto` bases they land as two stubs against the left edge under a
   full-width input — the ragged shape this bead is about, one row further down. A 50% basis is
   wider than what line one has left, so both are pushed to line two together and then grow into
   it; above the phone rung they go back to their own width and the row is one line again. */
@media(max-width:640px){
  .pns-tags-tag .tagi-search > .tagi-plus,
  .pns-tags-tag .tagi-search > .tagi-mag{flex:1 1 50%;margin-top:-1px;justify-content:center}
  .pns-tags-tag .tagi-search > .tagi-mag{margin-left:-1px}
}
.pns-tags-tag .tagi-ta{margin:0}
/* SAVE DESCRIPTION wears the panel's button. ⚠ `.btn` is NOT removed from that element the way
   /upload's `choose folder…` had it removed, because this one is built inside a branch that
   also reads `.tagi-save` elsewhere; the two classes are reconciled here instead, in the one
   place a reader looking at this screen will find them. */
.pns-tags-tag .tagi-save{align-self:flex-start;padding:6px 12px;font-size:11px;
  background:var(--sunk);color:var(--ink);border:1px solid var(--hair);border-radius:0}
.pns-tags-tag .tagi-save:hover{color:var(--bright);border-color:var(--hair-hi)}
@media(pointer:coarse){.pns-tags-tag .tagi-save{min-height:var(--tap-min)}}

/* ── THE THREE RUNGS ────────────────────────────────────────────────────────────────────── */
/* ⚖ THE LEGEND LIVES IN THE CELL IT EXPLAINS. Each cell draws the tag in that provenance state
   — which IS the key to the border language, by example rather than as an abstract row — over
   the count and the rung's name. Three cells at every width: the words are short, and two
   columns would leave the third cell spanning a row on its own for no reason. */
/* ⚑ DENSITY, forage-pia1.2 — 10px, not 14. The strip already stands off the save button by the
   button's own 44px target; the extra four were a second gap on top of a floor. */
.pns-tags-states{--pn-cols:3;margin-top:10px}
@media(max-width:999px){.pns-tags-states{--pn-cols:3}}
.pns-tags-state{display:flex;flex-direction:column;align-items:flex-start;gap:6px;
  border-top:2px solid transparent}
.pns-tags-state .tag{max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* the AI's accuracy, as the strip's meta line: right-aligned, quiet, tabular. */
.pns-tags-ai{margin-top:7px;text-align:right;font-size:10.5px;color:var(--dim);
  font-variant-numeric:tabular-nums}
.pns-tags-ai.is-bad{color:var(--bad)}

/* ── THE LINKS PANEL ────────────────────────────────────────────────────────────────────── */
.pns-tags-linkbody{display:flex;flex-direction:column;gap:8px;min-width:0}
/* the wikipedia row and the relation row as capped rows, same shape as the search row. */
.pns-tags-linkbody .tagi-link{display:flex;align-items:stretch;gap:0;flex-wrap:wrap;margin:0}
.pns-tags-linkbody .tagi-link > input{min-width:0;flex:1 1 160px}
.pns-tags-linkbody .tagi-link > .tagi-sel{flex:0 0 auto;border-radius:0}
.pns-tags-linkbody .tagi-link > .tagi-linkbtn{margin-left:-1px;border-radius:0}
.pns-tags-linkbody .tagi-link > input{margin-left:-1px}
/* ⚠ ON A PHONE THE ROW WRAPS AND THE BUTTON LANDS ALONE ON LINE TWO — a 120px `LINK` hanging
   under a full-width input reads as a stray control. Below the phone rung it grows into the
   line it was pushed onto, so a wrapped row is two full-width rows rather than one and a stub. */
/* ⚑ AND BOTH LINK FORMS WRAP THE SAME WAY (forage-qw2w.24's second half). At 390 the WIKIPEDIA
   row fitted its button INLINE and the LINK row 130px below it — a select, an input and a button
   — could not, so it put its button full-width underneath: two adjacent forms of the same kind,
   two button placements. The narrower form is made to wrap too, so the pair reads as one
   anatomy at the width where either of them has to break. Above the phone rung both fit inline
   and both stay inline. */
@media(max-width:640px){
  .pns-tags-linkbody .tagi-link > .tagi-linkbtn{flex:1 0 100%;margin-left:0;margin-top:-1px}
}

/* ⚖ THREE COUNTS IN ONE HAIRLINE ROW, NOT THREE BOXES SAYING "none". The relation columns were
   three 200px bordered boxes whose whole content, on most tags, was the word none three times —
   three quarters of the screen's width spent stating an absence. A count at the top is the same
   fact in one glyph, the names sit under it when there are any, and the box is one box. */
.pns-tags-rels{display:grid;gap:1px;margin-top:14px;--pn-cols:3}
@media(max-width:999px){.pns-tags-rels{--pn-cols:1}}
.pns-tags-rel{display:flex;flex-direction:column;align-items:flex-start;gap:5px;
  border-top:2px solid transparent;text-align:left}
.pns-tags-relh{margin:0}
.pns-tags-relbody{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;border:0;
  background:none;min-height:0;min-width:0}
.pns-tags-relbody:empty{display:none}

/* ── THE POSTS PANEL ────────────────────────────────────────────────────────────────────── */
/* the strip keeps its one-row, horizontally-scrolling shape (owner ruling 2026-07-31k: it is a
   SAMPLE of the tag, not a results page) and only loses the margin the column now owns. */
.pns-tags-posts .tagi-strip{margin:0}

/* ── THE TREE PANEL ─────────────────────────────────────────────────────────────────────── */
/* the collapsible was a bordered card; the panel is the card now. Collapsed at rest, as before.
   ⚠ DEVIATION FROM THE SPEC, STATED: the spec asks for Spectre's `├──` tree lines drawn as CSS
   borders. This tree is not a text tree — it is the radial TreeView engine genealogy uses
   (web/treeview.js, owner ruling 2026-08-01c), a canvas of nodes and edges shared with another
   surface — so there are no rows to draw rails between, and re-drawing it as an indented list
   would be replacing a navigable graph rather than repainting it. The panel is the change. */
.pns-tags-tree .tagi-tree{border:0;background:none;padding:0;margin:0}

/* ── THE TYPES RAIL ─────────────────────────────────────────────────────────────────────── */
/* ⛔ THE LEVER STAYS BESIDE THE LIST, NOT UNDER IT, AND THE FIRST PICTURE IS WHY. `.tagi-types`
   is a ROW whose lever is a vertical <input type=range> with `align-self:stretch` (app.css) —
   turn the row into a column and that 22px control stretches to the panel's full WIDTH and
   renders as a 250px empty box with a hairline down the middle of it, which is exactly the
   "empty box" this repaint is removing. The row is left alone; only its furniture goes.
   ⚠ Below 700px app.css already hides the lever — "a vertical lever beside a wrapped row means
   nothing" — and the rail is a column at every width, so nothing is lost there either. */
.pns-tags-types .tagi-types{padding:0;border:0;background:none;min-width:0}
.pns-tags-types .tagi-typelist{flex:1 1 auto;display:flex;flex-direction:column;gap:0;
  border:0;padding:0;background:none;min-width:0}
/* ⚑ THE COLOUR IS THE MARK. Every type already has a token — `.k-general`, `.k-character`, … —
   and the old selector spent it on the button's TEXT and its border, which made seven boxes of
   seven colours. A rail row spends it on the 2px left edge instead, so the column reads as one
   list with a colour key down its side, and the row you are on is the lit one.
   ⚠ `currentColor` IS DOING THE WORK: the `k-*` rule sets `color`, so the left edge follows
   whatever token that type carries without this sheet naming a single one of them. */
.pns-tags-types .tagi-type{justify-content:flex-start;text-align:left;width:100%;min-width:0;
  padding:7px 10px;white-space:normal;
  border:0;border-left:2px solid currentColor;background:none;border-radius:0;
  font-size:11px;letter-spacing:calc(.12em * var(--track))}
/* ⚠ AND THE FLOOR IS RESTATED FOR THIS SELECTOR, not inherited. Tightening the padding to
   7px/10px so seven type names fit a 230px rail makes this rule state a padding AND a
   font-size — the pair mobiletaps.test.js reads as "this element's height is its type plus its
   padding" — so the census measures 31px here and does not care that `.pn-rail` two lines up
   already carries `min-height:var(--tap-min)` on a coarse pointer: it matches by SELECTOR, and
   rightly, since a grant it cannot see is a grant it cannot see revoked either. */
@media(pointer:coarse){.pns-tags-types .tagi-type{min-height:var(--tap-min)}}
.pns-tags-types .tagi-type + .tagi-type{border-top:1px solid var(--hair)}
.pns-tags-types .tagi-type:hover:not(:disabled){background:var(--panel)}
/* "you are here" is the accent ground the rail's own mark uses, over the type's own edge. */
.pns-tags-types .tagi-type.on{background:color-mix(in srgb,var(--accent) 12%,transparent)}
/* ⚠ THE LEVER NEEDS ITS OWN TRACK RESERVED. `.tagi-types` is a flex row and the type buttons
   are `white-space:nowrap` with 14px of side padding, so in a 208px rail their min-content width
   is wider than the space and the list overflowed — which put the lever's amber grip ON TOP of
   the row it was pointing at, photographed at 1440. `min-width:0` lets the list shrink and
   `flex:0 0 22px` stops the lever being the thing that gives way. */
.pns-tags-types .tagi-lever{flex:0 0 22px;width:22px;margin:0;align-self:stretch}

/* ── THE CLOUD'S SORT ROW ───────────────────────────────────────────────────────────────── */
/* ⚑ AND THE `.sortopt` BAND IDIOM HAS TO BE UNDONE ON A THUMB — the same trap /u's tab row hit.
   app.css draws a coarse-pointer `.sortopt` as a 44px BOX whose visible 26px chrome is an
   absolutely-positioned ::after band, then hands the row the growth back with a negative
   margin-block. Give that element a real border and a real background too and you get two boxes
   stacked and rows overlapping by nine pixels where the bar wraps — and this bar runs six
   options, so it DOES wrap at 390. The band goes and the margin with it; the button is simply
   44px on a thumb, which is what every button in this grammar does. */
.pns-tags-sortbar .sortopt{padding:6px 12px;font-size:11px;line-height:1.4;border-radius:0;
  background:var(--sunk);color:var(--ink);border:1px solid var(--hair)}
.pns-tags-sortbar .sortopt:hover{color:var(--bright);border-color:var(--hair-hi)}
.pns-tags-sortbar .sortopt.on{background:var(--accent);color:var(--void);
  border-color:var(--accent);font-weight:700}
/* ⚑ DENSITY, forage-pia1.2 — THE COUNT AND THE OPTIONS SHARE A LINE ON A PHONE TOO, WHICH IS
   WHAT 1440 HAS ALWAYS DONE. `.resultbar` is `flex-wrap:wrap` and this bar's flex base size is
   its max-content — SIX options, 464px MEASURED at 390 — so it never fitted beside "32 TAGS" in
   a 362px row. ⚖ AND FLEX LINE-BREAKING HAPPENS BEFORE SHRINKING: the bar was pushed to a line
   of its own, given the full width there, and only THEN shrunk — so the count was orphaned on a
   17px row of its own and the bar wrapped internally anyway. A zero flex-basis makes it fit on
   the first line by construction, and `flex-grow` hands it every pixel the count does not use;
   `justify-content:flex-end` (app.css) still right-aligns it, so nothing moves horizontally.
   ⛔ NOTHING IS REMOVED, HIDDEN, REORDERED OR SHRUNK: the same six options, the same words, the
   same 44px targets, the same count. The bar still runs two rows at 390 — 464px of options in
   297px cannot be one row, and `min-width:auto` keeps the widest option from being squeezed.
   ⚠ THE BASIS IS `0` AND NOT `0%`: a percentage basis on a wrapped flex item resolves against a
   container whose own width this item is helping decide. MEASURED at 390: the result bar goes
   154px → 117px and the first tag chip 223px → 186px; 1440 is unchanged at 121px. */
.pns-tags-sortbar{flex:1 1 0}
@media(pointer:coarse){
  .pns-tags-sortbar .sortopt{min-height:var(--tap-min);margin-block:0}
  .pns-tags-sortbar .sortopt::after{display:none}
  /* ⚖ AND THE ROW GAP IS THE BAND'S ARITHMETIC ON THE ONE BAR THAT DELETED THE BAND. `.sortbar`
     takes `gap:calc(var(--tap-min) - 26px) 8px` = 18px/8px, and forage-c5m's own comment says
     why: with the band idiom a `.sortopt` carries `margin-block:-9px`, so its 44px HIT box
     overhangs its 26px margin box by nine each way and an 8px row gap leaves two rows'
     targets overlapping by ten. ⛔ THAT ARITHMETIC IS NOT TRUE HERE — the rule directly above
     sets `margin-block:0`, so on this bar the border box IS the margin box at 44px and two rows
     eight pixels apart are eight pixels apart. The 18 was buying clearance that the band, and
     only the band, ever needed. 8px is the column gap this same bar already runs between two
     44px targets, so the row and the column now clear by the same number. Ten pixels, no words. */
  .pns-tags-sortbar{row-gap:8px}
  /* ⚠ AND THE COUNT SITS ON ROW ONE, NOT IN THE GUTTER BETWEEN TWO. `.resultbar` is
     `align-items:center`, which is right when the bar is one row high and wrong the moment it is
     two: a 17px "32 TAGS" centred against a 96px bar lands exactly in the 8px gap above, reading
     as a caption for nothing. A `--tap-min` box at the START of the cross axis centres the words
     on the first row of options by arithmetic — the options ARE `--tap-min` tall here (the rule
     above), so the two line up whatever the floor becomes. ⛔ Coarse only: at ≥1000px the bar is
     a single 29px row and centred is the correct answer, so this must not reach it. */
  .pns-tags-count{align-self:flex-start;display:flex;align-items:center;min-height:var(--tap-min)}
}
/* ── THE CLOUD'S SEARCH BAR (owner 2026-09-14) — its own full-width first row above the count and sort ── */
.pns-tags-find{flex:1 1 100%;order:-1;display:flex}
.pns-tags-findinp{flex:1 1 auto;min-width:0;min-height:var(--tap-min);padding:0 12px;font-size:15px;
  background:var(--void);color:var(--bright);border:1px solid var(--hair-hi);border-radius:0}
.pns-tags-findinp:focus{outline:none;border-color:var(--accent)}
/* ▲▲▲ /tags ends ▲▲▲ */

/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ / and /search — THE SEARCH DESK (page-index.js) ▼▼▼  forage-qw2w.14.1
   docs/design/DESIGN.md §"/ and /search". ⚖ THE HOME-PAGE LAW IS UNTOUCHED:
   nothing here redirects, remembers or time-windows anything, and `/search` still serves what
   `/` serves. THE GRID TILES ARE NOT TOUCHED — the grid IS the picture (references README).

   WHAT WAS WRONG, photographed at 1440x900 before the change:
     · ROW 3 WAS THE NAMED FAIL. `MATCH · TEXT FIELDS · WHO / WHEN · MEDIA TYPE · CONTENT ·
       YOURS · RELATIONS · TAGS · SORT` — nine same-weight uppercase words in a row, which is
       the exact thing docs/design/references/README.md says none of the four pictures do.
       They are the query-syntax tabs (QUERY_EXAMPLES in page-index.js), and a tab is a
       CONTROL, so they are drawn as controls now: a hairline lozenge each, the open one
       filled in the accent. Nine words at one weight became one row of chips with a state.
     · ROW 2 WAS THREE ANATOMIES IN ONE ROW — `.sqnum` (a label beside a number, one line) and
       `.sqset` (a 7.5px caption OVER a 9.5px value, two lines) sat shoulder to shoulder, so
       four controls read as two unrelated families. ⚖ app-search.css's forage-401 block rules
       "two kinds of control, two shapes"; docs/design/DESIGN.md overrules it for this row
       ("each a lozenge with its current value in --ink after the label in --dim") and the
       references back it: "a row of mixed icon-with-word and icon-only cells" is on their
       never list. The DISTINCTION forage-401 was defending survives — the label is still
       --dim and the value still --ink, they are simply side by side now.
     · ROW 1's INPUT AND ITS BUTTON HAD AN 8px GAP, so the archive's one bar read as a box and
       a separate button. They are one capped row: the button is the input's right cap.
     · THE COUNT HAD ITS OWN LINE AND ITS OWN RULE above the grid. It is the panel's meta slot
       now — the count belongs to the desk that produced it.

   ⚑ NO HANDLER, NO ELEMENT AND NO FETCH CHANGED. Every chip is the same node with the same
   id, the same aria-label and the same click handler; `#sqcount`, `#sqsortv`, `#sqtimev` and
   `#sqsavedn` are still the only writers of their numbers. The active paint rides on the
   state classes the surface ALREADY sets (`.armed` on narrow, `.on` on the two settings, and
   `.on` on the open syntax tab), so no JS decides a colour — which is also why this whole
   region comes off with one revert and leaves the behaviour standing. */

/* THE DESK IS THE PANEL. `.searchwrap` gains `.pn`, so its 22px/0/8px padding becomes the
   panel's and the corner brackets and the tab arrive with it.
   ⚠ THE TAB SITS 9px ABOVE THE PANEL'S TOP BORDER and the masthead is directly above, so the
   column owes it that much room — lane G paid this exact bill on /tags. */
.pns-search{margin-top:18px}
/* 🐛 THE PANEL KEEPS ITS SIDE PADDING AT EVERY WIDTH, AND TAKING IT AWAY WAS A REAL DEFECT
   (forage-qw2w.22). It was removed to buy the control row 32px against a 360px ceiling; what it
   actually bought was a search input drawn flush to the panel's own hairline, its left and
   right edges OUTSIDE the x of the corner brackets — so the house ornament sat stranded in the
   padding above and below a box that had escaped it, on the first panel on the home page.
   ⚖ A FRAME THAT ITS CONTENT ESCAPES IS NOT A FRAME. The brackets are drawn 3px inside the
   PADDING box, so a panel with no padding paints them under its own content by construction;
   10px is the smallest inset that clears them and keeps a hairline of ground on both sides.
   The width the row lost comes back from its own chips below, not from the frame. */
/* ⚑ DENSITY, forage-pia1.3 — THE TOP NUMBER IS THE HOUSE NUMBER, NOT A LOCAL ONE. The 22px is
   `.searchwrap`'s own pre-panel padding (app.css) carried across when the desk became a `.pn`;
   the primitive's default has been 18px since forage-pia1's redo, and at ≥1000px this panel
   already takes that 18 because this rule does not apply there. So the phone was the only
   viewport paying four pixels for a number nothing chose. ⚠ THE SIDES AND THE FOOT DO NOT MOVE:
   the 10px is the bracket inset the paragraph above is the bug report for. */
@media(max-width:999px){ .pns-search{--pn-pad:18px 10px 14px} }
/* ⚰ THERE IS NO META SLOT ON THIS PANEL, AND A GATE IS WHY. The spec put the result count in
   it; the count's home is `#results .resultbar > span` and forage-qa's searchbox.js reads it
   there in five checks — one of which asks whether the span is DRAWN, because the last time
   this number vanished it was a sheet hiding it. Emptying it took the deploy gate to 501/543.
   The bar keeps the number and gives up its chrome instead: no rule, right-aligned to the
   desk's own edge, tabular and quiet. That is the meta slot's job, one line lower. */

/* ── ROW 1: THE BOX AND ITS BUTTON ARE ONE ROW ──────────────────────────────────────────── */
/* ⚠ TWO CLASSES, DELIBERATELY. `.searchbar .btn` (app.css, then app-search.css) is (0,2,0);
   a bare `.pns-search-go` is (0,1,0) and loses the tie no matter how late this sheet sits. */
.pns-search-bar{gap:0}
.pns-search-bar .pns-search-go{border-left:0;border-radius:0;margin:0}
/* the box gives up its own right corner so the pair reads as one drawn control, not two. */
.pns-search-bar .pns-search-box{border-right:0}

/* ── ROW 2: THE CONTROL ROW, ONE ANATOMY — AND ONLY WHERE IT FITS ─────────────────────── */
/* ⚑ OWNER, 2026-09-05: *"some buttons had text some didn't"*. `clear` was a bare ✕ and `save`
   a bare ☆ beside chips reading `SORT SHUFFLE` and `WHEN ALL`; a row of mixed icon-with-word
   and icon-only cells is the FIRST item on docs/design/references/README.md's never list. The
   answer is one anatomy: a hairline lozenge each, the label in --dim and its current value in
   --ink, side by side, every chip carrying a word.

   ⚖ AND IT IS A DESKTOP ANSWER, BECAUSE THE ROW IS 679px WIDE AND A PHONE HAS 328. That is a
   MEASUREMENT, taken with the harness (.scratch/laneH/measure.js) at 360 and 390 on the widest
   reachable query, not an estimate:
       sqnarrow 100 · sqsort 170 · sqtime 76 · sqtags 85 · sqsaved 163 · sqstar 66  = 679
   against a 328px scrollport at 390 and 298 at 360. Two thirds of the row is off the screen.
   Neither way out is open. WRAPPING costs four checks in forage-qa's searchbox.js — §19's
   `no control has wrapped its label onto a second line` reads the row at 97 and 145px against a
   40px ceiling, and §24's gap census reports `[-106.9,-84.2,…]` because a wrapped row's
   neighbours run backwards. SCROLLING costs §24's `no chip is clipped by the viewport`, which
   asks every chip's own rect against the viewport and does not care whose scrollport it is in —
   *"a control whose options are reachable only by a scroll nobody advertised is a control with
   hidden options, whatever its `overflow` says"* is that file's own sentence, one section up.
   ⚖ SO THE PHONE KEEPS THE SHAPE IT WAS RULED (owner 2026-08-23, forage-xb6.5/forage-401): the
   arrow-and-badge `narrow`, the book that replaced the word `saved`, the two-line caption over
   its value, the bare ✕ and the ☆ far right. Every one of those was minted to make this row FIT
   a 360px phone, and forage-qa pins all of them. The owner photographed the DESKTOP row; the
   desktop row is what changes. ⛔ Do not carry these rules below 1000px without moving that gate
   first — the deploy went 501/543 the last time, and this comment is the receipt.
   ⚠ THE ☆ IS EXEMPT AT EVERY WIDTH: searchbox.js asserts `star.textContent === '★'` on the
   character, and its §24 already rules the star the one control here allowed its own shape.

   ⚠ EVERY SELECTOR IS PREFIXED WITH `.pns-search`, AND THAT IS NOT DECORATION. app-search.css
   is SECOND-TO-LAST in CSS_ORDER (server/tests/cssparts.js) and this sheet is third — so
   `.sqnum` / `.sqset` / `.sq-narrow` / `.sqsort`, all single-class, would win every tie against
   a bare `.pns-search-chip`. Two classes clear a one-class rule; three clear the `.sqset .k`
   kind. ⚠ `.sqtags` is named beside `.pns-search-chip` everywhere because the `# TAGS n` chip
   is built LAZILY by page-index-controls.js and is not in the DOM to be given a class. */

/* THE ACCENT IS THE ONE THING THAT HOLDS AT EVERY WIDTH — it costs no width and it is the
   whole of "which of these is doing something to my results". `.armed` (narrow is holding
   filters) and `.on` (a setting is off its default) are written by the search surface already,
   so no JS here decides a colour and the region reverts without behaviour going with it.
   ⚠ AND IT IS `.sq-narrow.armed`, NOT ANY `.armed`: `.sqsaved .list.armed` means "you have
   saved searches", a fact about the account and not a narrowing, and a blanket rule made the
   SAVED chip the one filled control on an unfiltered desk.
   ⚠ `color` GOES ON THE CHIP, not only on its words — the narrow chip's arrow is a registry SVG
   drawn in `currentColor`, so an accent arrow on an accent ground lost its mark entirely. */
.pns-search .sq-narrow.armed,
.pns-search .sqset.on,
.pns-search .sqsort.on{background:var(--accent);border-color:var(--accent);color:var(--void)}
.pns-search .sq-narrow.armed .w,.pns-search .sq-narrow.armed .n,
.pns-search .sqset.on .k,.pns-search .sqset.on .v,
.pns-search .sqsort.on .k,.pns-search .sqsort.on .v,
.pns-search .sqsort.on .dir{color:var(--void)}
/* the raised badge inverts rather than vanishing: --accent-deep on an --accent ground is one
   dark tone on another. Filled chip, hollow badge. */
.pns-search .sq-narrow.armed .n.badge{background:var(--void);color:var(--accent)}
.pns-search .sqsort.on .dir{border-left-color:var(--void)}

/* ── AND THE ONE ANATOMY, AT THE WIDTHS THAT HOLD IT ───────────────────────────────────── */
@media(min-width:1000px){
  .pns-search .pns-search-chip,
  .pns-search .sqtags{position:relative;display:inline-flex;flex-direction:row;
    align-items:center;gap:6px;padding:5px 9px;font-size:9.5px;line-height:1;
    background:var(--sunk);border:1px solid var(--hair);border-radius:0;
    letter-spacing:calc(.12em * var(--track));text-transform:var(--caps);white-space:nowrap}
  /* THE LABEL IS --dim AND THE VALUE IS --ink, SIDE BY SIDE. It keeps the DISTINCTION
     forage-401 minted (a setting and its current value are two things) and drops the two
     ANATOMIES it spent to draw it. */
  .pns-search .pns-search-chip .k{font-size:9.5px;line-height:1;color:var(--dim);
    letter-spacing:calc(.12em * var(--track))}
  .pns-search .pns-search-chip .v{font-size:9.5px;line-height:1;color:var(--ink);
    letter-spacing:calc(.12em * var(--track))}
  .pns-search .pns-search-chip .w,
  .pns-search .sqtags .w{font-size:9.5px;line-height:1;color:var(--dim)}
  .pns-search .pns-search-chip .n,
  .pns-search .sqtags .n{font-size:9.5px;line-height:1;color:var(--ink);
    font-variant-numeric:tabular-nums}
  /* the sort's direction arrow is the one member of this row with a second target inside the
     drawn box; it keeps its hairline divider and its honest 44px width. */
  .pns-search .pns-search-chip .dir{border-left:1px solid var(--hair);font-size:11px;
    line-height:1;padding:0 7px;align-self:stretch}
  /* THE SORT IS THE ONE CHIP THAT IS A GROUP: the box is drawn on `.sqsort` and the two
     targets sit inside it, so the group gives its padding to the name and keeps the border. */
  .pns-search .pns-search-group{padding:0;gap:0}
  .pns-search .pns-search-chip .name{flex-direction:row;align-items:center;gap:6px;
    padding:5px 9px;background:none;border:0}
  .pns-search .pns-search-chip.on .name{background:none}
  /* the saved list's own `font:inherit` (app-search.css, two classes, later sheet) resets the
     size and the line-height this row's one-height rule depends on — its own comment says so. */
  .pns-search .pns-search-chip.list{font-size:9.5px;line-height:1}
  /* ⚠ THE STAR IS THE ONE CHIP app-search.css SIZES BY NAME. `.sqctrl .star` is (0,2,0) in a
     sheet BELOW this one, so it beats a two-class rule on padding and font-size. It keeps its
     real 44px min-width, which searchbox.js asks for by name, and its glyph's own size. */
  .pns-search .pns-search-chip.star{padding:5px 9px;font-size:11px;line-height:1}
  /* ⚑ AND THE WORD ON `narrow` IS A DESKTOP WORD. app-search.css hides it below 1000px and
     raises the count to the chip's corner, which is right for an arrow standing alone; here
     there is room for both and the badge sits inline beside the word. */
  .pns-search .sq-narrow .w{display:inline}
  .pns-search .sq-narrow .n.badge{align-self:center;margin:0}
}
/* ⚰ AND THE TWO WORDS COME OFF BELOW 1000px, the same way and for the same reason
   app-search.css takes `narrow`'s off: the row has to fit a 360px phone. The `<span>`s stay in
   the DOM — they are one class swap from coming back the day this row gets more room. */
@media(max-width:999px){ .pns-search .sqsaved .w{display:none} }
/* ⚑ AND THE LAST TEN PIXELS COME OUT OF THE CHIPS, NOT OUT OF THE FRAME (forage-qw2w.22). With
   the panel's 10px inset restored, forage-qa's §24 measured `sqstar` at right=370.5 on a 360px
   screen — the widest reachable query, over by ten. The frame is not where that is paid from:
   a panel whose content escapes its own brackets is the defect this ticket IS. It comes out of
   the row instead — one pixel of side padding from each of the two shapes and one from the
   uniform gap, which is 18px at the widest state and nothing a reader can name.
   ⚠ PADDING LONGHANDS ON PURPOSE: a rule that states a padding AND a font-size is the pair
   mobiletaps.test.js sizes a control by, and these two shapes already carry their coarse halo
   from app-search.css. Stating only the side padding leaves that grant exactly where it is. */
@media(max-width:999px){
  .pns-search .sqrow{gap:3px}
  .pns-search .sqnum{padding-left:5px;padding-right:5px}
  .pns-search .sqset{padding-left:5px;padding-right:5px}
}

/* ⚠ THE TAP FLOOR IS THE HALO, RESTATED FOR THESE SELECTORS AND NOT RE-INVENTED. Every element
   below already carries app-search.css's coarse overhang; mobiletaps.test.js matches by
   SELECTOR, so a rule of this file's that states a padding AND a font-size has to name the
   grant or the census reads it as revoked (lane G paid the same bill on /tags' rail). Same
   declarations, so the pseudo-element is the one that was already there. */
@media(pointer:coarse){
  .pns-search .pns-search-chip::after,
  .pns-search .sqtags::after{
    content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
    height:var(--tap-min)}
  /* 🐛 ⛔ EXCEPT ON THE GROUP, AND THIS ONE STOPPED THE SORT MENU FROM OPENING AT ALL. `.sqsort`
     is a DIV holding two buttons; a halo on it paints over both of them, and a tap in that band
     hits the div rather than `#sqsortname` — the "steal the taps" failure app-search.css chose
     `left:0;right:0` to avoid sideways, arriving vertically instead. It is why that sheet hangs
     the floor on `.sqnum` / `.sqset` (the leaves) and gives `.sqsort .dir` a line of its own,
     and the name inside this group is a `.sqset`, so it already has its own. Caught by the
     gate: `waiting for locator('.sqpop button') to be visible` — 8s, then red. */
  .pns-search .pns-search-group::after{content:none}
}

/* ── ROW 3: THE NINE WORDS BECOME NINE CHIPS ────────────────────────────────────────────── */
/* ⚠ THIS ROW IS DESKTOP-ONLY IN PRACTICE: xp-chrome.css hides `.qhelp` under `html.xp`, and
   `xpEnabled()` is `!HOVER_CAPABLE` — so a phone never draws it. The scroller below is still
   written, because a hover-capable narrow window is a real window and the page must never
   scroll sideways. */
.pns-search-qhelp{position:relative;margin-top:12px}
.pns-search-qtabs{display:flex;flex-wrap:nowrap;gap:5px;min-width:0;
  overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
.pns-search-qtabs::-webkit-scrollbar{display:none}
.pns-search-qtabs > *{flex:none}
/* THE FADE AT THE RIGHT EDGE — a mask reads ALPHA only, so the token here is a stand-in for
   "opaque" and carries no colour of its own. */
@media(max-width:999px){
  .pns-search-qtabs{mask-image:linear-gradient(to right,var(--ink) calc(100% - 26px),transparent);
    -webkit-mask-image:linear-gradient(to right,var(--ink) calc(100% - 26px),transparent)}
}
/* `.qtab` (app.css) is a borderless 10.5px word; this is the same node drawn as a control.
   `.qtab.on` is (0,2,0) and turns it --bright and 600 — the filled lozenge replaces that. */
.pns-search-qtab{padding:4px 9px;font-size:10px;line-height:1.4;color:var(--dim);
  background:var(--sunk);border:1px solid var(--hair)}
.pns-search-qtab:hover{color:var(--ink);border-color:var(--hair-hi)}
.pns-search-qtab.on{background:var(--accent);border-color:var(--accent);color:var(--void);
  font-weight:400}
@media(pointer:coarse){ .pns-search-qtab{min-height:var(--tap-min)} }
/* the examples panel below the tabs keeps its own chips and simply loses the rule that used to
   double the panel's own hairline. */
.pns-search-qhelp .qhelp-panel{border-top-color:var(--hair)}

/* ── THE OLD COUNT LINE ─────────────────────────────────────────────────────────────────── */
/* ⚠ THE DIV STAYS AND ONLY ITS CHROME GOES. page-index.js's own comment and tools/
   viewer_e2e.sh both depend on `#results .resultbar` EXISTING as the "the grid has rendered"
   signal; what moved is the number inside it, which is now the panel's meta and is still
   written in exactly one place. An empty flex box with no padding and no border draws nothing. */
/* ⚑ DENSITY, forage-pia1.3 — the bar has no rule left to stand off from, and the grid below it
   opens with its own 8px (14 at ≥1000). Twelve pixels under a quiet right-aligned count, on top
   of the grid's own gutter, was twenty pixels of nothing between the number and the first meme.
   Six keeps the count a line of its own and not a caption on the first tile. */
.pns-search-resbar{padding:2px 0 6px;border-bottom:0;justify-content:flex-end}

/* ── THE RECAP BANNER, IN THE SAME GRAMMAR AS ITS NEIGHBOURS ───────────────────────────── */
/* ⚑ forage-qw2w.23, review at 390. `.recap-panel` (app-feeds.css) is a 3px amber left bar, a
   3px radius, a drop shadow, a title inside the box and a bare × floating in the corner — four
   pieces of anatomy that exist nowhere else, on the one element sitting between two panels that
   do belong. The classes are added in page-index.js's `dressRecap`, on THIS screen only: the
   builder is `buildRecapPanel` in web/common-profile.js, which this lane may not edit and which
   the profile surfaces also call. They keep the banner exactly as it was.
   ⚠ TWO CLASSES ON EVERY OVERRIDE. app-feeds.css sits BELOW this sheet in CSS_ORDER, so a bare
   `.pns-search-recap` loses every tie to `.recap-panel`'s own single-class rule. */
/* 🐛 THE SHELL GIVES UP ITS CHROME AND THE `.pn` INSIDE DRAWS THE PANEL. `.recap-panel` sets
   `background:` — the SHORTHAND — in a sheet below app-panel.css, so `.pn` on the SAME element
   had its `background-image` reset to none and all four corner brackets vanished; they are
   eight gradient layers in one background-image by design, and restating them here would be
   the second copy of the primitive this file's header forbids. Photographed twice: once on
   /login, once here. The shell keeps its dismiss animation and the × keeps its corner halo.
   ⚠ AND IT NEEDS A TOP MARGIN. A tab label overhangs its own border by nine pixels, so a panel
   flush under another panel plants its name on the neighbour's bottom edge — caught at 390 in
   the first picture after this change. */
/* ⚑ DENSITY, forage-pia1.3 — THE GAP ABOVE IS THE ONE THE TAB NEEDS, AND THE GAP BELOW IS NOT.
   The 22 above is load-bearing arithmetic and is only trimmed to the number that still satisfies
   it: the tab label overhangs its own border by NINE pixels, so 18 leaves nine clear and the
   name still lands on ground rather than on the search desk's bottom edge — the same 18 that
   `.pns-search` itself takes from the masthead, so the two gaps on this screen are one number.
   The 18 BELOW was answering nothing: `.pns-search-resbar` opens with its own 2px and the result
   count under it wears no rule, so 12 is a clear band and not a seam. Ten pixels, no words. */
.pns-search-recapshell.pns-search-recapshell{border:0;background:none;border-radius:0;
  box-shadow:none;padding:0;margin:18px 0 12px}
/* the panel's own box is the primitive's default (18/14) rather than a local 20/16. */
.pns-search-recap{padding:18px 14px 14px}
/* THE TITLE IS THE TAB NOW. The original string stays in the document and leaves the screen —
   the primitive's own rule for a tab label, in this sheet's house spelling. */
.pns-search-recap .recap-title{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
/* the head kept a bottom margin for a row that is now empty of everything but that title. */
.pns-search-recap .recap-head{display:block;margin:0}
/* ⚠ THE × IS THE SAME BUTTON, IN THE META SLOT, WEARING THE PANEL'S OWN BUTTON. `.recap-x` is
   (0,1,0) in a later sheet — a 20px borderless glyph — so this needs two classes to dress it,
   and it keeps the CORNER HALO app-feeds.css gives it on a coarse pointer (32px drawn, 44px
   targeted, forage-num): nothing here states a height, so that grant is untouched. */
.pns-search-recap > .pns-search-recapmeta{padding:0;margin:0;line-height:0}
.pns-search-recap .pns-search-recapx{display:inline-flex;min-width:0;font-size:11px;
  line-height:1.4;padding:3px 9px;color:var(--dim);background-color:var(--sunk);
  border:1px solid var(--hair);border-radius:0}
.pns-search-recap .pns-search-recapx:hover{color:var(--bright);border-color:var(--hair-hi)}
/* ⚑ DENSITY, forage-pia1.3 — THE DISMISS AND THE RECAP SHARE ONE LINE BELOW THE RUNG. MEASURED
   at 390 on the real app: the slot was a 32px line holding one × and nothing else, the recap's
   own words were a 20px line six pixels under it, and the panel spent 96px to say one sentence
   on the FRONT DOOR. ⚖ NOTHING IS REMOVED AND NOTHING IS SHRUNK: the × keeps its 32px drawing
   and its 44px target, the lines keep their 13px and their left edge, and the panel gives back
   32px — the recap now costs a line and its box instead of two lines and a band.
   ⛔ A FLOAT, AND NOT A LIFT, AND forage-pia1's OWN REVERT IS WHY (8a0daa47 → 4a599ef7): a
   negative margin on a slot is measured against the CONSUMER's padding and printed the readout
   under the tab on `/clubs`. Nothing here is negative and nothing here is shared — both
   selectors carry `.pns-search-recap`, which only page-index.js's `dressRecap` ever writes.
   ⚠ `margin-left` IS THE GUTTER AND IT IS ALSO THE TAP GUARD. `.recap-x`'s coarse-pointer halo
   (app-feeds.css) is 44px wide around a ~26px chip, so it overhangs ~9px to its left; 16px of
   gutter keeps that overhang off `.recap-link`, which is a real anchor a thumb can miss.
   ⚠ `flow-root` IS LOAD-BEARING. The float is 32px and the flex line beside it is 20px, so a
   plain block panel would end 12px ABOVE its own ×, printing the dismiss over the result bar —
   measured. It contains the float without clipping, so the tab still overhangs the border. */
.pns-search-recap{display:flow-root}
.pns-search-recap .recap-lines{margin-top:0}
@media(max-width:999px){
  .pns-search-recap > .pns-search-recapmeta{float:right;margin-left:16px}
}
@media(min-width:1000px){
  .pns-search-recap .pns-search-recapx{background-color:var(--raised)}
}
/* ▲▲▲ / and /search ends ▲▲▲ */


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /login and /register — THE DOOR (page-login.js, page-register.js) ▼▼▼  forage-qw2w.14.2
   docs/design/DESIGN.md §"/login and /register".

   WHAT WAS WRONG, photographed signed out at 390x844 and 1440x900 before the change: a plain
   box with a heading, a sentence, four stacked label-then-input pairs and an outlined button.
   Six left-aligned things down one column at four different weights — the shape the owner
   called "a highschool student's first HTML page", on the only screen a stranger ever sees.
   Reference 04's SECURE_ACCESS panel is the same job done: a titled hairline panel, a quiet
   label column, the fields as capped rows in one box, and ONE filled primary control.

   ⚖ THE PERIMETER IS PAINT-DEEP ONLY. Registration still requires a valid unused invite code
   and the server still decides that; the field is the same input with the same id, name and
   autocomplete, in the same form, submitting the same request. Nothing here is a claim about
   who may come in. No copy changed — the tab label is the page's own <h2> text, moved.
   ⛔ No sheet but app-panel.css may write a `.pn-*` selector, so nothing below names one. */

/* THE SHELL IS THE LAYOUT, THE PANEL IS THE PAINT. `.authcard` (app-rank.css, which sits BELOW
   this sheet in CSS_ORDER) keeps the 380px cap, the 8vh drop and every coarse-pointer tap floor
   it hands its descendants; its own border, ground and padding come off, because the `.pn`
   inside it draws those.
   🐛 AND THAT NESTING IS WHY THE BRACKETS SURVIVE. `.authcard` sets `background:` — the
   SHORTHAND — in a later sheet, so `.pn` on the SAME element had its `background-image` reset to
   none and all four corner brackets vanished; they are eight gradient layers in one
   background-image by design, so there was nothing else for them to live in. Photographed at
   1440x900 as a plain box before the nesting. Restating those eight layers here would be a
   second copy of the primitive in a screen sheet, which this file's header forbids.
   ⚠ TWO CLASSES, because `.authcard`'s own declarations are (0,1,0) in a LATER sheet. */
/* ⚠ AND THE CARD IS 440px, NOT 380 — A MEASUREMENT, NOT A PREFERENCE. A label column takes
   112px out of the card's width, and at 380 the widest placeholder the door has
   (`username (a-z 0-9 _ . -)`, 24 characters at the 16px that keeps iOS from zooming) drew as
   `username (a-z 0-9 _` at BOTH viewports. 440 gives the field 264px of text, which fits it
   with room. At PHONE_BP the viewport is the cap anyway and the label becomes a caption. */
.pns-door.pns-door{border:0;background:none;padding:0;margin-top:9vh;max-width:440px}
.pns-door .pns-door-panel{padding:26px 18px 20px}
/* THE HEADING, OFF THE SCREEN AND STILL IN THE DOCUMENT — the primitive's rule for a tab
   label, in the house spelling. ⚠ `.authcard h2` is (0,1,1) in a later sheet, so this needs
   its own element+class pair to win. */
h2.pns-door-h{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  clip-path:inset(50%);white-space:nowrap;margin:0;padding:0;border:0}
/* the one sentence under the tab. It is a LINE, not a paragraph — the references allow a
   caption and refuse a block of prose in a box, and this is the invite law saying itself. */
.pns-door .sub{margin:2px 0 16px;color:var(--dim);font-size:11px;line-height:1.5}

/* ── THE FIELDS ARE ONE BOX OF CAPPED ROWS ──────────────────────────────────────────────── */
/* ⚠ THE SEPARATORS ARE THE CONTAINER'S OWN GROUND SHOWING THROUGH A 1px GAP, which is how the
   primitive draws cells: they can never disagree with the border, never double at a corner and
   never need a `:last-child` exception. */
.pns-door-rows{display:flex;flex-direction:column;gap:1px;background:var(--hair);
  border:1px solid var(--hair)}
.pns-door-row{display:grid;grid-template-columns:minmax(0,112px) minmax(0,1fr);
  align-items:stretch;background:var(--sunk)}
/* THE LABEL COLUMN — reference 01's, in --dim rather than in the accent: the tab and the
   submit already spend the one accent this screen has, and a third amber thing would make the
   panel a colour scheme instead of a hierarchy. It is still a `label[for]`, so it is still a
   tap target that focuses the field beside it. */
/* ⚠ THE COLUMN IS 112px AND THE TYPE IS 9px, AND BOTH NUMBERS WERE MEASURED, NOT CHOSEN. At
   88px and 10px on .14em tracking the word broke as `USERNAM / E` at both viewports — a label
   column whose whole job is to be scannable, hyphenating its two words. The widest label the
   door has is `NEW PASSWORD`; this fits it on one line inside a 380px card. */
.pns-door .pns-door-lab{display:flex;align-items:center;margin:0;padding:0 10px;
  border-right:1px solid var(--hair);color:var(--dim);font-size:9px;line-height:1.3;
  text-transform:var(--caps);letter-spacing:calc(.1em * var(--track))}
/* ⚠ 16px OR iOS ZOOMS THE PAGE ON FOCUS, the same reason `.searchbar input` carries it. The
   box draws no border of its own — the row it sits in is the drawn control. */
.pns-door .pns-door-in{border:0;border-radius:0;background:none;color:var(--ink);
  font:inherit;font-size:16px;padding:11px 13px;min-width:0;width:100%}
.pns-door .pns-door-in::placeholder{color:var(--faint)}
/* THE FOCUS RING IS THE ROW, NOT THE BOX. A ring drawn on a borderless input inside a hairline
   row is a rectangle floating inside a rectangle; `:focus-within` lights the row that IS the
   control, which is also the one the label belongs to. */
.pns-door .pns-door-in:focus{outline:none}
.pns-door-row:focus-within{background:var(--raised)}
.pns-door-row:focus-within .pns-door-lab{color:var(--accent);border-right-color:var(--accent)}
@media(pointer:coarse){ .pns-door .pns-door-in{min-height:var(--tap-min)} }
/* ⚠ BELOW 600px THE LABEL COLUMN BECOMES A CAPTION OVER ITS FIELD, AND THAT WAS MEASURED TOO.
   The widest placeholder on the door is `username (a-z 0-9 _ . -)`; at 16px (the size that
   keeps iOS from zooming the page on focus, so it cannot come down) it needs ~230px of field,
   and a 380px card minus a label column leaves 185. It drew as `username (a-z 0-9 _` — a hint
   clipped mid-word on the archive's front door, which it was NOT before this change.
   ⛔ THIS IS NOT THE OLD SHAPE BACK. The old screen was six loose things stacked down a box;
   this is one hairline container whose rows are separated by its own ground, with the caption
   INSIDE the row it belongs to. The label column is the desktop's, where the width is real.
   ⚠ THE RUNG IS PHONE_BP, NOT A NUMBER THIS SCREEN LIKED. 599 was the width the measurement
   wanted and mobilebreakpoints.test.js refused it: the ladder is 400/640/820/999/1280/1800 and
   a new rung needs a reason of its own, which "my card is 440px wide" is not. */
@media(max-width:640px){
  .pns-door-row{grid-template-columns:minmax(0,1fr)}
  .pns-door .pns-door-lab{border-right:0;padding:9px 13px 0;font-size:9.5px}
  .pns-door .pns-door-in{padding:3px 13px 11px}
  .pns-door-row:focus-within .pns-door-lab{border-right-color:transparent}
  .pns-door-coderow .pns-door-lab{flex-direction:row;align-items:center;gap:8px;
    padding:9px 13px 0}
}

/* THE INVITE MARKER — the one field on this site whose name is also a law, said once, beside
   the label it belongs to (docs/design/DESIGN.md). It is a `<span>`: the marker is a badge,
   not a second control, and a lozenge on a control is what the 44px census is for. */
.pns-door-coderow .pns-door-lab{flex-direction:column;align-items:flex-start;
  justify-content:center;gap:5px;padding-top:10px;padding-bottom:10px}
.pns-door-inv{align-self:flex-start;font-size:8.5px;padding:1px 5px}

/* ── THE SUBMIT, THE ERROR, THE WAY OUT ─────────────────────────────────────────────────── */
/* ⚠ `.authcard button.go` is (0,2,1) IN A LATER SHEET, so the primary needs an element and two
   classes here or the primitive's filled button loses to an outlined one. */
/* 🐛 AND IT RESTATES THE FILL AND THE INK, WHICH IS THE BUG THIS RULE WAS WRITTEN FOR.
   `.authcard button.go` sets `color:var(--accent)` at (0,2,1) in a later sheet, so the
   primitive's filled primary drew an ACCENT WORD ON AN ACCENT GROUND: an empty amber slab
   where `ENTER` should be, at both viewports, on the only screen a stranger ever sees.
   Photographed before it shipped. Tokens, and the primary's own pair. */
.pns-door .pns-door-panel button.pns-door-go{width:100%;margin-top:16px;padding:11px;
  border-radius:0;font-size:11px;background:var(--accent);color:var(--void);
  border:1px solid var(--accent);font-weight:700}
.pns-door .pns-door-panel button.pns-door-go:hover{background:var(--accent-deep);
  border-color:var(--accent-deep);color:var(--void)}
/* ⚠ AND THE FLOOR IS RESTATED FOR THIS SELECTOR. Stating a padding AND a font-size in one rule
   is the pair mobiletaps.test.js measures, and 11px of type on 11px of padding is 41 — three
   short. It matches by SELECTOR and a grant it cannot see is a grant it reads as revoked. */
@media(pointer:coarse){ .pns-door .pns-door-panel button.pns-door-go{min-height:var(--tap-min)} }
/* THE ERROR IS THE ONE ALARM ON THIS SCREEN. The message is the server's own sentence in
   --bad, under the hazard edge the primitive draws, with room above the text for the 6px
   stripe to stand in. At rest the row draws nothing at all — no reserved height, no empty box. */
.pns-door .pns-door-msg{margin:14px 0 0;font-size:11.5px;line-height:1.5}
.pns-door .pns-door-msg.bad{padding-top:14px;color:var(--bad)}
.pns-door .pns-door-msg:not(.bad){margin:0}
/* the way to the other door stays the quiet 11px sentence it was ruled to be (forage-69n), and
   keeps the real box it grew on a thumb. */
.pns-door .alt{margin-top:16px;color:var(--faint)}
/* ▲▲▲ /login and /register ends ▲▲▲ */

/* ════════════════════════════════════════════════════════════════════════════════════════
   /post — THE DESKTOP POST PAGE (docs/design/DESIGN.md, forage-qw2w.15)

   ⚖ THE WHOLE REGION IS FENCED AT ≥1000px, AND SO IS THE MARKUP. `web/viewer.js` is the
   MOBILE post surface by ruling (docs/SURFACES.md) and this page hands off to it on a coarse
   pointer, so nothing below may reach a phone. The fence alone could not promise that: the
   classes it hangs off come from the width-agnostic primitive, and `.pn > .pn-tab` is
   absolutely positioned at every width. So page-post.js reads the same rung (`PANEL_RUNG`)
   and simply does not add them below it — see the note there. Two gates, one number.
   ⚠ 1000px, NOT 821px, AND THAT IS A DELIBERATE GAP. The post grid goes two-column at 821px
   (app-feeds.css), but 1000px is the rung the PRIMITIVE itself changes at — `.pn > .pn-meta`
   comes off the panel's edge below it — and the spec names 1000px as the narrowest picture it
   is answerable for. Between the two this page keeps the look it had, whole, rather than half
   of each.

   ⚑ THIS SHEET LOADS *ABOVE* app-feeds.css / app-profile.css / app-tagrows.css, which is the
   opposite of every other region's situation: those three hold the post page's PRE-repaint
   rules and they win every tie. So a rule here that has to beat one of them carries the extra
   class to do it — `.pns-post .pns-post-sec.pns-post-sec` (0,3,0) over `.ctl .sec` (0,2,0),
   and so on down. The doubled class is the sheet's own house spelling (`.pns-door.pns-door`);
   it is not decoration and removing one is how a rule silently stops applying.

   TOKENS ONLY. No inline style but `--pn-fill` (this screen writes none at all).
   ════════════════════════════════════════════════════════════════════════════════════════ */
@media(min-width:1000px){

  /* ── THE TWO COLUMNS ────────────────────────────────────────────────────────────────────
     The tab labels sit 9px ABOVE their panel, so the column needs a little headroom or the
     first one is clipped by the masthead — the exact defect lane G photographed on /tags. */
  .pns-post{padding-top:24px}
  .pns-post .pns-post-ctl{gap:26px}
  .pns-post .pns-post-comments{margin-top:30px}

  /* ── THE FRAME ──────────────────────────────────────────────────────────────────────────
     The media keeps its frame and gains the references' corner brackets — a `.pn` with no tab
     and no padding, so the stage still fills it edge to edge. `.stage` drops the border it was
     drawing itself; two boxes one pixel apart is the doubled hairline the grammar exists to
     avoid, and the outer one is the one that carries the brackets. */
  .pns-post .pns-post-frame{--pn-pad:0}
  .pns-post .pns-post-frame .stage{border:0;background:none;padding:12px}
  /* the foot row: the hint at the left, the kind at the right, ON the frame's bottom edge. */
  /* ⚠ NO GROUND ON THE FOOT ROW. The corner brackets are BACKGROUND layers on the panel, 3px
     inside its padding box (PANEL_PRIMITIVE.md) — an opaque child painted over that corner
     erases the bottom two, which is how a bracketed frame ends up bracketed on one end only. */
  .pns-post .pns-post-foot{margin:0;padding:8px 14px;border-top:1px solid var(--hair);
    background:none}
  .pns-post .pns-post-foot .sh-text{font-size:11px;color:var(--dim)}
  .pns-post .pns-post-foot .pns-post-kind{border-color:color-mix(in srgb,var(--accent) 55%,transparent);
    color:var(--accent);letter-spacing:calc(.14em * var(--track))}

  /* ── THE ACTION BAR ─────────────────────────────────────────────────────────────────────
     Every control in one anatomy: a hairline box with a spaced label, the vote keeping its two
     arrows with the score between them, the ⋯ still flush right on its own `.push`. */
  .pns-post .pns-post-barwrap{margin-top:16px}
  /* ⚖ THE ROW AGREES BY CONSTRUCTION, NOT BY TWO MATCHING NUMBERS. `.pns-post-act` states the
     height and everything else in the bar stretches to it, so the vote pill can never drift a
     pixel out of line with the buttons beside it. ⚑ AND IT IS WHY THERE IS NO HEIGHT ON `.vote`:
     mobiletaps.test.js's census matches by SELECTOR and reddened the moment one was stated —
     rightly, since it cannot see that this whole region is fenced at 1000px. A control with no
     height of its own has nothing for the census to measure and nothing to keep in sync. */
  .pns-post .pns-post-bar{gap:6px;align-items:stretch}
  /* ⚑ ONE RHYTHM. Every cell states the same floor and centres what it holds, so a row of six
     reads as a row of six rather than as six accidents — 04-terminal-ui-dashboard's
     QUICK_ACTIONS is four equal cells and every one of them is a word. */
  .pns-post .pns-post-act{background:var(--sunk);border-color:var(--hair);color:var(--ink);
    height:34px;font-size:13px;padding:0 10px;min-width:104px;justify-content:center;
    flex:0 0 auto}
  /* ⚠ A FLOOR, AND DELIBERATELY NO GROW. At 118px the seventh cell missed the media column by
     15px and MORE dropped onto a line of its own — the exile this bead is about, wearing a
     different hat. The floor came down to 104px so all seven fit at 1440. ⛔ AND `flex-grow`
     IS NOT THE ANSWER TO THE REST: it looks right on a full line and is ugly on a short one —
     photographed at 1000px, where the row wraps, growing made SAVE a 965px cell beside a
     104px MORE. A stated floor with no grow gives every cell the same shape on every line and
     leaves the slack at the end of the row, which is where slack belongs. */
  /* ⚖ AND THE ⋯ COMES HOME. `.ovwrap.push` puts `margin-left:auto` on the overflow so its menu
     hugs the viewport's right edge — right on a phone, where the menu would otherwise open off
     screen, and wrong here: at 1440 it threw a seventh peer 900px away from the six it belongs
     to, "a special one with its own bar" in the owner's words. The menu is right-anchored to
     `.ovwrap` itself, so it still opens leftward and on-screen from wherever the row ends. */
  .pns-post .pns-post-bar .ovwrap.push{margin-left:0}
  .pns-post .pns-post-word{white-space:nowrap}
  .pns-post .pns-post-act:hover{border-color:var(--hair-hi);color:var(--bright);background:var(--raised)}
  .pns-post .pns-post-act .c{color:var(--dim);font-size:10px;
    text-transform:var(--caps);letter-spacing:calc(.12em * var(--track))}
  .pns-post .pns-post-bar .vote{background:var(--sunk);border-color:var(--hair);padding:0 8px}
  /* ⚖ THE SCORE IS THE DATUM ON THIS BAR, so it takes the accent and tabular figures — the
     one number the references would have drawn big. The arrows stay --faint until yours is
     cast, which is the state `.vote .vbtn.on` already paints. */
  .pns-post .pns-post-bar .vnet{color:var(--accent);font-variant-numeric:tabular-nums}

  /* ── THE META PANEL ─────────────────────────────────────────────────────────────────────
     The strip's facts, as cells in ONE hairline box. ⚖ THE FIGURE IS NOT 28px HERE and that is
     not a compromise: `.pn-num`'s size is written for a panel the width of a page, and three
     of these share a 356px column. The label under it is what makes it a readout rather than a
     number, and the label is the tooltip this row has always carried. */
  .pns-post .pns-post-meta{padding-bottom:0}
  .pns-post .pns-post-facts{--pn-cols:3}
  .pns-post .pns-post-facts.is-1{--pn-cols:1}
  .pns-post .pns-post-facts.is-2{--pn-cols:2}
  .pns-post .pns-post-facts.is-4{--pn-cols:2}
  /* five facts in three columns would leave one empty track, and an empty track in a
     `.pn-cells` grid is a solid block of --hair, not a gap (PANEL_PRIMITIVE.md). */
  .pns-post .pns-post-facts.is-5 .pns-post-fact:last-child{grid-column:span 2}
  .pns-post .pns-post-fact{padding:11px 11px 12px;min-width:0}
  .pns-post .pns-post-fig{font-size:15px;color:var(--bright);overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap}
  .pns-post .pns-post-flab{font-size:9px;margin-top:5px;letter-spacing:calc(.14em * var(--track));
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* the foot: who uploaded it, and whether its text is searchable. */
  .pns-post .pns-post-by{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
    padding:12px 2px 16px;font-size:12px;color:var(--dim)}
  .pns-post .pns-post-by .mi{display:inline-flex;align-items:center;gap:5px;min-width:0}
  .pns-post .pns-post-by .mi-g{color:var(--faint)}
  .pns-post .pns-post-by .pns-post-ocr{margin-left:auto}
  .pns-post .pns-post-metaage{font-variant-numeric:tabular-nums}

  /* ── EVERY DISCLOSURE IS A PANEL ────────────────────────────────────────────────────────
     DETAILS · TAGS · FAMILY · SIMILAR · IMPORTED FROM · COMMENTS. The header button IS the
     tab on the top edge and the count IS the meta slot at the top right, so the section's
     name and its number stop being two unrelated words at opposite ends of a rule.
     ⚠ overflow:visible, OR THE TAB IS CLIPPED. `.ctl .sec` hides its overflow for the fold;
     the clip that actually matters is on `.body > div`, which is untouched. */
  .pns-post .pns-post-sec.pns-post-sec{background:var(--raised);border-color:var(--hair);
    overflow:visible}
  .pns-post .pns-post-sec.pns-post-sec .inner{padding:4px 0 0}
  /* ⚖ A FOLDED SECTION IS A LABELLED BAR AND NOTHING ELSE (forage-qw2w.20). Shrinking the
     padding was not enough: what a closed DETAILS still drew was a 40px bracketed rectangle
     around nothing, and at that height the corner brackets — 11px arms 3px inside the box —
     nearly met and read as a broken frame rather than a bracket. So the box, the ground and the
     brackets all go while it is shut. What is left is the reference grammar for a thing you can
     open: the name on a chip, its count at the far end, a hairline under both. The bracketed
     panel is what OPENING draws, which is also what makes opening legible.
     ⚠ THE TAB AND THE META COME BACK INTO FLOW. Both are absolutely positioned against the
     panel's edges, and an edge that is no longer drawn cannot hold them — a closed section
     would collapse to nothing with its own label hanging above it. Ordered, not reordered in
     the DOM: `.pn > .pn-meta` must stay a DIRECT CHILD and it is inserted first (see
     panelizeSection), so `order` puts the name back in front of its count. */
  /* ⚠ ROW-GAP ZERO. The folded `.body` is a real flex item at zero height, so a symmetric gap
     pushed the hairline 19px below a bar that has nothing between them. */
  .pns-post .pns-post-sec.pns-post-sec:not(.open){display:flex;flex-wrap:wrap;align-items:center;
    gap:0 10px;padding:0 0 9px;border:0;border-bottom:1px solid var(--hair);background:none}
  .pns-post .pns-post-sec:not(.open) > .pns-post-tab.pns-post-tab{position:static;order:1}
  .pns-post .pns-post-sec:not(.open) > .pns-post-cnt{position:static;order:2;margin-left:auto;
    padding:0;background:none;max-width:none}
  .pns-post .pns-post-sec:not(.open) > .body{order:3;flex:0 0 100%}
  .pns-post .pns-post-sec > .pns-post-tab.pns-post-tab{width:auto;padding:4px 10px;gap:7px;
    background:var(--accent);color:var(--void);border:0;line-height:1;font-size:11px;
    letter-spacing:calc(.12em * var(--track))}
  .pns-post .pns-post-sec > .pns-post-tab.pns-post-tab:hover{color:var(--void);
    background:var(--accent-deep)}
  /* the fold triangle rides ON the accent chip, so it takes the chip's ink in both states —
     `.ctl .sec.open .sh .tri` paints it --accent, which on this ground is invisible. */
  .pns-post .pns-post-sec > .pns-post-tab.pns-post-tab .tri{color:var(--void);opacity:.75}
  .pns-post .pns-post-cnt b{color:var(--accent);font-weight:600}

  /* ── THE REMIX / RE-OPEN DOORS ──────────────────────────────────────────────────────────
     One panel row each: the label at the left, the door as a panel button flush right. */
  .pns-post .pns-post-door{padding:9px 0 11px;gap:10px}
  .pns-post .pns-post-doorlbl{color:var(--dim);letter-spacing:calc(.14em * var(--track))}
  .pns-post .pns-post-go{font-size:10.5px}

  /* ── TAGS ───────────────────────────────────────────────────────────────────────────────
     ⚖ THE ROWS ARE NOT RE-DRAWN. Each tag's border IS its rung on the taxonomy ladder —
     reinforced solid, suggested dashed — and docs/design/DESIGN.md says to keep those marks.
     What the panel adds is the box they sit in, a full-width confirm, and the add box as the
     panel's foot rather than a control floating under a list. */
  .pns-post .pns-post-tags .pns-post-confirm{width:100%;margin-top:12px;justify-content:center}
  .pns-post .pns-post-tags .pns-post-addrow{margin-top:12px;border-color:var(--hair)}
  /* the toast slot reserved a line of height whether or not it had anything to say, which
     inside a padded panel read as 60px of empty box under the add row. It still grows when a
     message lands in it — it just stops holding the space open for one that has not. */
  .pns-post .pns-post-tags .tzmsg{min-height:0;margin:0}

  /* ── COMMENTS ───────────────────────────────────────────────────────────────────────────
     ⚖ THIS RE-BOXES WHAT AN OWNER RULING UNBOXED (2026-07-29: "remove the box it sits in").
     That ruling's reason was that a 356px sidebar card drawn at full width is "just a
     rectangle around the page" — true of a rectangle, and the answer to it was to delete the
     heading's container, which is how COMMENTS and its count ended up ~1,100px apart with
     nothing joining them. docs/design/DESIGN.md is the later ruling and it asks for a panel:
     the tab and the meta ARE the join, and they only exist on a box. The rectangle earns its
     keep by carrying them.
     ⚠ (0,4,0) — `.post > .postcomments > .sec` zeroes the border at (0,3,0) and loads after. */
  .pns-post .pns-post-comments > .pns-post-sec.pns-post-sec{border:1px solid var(--hair);
    background:var(--raised)}
  /* the log-row head: author, then the row's own marks, at the reference's label size. */
  .pns-post .pns-post-cmeta{font-size:11px;gap:6px;margin-bottom:5px}
  .pns-post .pns-post-cmeta .ulink{color:var(--accent)}
  /* `reply · delete · edit` were bare words; they are controls, so they are drawn as controls. */
  .pns-post .pns-post-comments .cactions{gap:6px;margin-top:8px}
  .pns-post .pns-post-clink{background:var(--sunk);border:1px solid var(--hair);color:var(--dim);
    padding:3px 9px;font-size:9.5px;text-transform:var(--caps);
    letter-spacing:calc(.12em * var(--track))}
  .pns-post .pns-post-clink:hover{color:var(--accent);border-color:var(--hair-hi);
    background:var(--raised)}
  .pns-post .pns-post-clink.danger:hover{color:var(--bad);
    border-color:color-mix(in srgb,var(--bad) 55%,transparent)}
  .pns-post .pns-post-composer{margin-top:18px}
}


/* ════════════════════════════════════════════════════════════════════════════════════════
   ▼▼▼ /dupes — THE DUPLICATE REVIEW (page-dupes.js) ▼▼▼  forage-qw2w.17.1
   docs/design/DESIGN.md §/dupes. Each review is ONE panel: the tier word is the tab, the
   header paragraph is a row of fact cells, the pair keeps its frames, the caption is a row of
   parts and the two verbs are the panel button.

   ⚑ THE THREE NAMED FAULTS THIS REGION EXISTS TO ANSWER (owner, 2026-09-05). The card carried
   all of them at once and they were photographed before a line of this was written:
     · MIXED ANATOMY IN ONE ROW — `Δ1` in accent, `1/1 frames match · min Δ1` in --ink at a
       different tracking, `close + corroborated` in --faint, `POSSIBLE DUPLICATE` in --dim
       caps, and a bordered `LIKELY` chip, all in one flex line. Four typographic systems
       spelling four unrelated facts. They are cells now, and every cell is a value over its
       name over its arithmetic — one anatomy, repeated.
     · A "SPECIAL" CELL AMONG PEERS — the `LIKELY` chip was the only bordered thing in that
       row and it sat at the same size as the words beside it, so the verdict read as one more
       fact rather than as the card's answer. It is the panel's TAB now: on the edge, in its
       own tone, where a name goes and where nothing competes with it.
     · CONTENT NOT CENTRED IN ITS CONTAINER — `.cand` is a flex column and the media is
       `object-fit:contain`, so the shorter of the two candidates sat at the top of its box with
       a band of dead --void under it. Both are centred in the space they have now.

   ⚠ THREE SHEETS DRAW THIS SCREEN AND ONE OF THEM LOADS *AFTER* THIS ONE. app.css §"dupes
   review page" holds `.dupe`, `.dhead`, `.pair`, `.cand`, `.cmeta`, `.dactions`; app-moderation
   .css adds the tier chip, the segment strip and the play curtain and sits at CSS_ORDER slot 6,
   BELOW this file. So a bare `.pns-dupes-cand` loses every tie to `.dupe .cand` (0,2,0) and
   loses even at (0,2,0) to app-moderation's copy of it. The house spelling from Lane I is the
   answer and it is used throughout: the screen's class DOUBLED, under the card's class, for
   (0,3,0). ⛔ Removing one of the two identical classes is how a rule here silently stops
   applying — it will not error, it will simply be outranked by a sheet loaded later.

   ⚠ NOT TOUCHED, DELIBERATELY: the play curtain, the shared-segment strip, the pair's paired
   start and drift correction, the gif size guard, the dot bar, the read-only branch, the three
   resolution animations and both second-pass verbs. docs/surfaces/dupes.md calls this screen an
   INSTRUMENT and every one of those is the instrument, not the paint.
   ════════════════════════════════════════════════════════════════════════════════════════ */

/* #app IS the column here (page-dupes.js says why it may not gain a wrapper), so the ground's
   dot grid is behind the whole page.
   ⛔ AND THAT IS WHY THIS IS NOT `.pns-col`. The shared column states `padding:18px 0 40px`,
   which is correct on a DIV inside `.wrap` and catastrophic on `.wrap` ITSELF: the shorthand
   re-states the horizontal padding as 0 and app-screens.css loads after app.css, so `.wrap`'s
   14px gutters vanished and the panel ran off both edges of a 390px screen. CAUGHT BY LOOKING
   at the first render. The two axes are set separately here and the gutters are left alone. */
.pns-dupes-page{display:flex;flex-direction:column;gap:22px;
  padding-top:18px;padding-bottom:40px}
/* ⚠ The grid is a `::before` at z-index 0 and a STATIC child paints under it (app-panel.css's
   own warning); `.pn` carries `position:relative` already, but the counter strip and the
   second-pass section do not, so they claim their layer here. */
.pns-dupes-page > .pns-dupes-count,
.pns-dupes-page > .pns-dupes-second{position:relative;z-index:1}

/* ⚖ THE COUNT IS A LABELLED RULE, NOT A SENTENCE FLOATING OVER THE PAGE. `1 OPEN REVIEW` sat
   alone above everything in --dim, at the same weight as every other caption on the screen,
   joined to nothing — SCiPNET puts `LIVE STREAM` on the accent, on a rule, above the thing it
   names, and that is one node's worth of difference. ⛔ It stays `#app > .resultbar` and its
   text stays whatever updateBar() writes: assert.js §1 reads the number back out of it. */
.pns-dupes-count.pns-dupes-count{color:var(--accent);border-bottom:1px solid var(--hair-hi);
  padding:0 0 8px;letter-spacing:calc(.14em * var(--track))}
/* the second-pass strip is the same shape one rung quieter — it heads a section, not the page. */
.pns-dupes-second .pns-dupes-count.pns-dupes-count{color:var(--dim);
  border-bottom-color:var(--hair)}
.pns-dupes-second{margin-top:4px}

/* THE CARD. `.dupe` draws its own hairline box on --panel; the panel primitive draws the same
   box on --raised WITH its corner brackets, so the old ground and margin are handed over rather
   than doubled — the column's own gap is the space between cards now. */
/* ⚠ THE FALLBACK IS THE PRIMITIVE'S AND MUST BE COPIED WHEN IT MOVES (forage-pia1.7). This is a
   second spelling of `.pn`'s own `padding:var(--pn-pad,…)` — a DUP in the cssmap sense — written
   here only to beat `.dupe`'s own padding at equal specificity. Left at the old `22px 16px 16px`
   it would have made /dupes the one screen the density pass did not reach, suite green.
   Pinned by server/tests/paneldensity.test.js, which asserts the two spellings agree. */
.pns-dupes-card.pns-dupes-card{margin:0;padding:var(--pn-pad,18px 16px 14px);
  background-color:var(--raised)}

/* THE VERDICT, ON THE EDGE. ⚖ THE COLOUR IS THE MEANING and app-moderation.css already decided
   it: good / accent / faint is "how many independent rungs agreed", not decoration. The tab is
   a FILLED chip, so the tier's token is spent on `color` and `background:currentColor` fills
   the chip with it; `-webkit-text-fill-color` then paints the GLYPHS in --void, so one node is
   a filled chip in the tier's own colour with a legible label on it (the Lane G spelling).
   ⚠ (0,2,0) to beat `.pn > .pn-tab`, which is two selectors' worth; app-screens.css loads after
   app-panel.css so the tie goes here. */
.pns-dupes-verdict.pns-dupes-verdict{background:currentColor;color:var(--dim);
  -webkit-text-fill-color:var(--void)}
.pns-dupes-verdict.is-confirmed{color:var(--good)}
.pns-dupes-verdict.is-likely{color:var(--accent)}
.pns-dupes-verdict.is-weak{color:var(--faint)}
.pns-dupes-verdict.is-merged{color:var(--accent)}
.pns-dupes-verdict.is-dismissed{color:var(--faint)}
/* `possible` is the no-strength fallback and keeps the neutral --dim above. */

.pns-dupes-meta.pns-dupes-meta{font-variant-numeric:tabular-nums}

/* ── THE FACTS, AS CELLS ──────────────────────────────────────────────────────────────────
   ⚠ `auto-fit`, NOT `--pn-cols`, AND NEVER `auto-fill`. The cell count is DATA: a still image
   pair has one fact, a deep-verified video pair has four. A fixed column count leaves an empty
   track, and because the primitive's separators ARE the container's ground, an empty track
   renders as a solid block of --hair — the hole app-panel.css warns about. `auto-fill` invents
   exactly that; `auto-fit` collapses it. At 390px one 150px minimum fits twice, so the phone
   gets the 2-up the spec asks for without this file naming a breakpoint. */
.pns-dupes-facts.pns-dupes-facts{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  margin:0 0 14px}
/* ⚠ AND AUTO-FIT ONLY COLLAPSES A TRACK THAT IS EMPTY END TO END — not a hole in the LAST ROW.
   CAUGHT BY LOOKING: three cells in two columns at 390px left the fourth position empty and it
   rendered as a solid block of --hair beside `SIGNALS`, which is the exact defect app-panel.css
   describes. Below the house rung the row is two columns and an odd last cell spans them both;
   above it every cell fits on one row, so there is no last row to hole. */
@media(max-width:999px){
  .pns-dupes-facts.pns-dupes-facts{grid-template-columns:repeat(2,minmax(0,1fr))}
  .pns-dupes-cell:last-child:nth-child(odd){grid-column:1/-1}
}
/* ⚠ RESERVE WHAT LIGHTS (the primitive's fourth law): nothing in this row is lit today, but a
   cell that cannot take the 2px is a cell the next person cannot light without moving it.
   ⚖ CENTRED IN ITS OWN CONTAINER, which is the owner's third named fault answered a second
   time. A 540px cell at 1440 holding `Δ1` against its left wall is a datum stranded in a box;
   the dashboard's tiles and SCiPNET's status cell both centre, and a column of centred values
   is also what makes three unequal words read as one row. */
.pns-dupes-cell{border-top:2px solid transparent;padding:11px 13px 12px;text-align:center}
.pns-dupes-val{font-size:22px}
/* the third line — `min Δ1 · 2 in sequence`, `close + corroborated`. It is EVIDENCE, so it is
   the quietest thing in the cell and it never wears caps: these are arithmetic and rung names,
   and spacing them out as a label would make them compete with the label above them. */
.pns-dupes-note{margin-top:5px;font-size:10.5px;line-height:1.45;color:var(--faint);
  overflow-wrap:anywhere}

/* ── THE PAIR ────────────────────────────────────────────────────────────────────────────── */
.pns-dupes-card .pns-dupes-pair.pns-dupes-pair{gap:14px}
/* ⚖ THE KEEPER'S GREEN EDGE IS MEANING AND IT STAYS (app.css `.dupe .cand.keep`). */
.pns-dupes-card .pns-dupes-cand.pns-dupes-cand{background:var(--sunk);
  border:1px solid var(--hair);padding:10px 10px 16px;gap:8px;justify-content:center}
.pns-dupes-card .pns-dupes-cand.keep.keep{border-color:var(--good)}
/* ⚠ CENTRED IN ITS OWN CONTAINER — the third named fault. `object-fit:contain` inside a flex
   column whose sibling column is taller left the picture parked at the top of its box with a
   band of --void under it. `margin-block:auto` hands the slack back to both sides equally. */
.pns-dupes-card .pns-dupes-cand.pns-dupes-cand > img,
.pns-dupes-card .pns-dupes-cand.pns-dupes-cand > a,
.pns-dupes-card .pns-dupes-cand.pns-dupes-cand > video,
.pns-dupes-card .pns-dupes-cand.pns-dupes-cand > .cand-gifhold{margin-block:auto}

/* THE CAPTION ROW. Three jobs, three shapes: the id is a chip you can press, the facts are a
   dim run with a hairline dot between them, and the outcome is a --good lozenge. */
.pns-dupes-card .pns-dupes-cmeta.pns-dupes-cmeta{gap:8px;align-items:center;
  font-variant-numeric:tabular-nums}
/* ⛔ THE ID CHIP IS THIS SCREEN'S OWN LOZENGE, NOT `.pn-loz` — page-dupes.js says why (it is an
   `<a>` on the video branch, and the census would take the primitive's 19px chip under the 44px
   floor site-wide). Same shape, restated; the floor is spent as a vertical OVERHANG, the house
   idiom (app.css `.btn::after`, app-screens.css `.pns-alerts-toggle`). ⚠ `left:0;right:0` and
   never a width: a horizontal halo would steal the tap from the facts beside it. */
.pns-dupes-card .pns-dupes-id.pns-dupes-id{position:relative;flex:0 0 auto;
  display:inline-block;font-size:10px;line-height:1.5;padding:1px 6px;white-space:nowrap;
  text-transform:var(--caps);letter-spacing:calc(.1em * var(--track));
  font-variant-numeric:tabular-nums;color:var(--accent);text-decoration:none;
  border:1px solid color-mix(in srgb,var(--accent) 55%,transparent)}
a.pns-dupes-id:hover{color:var(--bright);border-color:var(--accent)}
/* ⚠ THE OVERHANG'S SELECTOR MUST BE THE BASE RULE'S SELECTOR, CHARACTER FOR CHARACTER. The
   census credits an escape hatch to the exact string it finds, minus the `::after` — a rule
   written for `a.pns-dupes-id` would not have covered `.pns-dupes-card .pns-dupes-id
   .pns-dupes-id` and the gate said so. It reaches the SPAN variant too, which costs nothing:
   there is no other control within the chip's own width. */
@media(pointer:coarse){
  .pns-dupes-card .pns-dupes-id.pns-dupes-id::after{content:"";position:absolute;
    left:0;right:0;top:50%;transform:translateY(-50%);height:var(--tap-min)}
}
.pns-dupes-fact{color:var(--dim);font-size:11px}
/* the separator is the row's own punctuation, not a word — the same argument as the cells'
   hairlines being the container's ground showing through. */
.pns-dupes-fact + .pns-dupes-fact::before{content:"·";margin-right:8px;color:var(--hair-hi)}
.pns-dupes-card .pns-dupes-keep.pns-dupes-keep{font-size:10px;line-height:1.5;padding:1px 6px;
  letter-spacing:calc(.1em * var(--track));margin-left:auto}

/* ── THE ACTION ROW ──────────────────────────────────────────────────────────────────────
   The two verbs take the panel button and MERGE takes the primary fill, because one of them
   soft-deletes a member's post and the other clears the card. ⚠ `.btn.ghost` (0,2,0) still
   tints dismiss --dim and it is left alone: that is the same "one primary, one quiet" pair the
   references draw, already spelled correctly by app.css. */
.pns-dupes-card .pns-dupes-acts.pns-dupes-acts{gap:10px;margin-top:16px;flex-wrap:wrap}
/* ⚠ A WRAPPED BUTTON THAT KEEPS ITS LABEL'S WIDTH IS A STUB. CAUGHT BY LOOKING at 390: the two
   verbs wrapped onto two lines and each drew 192px of a 358px row, so the panel ended on two
   short bars floating against its left wall. On a phone they are rows; above the rung they stay
   the size of their own labels, because a 780px MERGE button is a different mistake. The
   element selector is deliberate — this sheet may not name a `.pn-*` class (see the prelude). */
@media(max-width:999px){
  .pns-dupes-card .pns-dupes-acts.pns-dupes-acts > button{flex:1 1 160px}
}
/* the read-only branch, and the outcome note a resolved card is left holding. `.dupe .dnote`
   pushes both flush right with `margin-left:auto`, which is right for a note BESIDE two buttons
   and wrong for one that is the whole row. */
.pns-dupes-card .pns-dupes-hint.pns-dupes-hint{margin-left:0;color:var(--dim);font-size:11.5px}
.pns-dupes-card .pns-dupes-acts > .dnote:only-child{margin-left:0;color:var(--dim);
  font-size:11.5px}

/* ── THE SECOND PASS ─────────────────────────────────────────────────────────────────────
   The byline is a log row's grammar: who, then what they did. The name takes the actor's tone
   (app-panel.css `.pn-actor`) because a name IS the datum on that line. */
.pns-dupes-card .pns-dupes-byline.pns-dupes-byline{display:flex;flex-wrap:wrap;gap:0 4px;
  align-items:baseline;margin:0 0 14px;font-size:11.5px;text-transform:none;
  letter-spacing:calc(.02em * var(--track))}
.pns-dupes-who{color:var(--accent)}
.pns-dupes-did{color:var(--dim)}
/* ▲▲▲ /dupes ends ▲▲▲ */
