/* shell.css - the shell's own styles, lifted verbatim out of index.html.
   Nothing here changed in the move: the rules, their order and their specificity
   are the ones that were in the <style> element, because the cascade is order and
   a reordering is a redesign. The markup now says what it is and this says how it
   looks, which is the whole of the change.
   Tokens live in tokens.css and are loaded first. */
/* ══ F1 · the shell ═══════════════════════════════════════════════════════
   Structure: variant D - tree · reader · outline. Content areas are
   placeholders until F2 (reader) and F3 (search) land. Role gating is a
   [data-role="reader"] hides .ed-only; writer and admin see it. The role is
   written once, from /api/me - see F4-roles.md.                              */
@font-face{font-family:'Newsreader';src:url(/fonts/Newsreader-var.woff2) format('woff2');font-weight:200 800;font-display:optional}
@font-face{font-family:'JetBrains Mono';src:url(/fonts/JetBrainsMono-var.woff2) format('woff2');font-weight:100 800;font-display:optional}

*{box-sizing:border-box}
.skip{position:absolute;left:-9999px;top:10px;z-index:200;background:var(--card);border:1px solid var(--rule2);border-radius:var(--r-md);padding:8px 14px;font-size:13px;color:var(--accent-ink)}
.skip:focus{left:10px}
main:focus{outline:none}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:.01ms !important;animation-duration:.01ms !important;scroll-behavior:auto !important}} html,body{height:100%}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-xs)}
a{transition:color var(--dur-micro) var(--ease),background-color var(--dur-micro) var(--ease),border-color var(--dur-micro) var(--ease)}
button,.search{transition:color var(--dur-micro) var(--ease),background-color var(--dur-micro) var(--ease),border-color var(--dur-micro) var(--ease),box-shadow var(--dur-small) var(--ease),transform var(--dur-small) var(--ease)}
body{margin:0;background:var(--paper);color:var(--text);font-family:var(--font-ui);font-size:var(--fs-ui);line-height:var(--lh);display:flex;flex-direction:column;-webkit-font-smoothing:antialiased}
a{color:var(--accent)}
::selection{background:var(--wash2)}
/* Deny by default: furniture is hidden unless the role in force is one that may
   edit. This used to read [data-role="reader"], which hid it from readers and
   showed it to every other value - including a typo. Found 2026-08-23. */
html:not([data-role="writer"]):not([data-role="admin"]) .ed-only{display:none !important}
/* The tree is the corpus-keeper's inventory, not the reader's map (decider,
   2026-08-18). Readers get the curated home, search, and prev/next; the tree
   returns with the editor role, where tending the corpus is the job. */
html:not([data-role="writer"]):not([data-role="admin"]) .tree{display:none}
/* Wrapped in the same breakpoints the base grid uses - the unscoped version outranked
   the phone rule and squeezed the home page into a 150px column beside a dead toc
   rail. Found on a screenshot, again. */
/* R1 - The Essay (decider's pick, D13, 2026-08-18, after the left-rail direction
   read as "distant"). Readers get ONE centered column and nothing beside it; the
   contents live behind a quiet pill next to the progress bar. The tree and the
   right rail remain editor furniture. */
html:not([data-role="writer"]):not([data-role="admin"]) aside.toc{display:none}
@media (min-width:769px){html:not([data-role="writer"]):not([data-role="admin"]) .shell{grid-template-columns:minmax(0,1fr)}}

/* THE LOBBY HAS NO FURNITURE, whatever role is reading (2026-08-23).
   The tree is an index of the corpus and the rail is an outline of one document.
   On the front door the first is a duplicate - the Lobby IS the index, and the two
   disagreed about ordering - and the second has no document to outline, so it showed
   its placeholder sentence beside a page that is not a document. An editor arriving
   home met three columns, two of them redundant.
   Scoped to >=781px because below that the phone rules already collapse both, and an
   unscoped rule here would fight them - which is the mistake recorded twelve lines
   above, where an unscoped role rule squeezed the home page into a 150px column. */
@media (min-width:769px){
  .shell[data-view="home"]{grid-template-columns:minmax(0,1fr)}
  .shell[data-view="home"] aside.tree,
  .shell[data-view="home"] aside.toc{display:none}
}


/* THE "desk" SHELL VALUE IS RETIRED (2026-09-17), and this is its headstone.
   V8 shipped as data-view="doc" and inherited the reader's furniture: an editor
   writing a draft got the corpus index on the left and, on the right, an outline
   rail showing its placeholder sentence, because a draft is not a published
   document and has nothing to outline. The fix was a shell value of its own,
   shared with Settings, that hid both asides - and it was right about the asides
   and wrong about being separate. The Desk, Settings and the Registry are three
   rooms of one wing and they want the same frame, so all three sit on "admin"
   below and this value has no views left. Removed rather than kept as dead CSS,
   because checks/check-furniture.mjs reads these rules as the claim they are.

/* THE REGISTRY IS THE ADMIN SHELL (V10, 2026-09-11). It wants furniture no other view
   has - a collapsible sidebar beside the register - and neither the corpus tree nor the
   outline rail, so it carries a shell value of its own rather than sharing one
   (docs/views/README.md, item 7). The sidebar is the third kind of furniture and it is
   OPT-IN: no markup carries it, the view renders it, and this sheet keeps it hidden on
   every shell except the one that grants it with a display rule. checks/check-furniture.mjs
   reads both halves - the bare hide and the shell grant - so removing either is a blocked
   commit, not a sidebar that quietly appears on the Lobby or vanishes from the Registry.
   Not scoped to a breakpoint, like the Desk: below --bp-md the phone rules already
   collapse both asides, and the view's own sheet turns the sidebar into a drawer there. */
.shell[data-view="admin"]{grid-template-columns:var(--w-side) minmax(0,1fr)}
.shell[data-view="admin"] aside.tree,
.shell[data-view="admin"] aside.toc{display:none}
aside.sidebar{display:none}
.shell[data-view="admin"] aside.sidebar{display:block}
/* The register is a table, not prose: the reading measure does not apply, and the
   sidebar column needs the canvas's full width and no padding of its own. Scoped to
   the Registry BY ID, not to the shell: the Desk and Settings now sit on the same
   shell value and both want a measured column, and a shell-wide rule here flattened
   the Desk's 980px canvas to full bleed with no padding the first time they moved. */
.shell[data-view-id="V10"] .doc{max-width:none;padding:0}

/* ── the admin frame ─────────────────────────────────────────────────────────
   The column beside the canvas, and the widths it takes. THE COLLAPSE IS A STATE
   SWITCH, NOT A TRANSITION: the grid column changes at once and the labels leave on
   opacity and a 6px translate, because animating grid-template-columns is a layout
   animation on every frame and docs/motion.md refuses it.
   Three arrangements, and only the third is a preference:
     <=768   the column is a drawer over the page, opened by the toggle
     769..1024  the rail is FORCED - there is room for icons, not for labels
     >1024   whatever the admin chose, read from hov.admin.sidebar   */
.shell[data-view="admin"][data-sidebar="rail"]{grid-template-columns:var(--w-side-rail) minmax(0,1fr)}
.adm-side{position:sticky;inset-block-start:0;align-self:start;block-size:calc(100vh - var(--h-header));overflow:hidden;border-inline-end:1px solid var(--rule);background:var(--paper);padding-block:14px}
.adm-nav{display:flex;flex-direction:column;gap:2px;padding-inline:8px}
.shell[data-sidebar="rail"] .adm-nav{padding-inline:4px}
.adm-item{display:flex;align-items:center;gap:10px;block-size:40px;padding-inline:8px;border-radius:var(--r-md);color:var(--body);text-decoration:none;white-space:nowrap;font-family:var(--font-ui);font-size:var(--fs-ctl);font-weight:500}
.shell[data-sidebar="rail"] .adm-item{padding-inline:10px}
.adm-item svg{inline-size:20px;block-size:20px;flex:none}
.adm-item:hover{background:var(--wash);color:var(--text)}
html:not([data-role="admin"]) .adm-item[data-admin-only]{display:none}
.adm-item[aria-current="page"]{background:var(--wash);color:var(--accent-ink);font-weight:600}
/* The labels: present, or fading out toward the edge the rail keeps. The open state
   re-declares the pair on the entrance curve, per the tokens' own pairing rule. */
.adm-lbl{opacity:1;transform:none;visibility:visible;transition:opacity var(--dur-small) linear,transform var(--dur-small) var(--ease),visibility 0s linear 0s}
.shell[data-sidebar="rail"] .adm-lbl{opacity:0;transform:translateX(-6px);visibility:hidden;transition:opacity var(--dur-small) linear,transform var(--dur-small) var(--ease-exit),visibility 0s linear var(--dur-small)}
.adm-scrim{display:none}
/* The app bar in admin mode. The reading surface's own affordances step aside - the
   corpus search is a reader's tool and the breadcrumb describes a corpus position -
   and the frame's three injected controls take their place. Nothing is deleted: this
   is one attribute on <html>, so leaving the wing gives the bar back whole. */
html[data-admin] header .brand,
html[data-admin] header .crumb,
html[data-admin] header .search,
html[data-admin] header #msearch,
html[data-admin] header #ham,
html[data-admin] #adminbar{display:none}
.adm-mark{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-ui);font-weight:650;font-size:15px;letter-spacing:-.01em;color:var(--text);text-decoration:none;white-space:nowrap}
.adm-mark .mk{width:20px;height:20px;color:var(--accent)}
.adm-toggle{flex:none}
.adm-viewsite{flex:none}
@media (min-width:769px) and (max-width:1024px){
  /* The forced rail. The preference is kept, not overwritten - widening the window
     gives the admin back the sidebar they chose. */
  .shell[data-view="admin"]{grid-template-columns:var(--w-side-rail) minmax(0,1fr)}
}
@media (max-width:768px){
  .shell[data-view="admin"],
  .shell[data-view="admin"][data-sidebar="rail"]{grid-template-columns:minmax(0,1fr)}
  .adm-side{position:fixed;inset-block:0;inset-inline-start:0;inline-size:min(288px,84vw);block-size:auto;z-index:95;border-inline-end:1px solid var(--rule2);transform:translateX(-103%);visibility:hidden;transition:transform var(--dur-med) var(--ease-exit),visibility 0s linear var(--dur-med)}
  .shell[data-drawer="open"] .adm-side{transform:none;visibility:visible;transition:transform var(--dur-med) var(--ease),visibility 0s linear 0s}
  .shell[data-sidebar="rail"] .adm-nav,
  .shell[data-sidebar="rail"] .adm-item{padding-inline:8px}
  .shell[data-sidebar="rail"] .adm-lbl{opacity:1;transform:none;visibility:visible}
  .adm-scrim{position:fixed;inset:0;z-index:94;background:var(--veil);border:0;padding:0;cursor:pointer}
  .shell[data-drawer="open"] .adm-scrim{display:block}
}

/* ── the admin bar, on the reading surface ───────────────────────────────────
   32px, above the app bar, and a nav rather than a banner. Every target clears
   24x24, which is what a pointer needs and what this strip is short enough to make
   easy to get wrong. */
/* The hairline is what gives the strip an edge in the dark theme, where the header
   immediately below it is another dark surface. It is `transparent` in light, where
   the 17:1 inversion already draws the boundary - declared rather than omitted so the
   32px in --h-adminbar is the same 32px in both themes (border-box, shell.css:15),
   which three suites assert to the pixel. */
.adminbar{display:flex;align-items:center;gap:4px;block-size:var(--h-adminbar);padding-inline:12px;background:var(--block);color:var(--block-fg);border-block-end:1px solid var(--block-edge);flex:none;font-family:var(--font-ui);font-size:12.5px;z-index:21}
.ab-item{display:inline-flex;align-items:center;gap:6px;min-block-size:24px;padding-inline:8px;border:0;border-radius:var(--r-xs);background:none;color:var(--block-fg);font:inherit;font-weight:500;text-decoration:none;cursor:pointer;white-space:nowrap}
.ab-item:hover{background:rgba(255,255,255,.12);color:#fff}
.ab-item[hidden]{display:none}
.ab-mark{font-weight:650}
.ab-mark .mk{width:14px;height:14px;color:currentColor}
.ab-gap{margin-inline-start:auto}
.ab-who{display:inline-flex;align-items:center;gap:8px;min-block-size:24px;padding-inline:8px;color:var(--block-fg);opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ab-who b{font-weight:600}
.ab-chip{font-family:var(--font-mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;border:1px solid currentColor;border-radius:var(--r-xs);padding:1px 5px}
.ab-chip[hidden]{display:none}
@media (max-width:640px){.ab-who{display:none}}

/* F2 - reader */
/* Type tuned by measurement, 2026-08-18 (audit script in the commit). Targets from
   the canon: measure 66ch (was 75), heading scale ~1.35 (h2 was a flat 1.14),
   leading 1.65 for this measure (was 1.72), phone at its own size and gutter
   (was desktop values giving 37ch). Every number below is derived, not felt. */
.doc{max-width:var(--measure);margin:0 auto;padding:56px 32px 90px}
@media (max-width:768px){.doc{padding:36px 22px 70px}}
/* Measure: see the LAYOUT block in tokens.css - why 630/690, what it was
   measured at, and why 75 is not a ceiling. */
@media (min-width:1400px){.doc{max-width:var(--measure-wide)}}
/* A LIST IS NOT PROSE. The Lobby, the hubs, the Vault and the Study render indexes,
   tables and card grids into this same container, and a 66ch measure is a rule about
   READING SENTENCES - applied to a two-column index it just wastes the screen and
   wraps titles that had no reason to wrap. Now that the Lobby has no furniture beside
   it, this is where its width comes from.
   SELECTED BY data-view-id, NOT data-view, and that is the point of the id: the
   Vault and the Study both set data-view="doc" because they paint into the document
   canvas, so the shell attribute genuinely cannot tell them from a publication. The
   id can. It is the first thing in the app to USE the identity the roster argues for,
   rather than only asserting it - and it is why V3, the one view here that is really
   prose, keeps its measure. */
@media (min-width:1024px){
  .shell[data-view-id="V1"] .doc,   /* the Lobby - the corpus index */
  .shell[data-view-id="V2"] .doc,   /* a Wing - one group, newest first */
  .shell[data-view-id="V4"] .doc,   /* the Vault - a row per publication */
  .shell[data-view-id="V5"] .doc{   /* the Study - cards and a table */
    max-width:var(--w-index);
  }
  /* The Desk is the third case, and it is neither of the other two. Since the meta
     moved off the canvas into a panel beside it (2026-09-02) the view is a reading
     measure AND a 320px column, and holding that pair inside a 630px prose measure
     left the writer a 280px canvas - narrower than the phone the Desk refuses.
     ⚠ THE FOUR SELECTORS ABOVE MATCH NOTHING. src/app/routes.mjs writes the id
     ZERO-PADDED - `V01`, `V02`, `V04`, `V05` - and app.mjs copies it to
     data-view-id verbatim, so `V1` has never matched and the Lobby, Wings, Vault
     and Study have been rendering at the prose measure since this block landed.
     Found 2026-09-02 while adding the Desk's row, which is written correctly
     below. NOT fixed here: widening four views nothing in this change asked about
     is a layout decision with no suite behind it, and it belongs in its own
     commit rather than riding a panel move. */
  .shell[data-view-id="V08"] .doc{ max-width:var(--w-desk); }
}
.docmeta{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px}
.chip{font-family:var(--font-mono);font-size:10.5px;padding:3px 8px;border-radius:var(--r-sm);border:1px solid var(--rule);white-space:nowrap}
.chip-ok{color:var(--accent-ink);background:var(--wash)}
.chip-stale{color:#8a5a00;background:#fdf3e0;border-color:#e8cf9a}
[data-mode="dark"] .chip-stale{color:#e8c37a;background:#3a2e14;border-color:#6a5524}
.prov{font-family:var(--font-ui);font-size:12.5px;color:var(--soft);border-top:1px solid var(--rule2);padding:18px 0 0;margin:44px 0 0}
.prov-title{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);border:0;margin:0 0 12px;padding:0}
.verify-btn{font:inherit;font-size:11.5px;font-family:var(--font-mono);color:var(--accent-ink);background:var(--wash);border:1px solid var(--rule);border-radius:var(--r-sm);padding:10px 14px;cursor:pointer;min-height:40px}
.verify-out{font-size:11.5px}.v-ok{color:var(--accent-ink)}.v-ok::before{content:"✓ ";font-weight:700}.v-bad{color:#8a5a00;font-weight:600}.v-bad::before{content:"✗ "}.v-part{color:var(--body)}.v-unjudged{color:var(--soft)}
.prov dl{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:10px 0 2px}
.prov dt{color:var(--muted);font-weight:600}.prov dd{margin:0;overflow-wrap:anywhere}
.prov .hash{overflow-wrap:anywhere;word-break:break-all}
.prov-flag{color:#8a5a00}
[data-mode="dark"] .prov-flag{color:#e8c37a}
.docbody{font-family:var(--font-serif,Newsreader,serif);font-size:19.5px;line-height:1.65}
@media (max-width:768px){.docbody{font-size:17.5px;line-height:1.62}}
.docbody p{text-wrap:pretty}
@media (max-width:768px){.docbody p{hyphens:auto;-webkit-hyphens:auto}}
.dochead{margin:6px 0 34px}
.crumbline{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
.doctitle{font-size:38px;line-height:1.12;letter-spacing:-.022em;font-weight:650;margin:0 0 16px;text-wrap:balance}
@media (max-width:768px){.doctitle{font-size:30px}}
.metaline{display:flex;align-items:center;flex-wrap:wrap;gap:6px 0;font-family:var(--font-ui);font-size:12.5px;color:var(--soft)}
/* segments wrap internally - nowrap made the long audience line 411px in a 390px
   viewport, the exact shear the phone stranger reported. The separating dot is a
   ::before glued to the segment's first word, so it cannot orphan on a wrap. */
.metaline .mseg{white-space:normal}
.metaline .mseg+.mseg::before{content:"·";color:var(--rule2);margin:0 8px}
/* The row the late segments land in. It is present from the first paint, empty, at
   exactly the height of one line, so the reading estimate and the integrity verdict
   arriving cannot reflow the head. Its first segment carries no separating dot,
   because it is the first thing on its own row rather than a continuation. */
.metalate{min-height:calc(12.5px * var(--lh));margin-top:2px}
.metalate .mseg:first-child::before{content:none}
.chip[data-provcard]{cursor:pointer}
.av-ok{color:var(--accent-ink);font-weight:600}.av-bad{color:#8a5a00;font-weight:700}
.readcov{color:var(--muted)}
.rgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;margin:26px 0 10px}
.rcard{border:1px solid var(--rule);border-radius:var(--r-md);padding:14px 16px;background:var(--card);font-family:var(--font-ui)}
.rcard-quiet{opacity:.72}
.rc-k{font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.rc-n{font-size:32px;font-weight:650;letter-spacing:-.02em;margin:6px 0 2px}
.rc-u{font-size:12.5px;font-weight:400;color:var(--muted);margin-left:6px}
.rc-d{font-size:12px;color:var(--soft);line-height:1.5}
.rc-w{font-size:11.5px;color:var(--muted);margin-top:8px;line-height:1.45}
.sweep{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:22px 0 8px}
.sweeplist{list-style:none;margin:14px 0 0;padding:0;font-family:var(--font-ui);font-size:13px}
.sweeplist li{display:flex;gap:14px;align-items:baseline;padding:7px 0;border-bottom:1px solid var(--rule)}
.sweeplist .sw-t{flex:1;color:var(--text)}
.sweeplist .sw-v{font-family:var(--font-mono);font-size:11px;white-space:nowrap}
.resumepill{display:inline-block;margin-top:12px;font-family:var(--font-mono);font-size:10.5px;letter-spacing:.06em;color:var(--accent-ink);background:var(--wash);border:1px solid var(--rule);border-radius:999px;padding:8px 14px;cursor:pointer}
.resumepill:hover{border-color:var(--rule2)}
.hmark{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;border-radius:4px;padding:2px 6px;margin-left:8px;vertical-align:2px}
.hm-changed{color:#8a5a00;background:#fdf3e0;border:1px solid #e8cf9a}
[data-mode="dark"] .hm-changed{color:#e8c37a;background:#3a2e14;border-color:#6a5524}
/* provenance card - the time machine behind the chip */
.provcard{display:none;margin:14px 0 6px;border:1px solid var(--rule2);border-radius:var(--r-lg,12px);background:var(--card);box-shadow:var(--sh-pop);padding:16px 18px;font-family:var(--font-ui);font-size:13px;color:var(--soft)}
.provcard.open{display:block}
.pc-status{font-size:13.5px;color:var(--text);margin-bottom:12px}
.pc-row{display:grid;grid-template-columns:76px 1fr;gap:10px;padding:5px 0;border-top:1px solid var(--rule)}
.pc-row>span,.pc-hist>span{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding-top:2px}
.pc-hist{display:grid;grid-template-columns:76px 1fr;gap:10px;padding:7px 0;border-top:1px solid var(--rule)}
.pc-commit{padding:2px 0;line-height:1.5}
.pc-commit code{font-family:var(--font-mono);font-size:11px;color:var(--accent-ink);background:var(--wash);border-radius:var(--r-xs);padding:1px 5px;margin-right:6px}
.pc-commit time{font-family:var(--font-mono);font-size:11px;color:var(--muted);margin-right:6px}
.pc-actions{display:flex;align-items:center;gap:12px;padding-top:12px;border-top:1px solid var(--rule);margin-top:2px}
.shachunks{all:unset;cursor:pointer;font-family:var(--font-mono);font-size:11.5px;letter-spacing:.03em;color:var(--accent-ink);overflow-wrap:anywhere}
.shachunks:hover{text-decoration:underline}
/* the verification theater */
.vt{position:fixed;inset:0;z-index:120;background:rgba(20,16,10,.45);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;padding:20px}
.vt-card{background:var(--card);border:1px solid var(--rule2);border-radius:var(--r-xl,16px);box-shadow:var(--sh-pop);padding:26px 28px;max-width:min(520px,calc(100vw - 32px));width:100%;box-sizing:border-box;font-family:var(--font-ui);transition:border-color var(--dur-small) var(--ease)}
.vt-h{font-size:14px;font-weight:650;color:var(--text);margin-bottom:16px}
.vt-row{display:grid;grid-template-columns:74px 1fr;gap:10px;padding:8px 0;border-top:1px solid var(--rule);font-size:12px}
.vt-row span{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding-top:2px}
.vt-row code{font-family:var(--font-mono);font-size:12px;line-height:1.7;overflow-wrap:anywhere;color:var(--text)}
.vt-verdict{margin:14px 0 4px;font-size:13.5px;font-weight:650;min-height:20px}
.vt-ok{border-color:var(--accent)}.vt-ok .vt-verdict{color:var(--accent-ink)}
.vt-bad{border-color:#c47f00}.vt-bad .vt-verdict{color:#8a5a00}
.vt-close{all:unset;cursor:pointer;font-size:12.5px;color:var(--muted);padding:8px 0 0}
.vt-close:hover{color:var(--accent-ink)}
.metadot{color:var(--rule2)}
/* Contents pill + popover: the outline waits behind one quiet control beside the
   progress bar instead of owning a pane. Hidden when a doc has under 3 sections. */
.tocpill{position:fixed;top:64px;right:18px;z-index:6;font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--muted);background:var(--card);border:1px solid var(--rule);border-radius:999px;padding:10px 14px;cursor:pointer}
/* On a phone the pill was floated, so the first paragraph wrapped around it and the
   prose ran ragged for four lines against a 390px measure. A reading column that
   narrow has no room for anything beside it.
   Giving it its own line in normal flow fixed the wrap and immediately broke
   something else: the pill only exists once the body has been parsed for headings, so
   a 53px block appearing after the fetch pushed the prose down by exactly that much -
   the same late-arrival shift the reserved meta row was built to stop, in a new place.
   Reserving a slot for it is no good either, because a document with fewer than three
   sections never gets a pill and would carry a permanent blank line instead.
   So on a phone it leaves the flow entirely and becomes a corner button, where the
   thumb is anyway. Out of flow costs no layout, so it can arrive whenever it likes. */
@media (max-width:768px){
  .tocpill{position:fixed;top:auto;bottom:16px;right:16px;float:none;margin:0;z-index:7;
    box-shadow:var(--sh-pop);background:var(--card);padding:12px 16px;min-height:44px}
  .tocpop{top:auto;bottom:64px;right:16px;left:16px;min-width:0}
  /* A fixed button covers whatever is under it, and at the END of a document that is
     the last thing the reader came to read, permanently. Clear its height plus its
     inset so the page can always be scrolled out from under it. */
  .doc{padding-bottom:calc(70px + 44px + 32px)}
}
.tocpill:hover{color:var(--accent-ink);border-color:var(--rule2)}
.tocpop{position:fixed;top:100px;right:18px;z-index:60;background:var(--card);border:1px solid var(--rule2);border-radius:var(--r-lg,12px);box-shadow:var(--sh-pop);padding:10px 6px;min-width:230px;max-height:60vh;overflow-y:auto;display:none}
.tocpop.open{display:block}
.tocpop a{display:block;font-family:var(--font-ui);font-size:13px;color:var(--soft);text-decoration:none;padding:7px 14px;border-radius:var(--r-sm)}
.tocpop a:hover,.tocpop a.cur{background:var(--wash);color:var(--accent-ink)}
.docbody h2{font-size:26px;letter-spacing:-.015em;line-height:1.25;margin:2.1em 0 .5em;padding-top:.75em;border-top:1px solid var(--rule);text-wrap:balance}
.docbody h3{font-size:21px;line-height:1.3;margin:1.6em 0 .35em}
.docbody p{margin:.95em 0}
.docbody ul,.docbody ol{padding-left:1.4em;margin:.9em 0}
.docbody li{margin:.35em 0}
.docbody code{font-family:var(--font-mono);font-size:.8em;background:var(--wash);border-radius:var(--r-xs);padding:1.5px 5px;color:var(--accent-ink)}
.docbody pre{background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);padding:14px 16px;overflow-x:auto;margin:1.2em 0}
.docbody pre:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.docbody pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:var(--text)}
.docbody table{border-collapse:collapse;width:100%;font-size:14px;line-height:1.5;font-family:var(--font-ui);margin:1.5em 0;display:block;overflow-x:auto}
.docbody th,.docbody td{border:0;border-bottom:1px solid var(--rule);padding:8px 14px 8px 0;text-align:left;vertical-align:top}
.docbody thead th{border-bottom:1.5px solid var(--rule2);font-weight:650;font-size:12px;letter-spacing:.02em;color:var(--soft)}
.docbody blockquote{border-left:2px solid var(--accent);margin:1.2em 0;padding:.1em 0 .1em 18px;color:var(--soft);font-style:italic}
.docbody a{color:var(--accent)}
.tree .st-dot,.drawer .st-dot{width:7px;height:7px;border-radius:50%;flex:none;margin-left:auto}
.st-ok{background:var(--accent)}.st-stale{background:#d99a1f}

.gfilter{width:100%;font:inherit;font-family:var(--font-ui);font-size:14px;color:var(--text);background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);padding:8px 12px;margin:14px 0 18px}
.gfilter:focus{outline:2px solid var(--accent);outline-offset:1px}
.hublist{list-style:none;margin:0;padding:0;font-family:var(--font-ui)}
.hublist li a{display:flex;align-items:center;gap:14px;padding:9px 10px;border-bottom:1px solid var(--rule);text-decoration:none;color:var(--text);font-size:14.5px;border-radius:var(--r-sm)}
.hublist li a:hover{background:var(--card)}
.hublist .hub-date{font-family:var(--font-mono);font-size:11px;color:var(--muted);flex:none;min-width:84px}
.hublist .hub-title{flex:1}
.hublist .st-dot{width:7px;height:7px;border-radius:50%;flex:none}
.tree .all-link{color:var(--accent);font-weight:600}

/* reader home - a table of contents, not an empty state */
.home{max-width:720px;margin:0 auto;padding:44px 32px 80px;font-family:var(--font-ui)}
/* The home route's page heading. It is deliberately quiet - the lede below it is the
   line a sighted reader reads first, and this is the one a screen reader announces
   first. Sized down rather than hidden: a visually-hidden h1 is a legal answer to
   `page-has-heading-one` and a worse one, because it makes the accessible name and the
   visible page disagree. */
.home .hh1{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--soft);font-weight:500;margin:0 0 10px}
.home .hlede{font-family:var(--font-serif,Newsreader,serif);font-size:19px;line-height:1.55;color:var(--text);margin:0 0 8px}
.home .hsub{font-size:13px;color:var(--soft);margin:0 0 36px}
.home .hsec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:30px 0 6px;padding-top:14px;border-top:1px solid var(--rule)}
.home .hlist{list-style:none;margin:0;padding:0}
.home .hlist a{display:block;padding:10px 2px;text-decoration:none;color:var(--text);border-radius:var(--r-sm)}
.home .hlist a:hover .ht{color:var(--accent)}
.home .ht{font-size:15.5px;font-weight:600;letter-spacing:-.01em}
.home .hx{font-size:12.5px;color:var(--soft);margin-top:2px;line-height:1.5}
.home .hmore{font-size:12.5px;color:var(--accent);font-weight:600;text-decoration:none;display:inline-block;padding:8px 2px}
.prevnext{display:flex;justify-content:space-between;gap:16px;margin-top:34px;padding-top:16px;border-top:1px solid var(--rule);font-family:var(--font-ui);font-size:13px}
.prevnext a{color:var(--accent);text-decoration:none;max-width:46%;padding:10px 0;display:inline-block}
.prevnext .pn{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);display:block;margin-bottom:2px}

/* header */
header{display:flex;align-items:center;gap:16px;padding:0 18px;height:var(--h-header);background:var(--paper);border-bottom:1px solid var(--rule);flex:none;z-index:20}
.brand{display:flex;align-items:center;gap:10px;font-weight:650;font-size:15px;letter-spacing:-.01em;white-space:nowrap;color:var(--text);text-decoration:none}
.brand .mk{width:20px;height:20px;color:var(--accent)}
svg{display:block}
.hbtn svg{width:16px;height:16px}
.ni{width:14px;height:14px;color:var(--muted);flex:none}
.crumb{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--muted);padding-left:16px;border-left:1px solid var(--rule);height:20px;margin-left:2px}
.crumb a{color:var(--muted);text-decoration:none}
.crumb a:hover{color:var(--accent)}
.actions{margin-left:auto;display:flex;align-items:center;gap:10px;min-width:0}
.actions .sep{width:1px;height:20px;background:var(--rule);margin:0 2px}
.search{display:flex;align-items:center;gap:9px;background:var(--card);border:1px solid var(--rule);height:32px;padding:0 11px;font-size:13px;color:var(--muted);min-width:260px;cursor:pointer;border-radius:var(--r-md);box-shadow:var(--sh)}
/* THIS ELEMENT DOES NOT TRANSITION ITS COLOUR, AND THAT IS AN ACCESSIBILITY FIX.
   `button,.search` above transitions color and background-color for hover states.
   The search bar has no such state - its hover moves the border and the shadow, its
   active moves the transform - so those two properties only ever animate on the ONE
   style change nobody intended to animate: the theme flip. theme.mjs is a module, so
   it sets data-mode after the first style resolution, and `body` carries no
   transition at all. The ground therefore snaps to dark instantly while this one box
   spends 120ms fading down from the light theme's #fff/#6f737d, stranded pale on an
   already-dark page - and its own kbd hint, which is not a button and never
   transitioned, snaps ahead of it.
   Measured mid-fade on the uat rung (docs/environments.md) by src/app/tests/verify-accessibility.mjs:
   #848891 on #2c2d31 = 3.87:1, and as low as 2.21:1 on a slower sample, against the
   4.5:1 that 1.4.3 owes 13px text. At rest the same pair is #868a93 on #1c1d22 =
   4.86:1 and always was, so this was never a token defect - it was the animation
   between two passing values.
   `.search` (0,1,0) outranks `button` (0,0,1), so naming the transition here replaces
   the blanket one for this element only and leaves every real button's hover intact. */
.search{transition:border-color var(--dur-micro) var(--ease),box-shadow var(--dur-small) var(--ease),transform var(--dur-small) var(--ease)}
.search:hover{border-color:var(--rule2);box-shadow:var(--sh-raise)}
.search:active{transform:translateY(.5px)}
.search kbd{font-family:var(--font-mono);font-size:10px;border:1px solid var(--rule);background:var(--paper);padding:1px 5px;border-radius:var(--r-xs);margin-left:auto;color:var(--muted)}
.hbtn{font-size:14px;background:none;border:none;color:var(--soft);width:32px;height:32px;cursor:pointer;border-radius:var(--r-md);display:inline-flex;align-items:center;justify-content:center}
.hbtn:hover{background:var(--wash);color:var(--accent)}
.hbtn:active{transform:translateY(.5px)}
/* The theme control carries three icons and shows one. Selecting on
   [data-theme] - the PREFERENCE, written by public/js/theme-boot.js before the
   first paint - rather than switching them from lib/theme.mjs is what keeps the
   button from showing the wrong shape for a frame on every load. Note it is
   data-theme and not data-mode: a reader following the system sees the monitor,
   not the sun, whichever way the system happens to resolve. */
#ic-sun,#ic-moon,#ic-system{display:none}
[data-theme="light"] #ic-sun,[data-theme="dark"] #ic-moon,[data-theme="system"] #ic-system{display:block}
.mob{display:none}

/* shell grid */
.shell{flex:1;display:grid;grid-template-columns:var(--w-tree) minmax(0,1fr) var(--w-rail);min-height:0}
aside.tree{border-right:1px solid var(--rule);overflow-y:auto;padding:24px 0 64px;font-size:13.5px;background:var(--paper)}
.grp{margin-bottom:24px}
.grp .lab{padding:0 20px 8px;font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:500}
.grp ul{list-style:none;margin:0;padding:0}
.grp li a{display:flex;align-items:center;gap:10px;height:32px;padding:0 20px;color:var(--body);text-decoration:none;line-height:1.4}
.grp li a:hover{background:var(--wash);color:var(--text)}
.grp li a:active{background:var(--wash2)}
.grp li a.on{background:var(--wash);color:var(--accent-ink);font-weight:600;box-shadow:inset 2px 0 0 var(--accent)}
.grp li a .cnt{margin-left:auto;font-family:var(--font-mono);font-size:11px;font-variant-numeric:tabular-nums;color:var(--muted);min-width:30px;text-align:right}
.dot{width:6px;height:6px;border-radius:var(--r-circle);flex:none;margin:0 4px}
.tw{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* main placeholder (F2 replaces the inside, never the frame) */
main{overflow-y:auto;overflow-x:hidden;min-width:0;background:var(--page);position:relative}
/* Reading progress - a hairline under the header, filled as the reader moves
   through the document. Doc view only: a progress bar on an index is noise.
   It is aria-hidden by design; the accessible equivalent is the static
   "N min read - N words" in the meta line plus the contents pill, which answer
   the same two questions without a per-percent announcement. */
.readbar{position:sticky;top:0;left:0;height:2px;width:100%;z-index:5;background:transparent;display:none}
.shell[data-view="doc"] .readbar{display:block}
/* Reveal, not permanent furniture. The bar is absent at the head of a document,
   where a bar reading zero is just a line, and fades in once the prose has begun
   moving. Opacity only - a 2px bar cannot slide anywhere perceptibly. In is slower
   than out so arriving is gentle and leaving is not a lingering ghost. Hysteresis
   on the threshold lives in reading-position.mjs; without it a reader parked on the boundary
   strobes. This fade is the bar's ONLY independent motion, which is why reduced
   motion has something to turn off here and nothing to turn off on the fill. */
.readbar{opacity:0;transition:opacity 110ms linear}
.readbar.shown{opacity:1;transition:opacity 160ms ease-out}
/* scaleX, not width: width relayouts and repaints the bar on every scroll frame,
   a transform is composited. transform-origin pins the growth to the reading
   edge, which is the right edge under RTL. */
/* NO transition. The bar's motion IS the scroll, and a tween can only put the paint
   behind the reader. Measured on docs/build-protocol.md at 1440x900 with the 80ms
   linear transition this line used to carry: at scrollTop 3800 the code had set
   scaleX(0.667) and the compositor was painting 0.508 - sixteen points behind, every
   frame, for the whole length of a continuous scroll, converging only once the reader
   stopped. Each frame set a new target and restarted the tween from wherever the last
   one had got to, so the bar never caught up while it was moving. The transition was
   inherited from the earlier `transition: width 80ms linear` and never questioned. */
.readbar i{display:block;height:100%;width:100%;background:var(--accent);transform:scaleX(0);transform-origin:0 50%;will-change:transform}
.readbar i:dir(rtl){transform-origin:100% 50%}
[data-region="outline-rail"] .ph a.cur{color:var(--accent-ink) !important;font-weight:600}
.empty{max-width:460px;margin:20vh auto 0;text-align:center;padding:0 24px}
.empty .tile{width:44px;height:44px;border:1px solid var(--rule);border-radius:var(--r-xl);background:var(--card);box-shadow:var(--sh);display:inline-flex;align-items:center;justify-content:center;color:var(--accent);margin-bottom:18px}
.empty .tile svg{width:18px;height:18px}
.empty h1{font-family:var(--font-display);font-weight:500;font-size:25px;letter-spacing:-.01em;margin:0 0 8px}
.empty p{color:var(--soft);font-size:13.5px;line-height:1.7;margin:0;text-wrap:balance}
.empty .keys{display:flex;gap:18px;justify-content:center;margin-top:22px;font-size:12px;color:var(--muted)}
.empty .keys span{display:inline-flex;align-items:center;gap:7px}
.empty .k{font-family:var(--font-mono);font-size:10.5px;color:var(--soft);border:1px solid var(--rule);background:var(--card);border-radius:var(--r-sm);min-width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;padding:0 5px;box-shadow:var(--sh)}

/* ══ THE LOADING LAYER ═════════════════════════════════════════════════════
   Three lines govern which regions here may show a loading state at all, and
   they are the reason this block is short. The rules and the argument are in
   docs/decisions/0017-the-region-contract.md; the timing is in loading.mjs;
   verify-loading.mjs blocks a commit that breaks either.

     CHROME NEVER LOADS.  The masthead, the crumb, the toolbar, the tree and the
     drawer are present and interactive, or the page has not started. There is no
     third state and there is no skeleton anywhere in them. Chrome is the escape
     route: an unreadable nav removes the only way to abandon a slow page at the
     exact moment a reader wants to. Its labels are constants, not fetched data,
     so skeletonising them would be theatre. The ONE exception is the hairline
     below, which keeps every one of those properties intact.

     THE CANVAS LOADS ONCE PER NAVIGATION, not once per thing inside it.

     A WIDGET LOADS ONLY IF THE READER CAN TRIGGER IT ON ITS OWN. The verify
     button and the corpus sweep can. The status chip, the authority line and the
     provenance card cannot - they start when the document does, so they are the
     canvas's event and they show nothing.                                       */

/* The exception, and it earns it on all four counts: the nav stays readable, the
   nav stays interactive, it takes 2px of nothing, and it represents work the
   reader asked for. Opacity only - it never occupies layout, which is also why
   it carries no minimum display time. */
.navprog{position:fixed;top:0;left:0;right:0;height:2px;z-index:30;pointer-events:none;opacity:0;transition:opacity 120ms linear}
.navprog[data-on]{opacity:1}
.navprog i{display:block;height:100%;width:100%;background:var(--accent);transform-origin:0 50%;animation:navslide 1.15s var(--ease) infinite}
@keyframes navslide{0%{transform:scaleX(.02) translateX(0)}55%{transform:scaleX(.72) translateX(20%)}100%{transform:scaleX(.02) translateX(4900%)}}
/* Indeterminate, and honest about it. It never fills to a number, because a bar
   creeping to 90% and stalling is the worst-measured shape in the literature and
   this page has no numerator to fill it from. The sweep DOES have one; that bar
   is determinate, and it is a different element. */
/* Reduced motion means remove LARGE-SCALE TRANSLATION, not remove liveness. Freezing
   the bar at a fixed width leaves a static line that says nothing and never resolves -
   the reader loses the only signal that anything is still happening. So the sweep
   across the viewport goes and the fill stays: the bar grows in place from the leading
   edge and holds, which is motion confined to a 2px strip with no travel. */
@media (prefers-reduced-motion:reduce){
  /* `!important` because the blanket reduced-motion rule at the top of this file sets
     animation-duration to .01ms on every element, including this one. Without the
     override the fill below would cycle 0.12 -> 0.6 a hundred times a second: the
     accessibility rule meant to calm the page would have manufactured a strobe in it,
     which is the opposite failure and a photosensitivity risk rather than a cure. */
  .navprog i{animation:navgrow 2.4s var(--ease) infinite alternate !important;transform-origin:0 50%}
}
@keyframes navgrow{from{transform:scaleX(.12)}to{transform:scaleX(.6)}}

/* Skeletons. Geometry only - what a skeleton measurably buys is that content does
   not jump when it lands, not that the wait feels shorter (no study shows that).
   Static by default, which is what most shipped skeletons are; the shimmer is
   opt-in via prefers-reduced-motion being absent AND is deliberately slow enough
   to sit far under any photosensitivity threshold. */
/* NO PADDING AND NO MEASURE OF ITS OWN. Every skeleton here renders inside
   `article.doc`, which already sets --measure and the 56/32 padding, and
   the first version set them again - so the placeholder sat ~56px lower than the
   prose that replaced it and the whole page jumped up on arrival. A skeleton that
   causes the layout shift it exists to prevent is worse than no skeleton. Caught on
   a screenshot; nothing in either gate could see it, which is now said out loud in
   docs/decisions/0017-the-region-contract.md. */
.sk{padding:0;margin:0}
/* NO OPACITY. `--sk` is computed to clear 3:1 against the reading surface, and the
   first version then drew it at opacity .34 - which composites #909090 over white
   to about #d6d6d6, roughly 1.45:1. The measurement was real and the paint threw it
   away. Contrast has to be computed over the surface actually being painted, which
   is the same trap the token comment warns about one file over. */
.skbar{display:block;background:var(--sk);border-radius:var(--r-xs);margin:0 0 9px}
@media (prefers-contrast:more){.skbar{background:var(--sk-strong)}}
.skpara{display:block;margin:0 0 26px}
.skrow{display:block;margin:0 0 26px}
.skhead{display:block;margin:0 0 34px}
.sk[data-sk="prose"] .skbar{border-radius:3px}
/* STATIC, and that is the shipped industry default rather than a shortcut: GitHub,
   Stripe and gov.uk all draw a flat placeholder with animationName `none`. A shimmer
   has no measured perceived-speed benefit to trade against the flash-threshold
   question it opens, and any opacity pulse would walk back through the floor above. */

/* The words that go with the shape. A 3:1 grey rectangle is a non-text contrast
   pass and nothing more; this is the part a screen reader and a low-vision reader
   actually get, and it is the reason the skeletons above are aria-hidden. */
.loadnote{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* Determinate progress - permitted ONLY where a real numerator and a real
   denominator exist. The corpus sweep has both: n files checked of N published.
   Nothing else in this app may draw one. */
.dprog{display:block;width:100%;max-width:340px;height:6px;border-radius:999px;background:var(--wash);overflow:hidden;margin:10px 0 2px}
.dprog i{display:block;height:100%;background:var(--accent);border-radius:999px;transform-origin:0 50%;transform:scaleX(0);transition:transform 140ms linear}
/* An in-place affordance holds its own width, so the control it lives on cannot
   jump under the pointer that is still on it. */
.holdw{display:inline-block;text-align:center}
/* aria-disabled buys back focus but buys nothing visually - the native attribute was
   doing that for free. Say it in ink and in the cursor, and keep the contrast of the
   label above the 4.5:1 floor rather than greying it to the usual unreadable ghost. */
[aria-disabled="true"]{cursor:default;opacity:.72}
[aria-disabled="true"]:active{transform:none}
/* A refetch with results already on screen: the results stay, and the ONLY thing
   that changes is this hairline on the box's own edge. It occupies no layout, so
   it carries the hairline timing (250ms, no floor) rather than a skeleton's. */
.pal[data-refreshing]::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--accent);opacity:.5;animation:navslide 1.15s var(--ease) infinite;transform-origin:0 50%}
@media (prefers-reduced-motion:reduce){.pal[data-refreshing]::after{animation:none;transform:scaleX(.35)}}

/* outline rail placeholder */
aside.toc{border-left:1px solid var(--rule);overflow-y:auto;padding:24px 20px;font-size:12.5px;background:var(--paper)}
aside.toc .t{font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:12px;font-weight:500}
aside.toc .ph{color:var(--soft);font-size:12px;line-height:1.55;max-width:180px}

/* ⌘K stub (F3 wires the corpus index) */
.veil{position:fixed;inset:0;background:var(--veil);z-index:80;opacity:0;pointer-events:none;visibility:hidden;transition:opacity var(--dur-med) linear,visibility 0s linear var(--dur-med)}
.veil.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity var(--dur-med) linear,visibility 0s linear 0s}
.pal{position:fixed;left:50%;top:14vh;transform:translateX(-50%) translateY(-6px) scale(.985);width:min(600px,92vw);background:var(--card);border:1px solid var(--rule2);border-radius:var(--r-xl);box-shadow:var(--sh-pop);z-index:90;opacity:0;pointer-events:none;visibility:hidden;transition:opacity var(--dur-med) linear,transform var(--dur-med) var(--ease-exit),visibility 0s linear var(--dur-med);overflow:hidden}
.pal.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity var(--dur-med) linear,transform var(--dur-med) var(--ease),visibility 0s linear 0s;transform:translateX(-50%) translateY(0) scale(1)}
.pal input{all:unset;display:block;width:100%;padding:15px 19px;font-size:16px;color:var(--text);border-bottom:1px solid var(--rule);box-sizing:border-box}
.pal .stub{padding:26px;text-align:center;color:var(--muted);font-size:13px}
.pal .ft{border-top:1px solid var(--rule);padding:8px 15px;font-size:11.5px;color:var(--muted);display:flex;gap:16px}
.pal .results{list-style:none;margin:0;padding:6px;max-height:46vh;overflow-y:auto}
.pal .results:empty{display:none}
.pal .noresults{padding:18px;text-align:center;color:var(--muted);font-size:13px}
.pal .results a{display:block;padding:9px 13px;border-radius:var(--r-md);text-decoration:none;color:var(--text)}
.pal .results li.sel a,.pal .results a:hover{background:var(--wash)}
.pal .results .rt{font-size:14px;font-weight:600}
.pal .results .rx{font-size:12px;color:var(--soft);margin-top:2px;font-family:var(--font-ui)}
.pal .results .rg{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);float:right;margin-top:3px}
.pal .ft kbd{font-family:var(--font-mono);border:1px solid var(--rule);border-radius:var(--r-xs);padding:0 4px;font-size:10px}

/* .newbtn IS GONE (2026-09-17). It styled the app bar's New and Edit, and both moved
   to the admin bar above the header - New is not a destination the bar owes a reader,
   and a page's primary action belongs to the page. The 640px rule that hid it, and
   the three #editdoc rules that made Edit the quiet twin of New, went with it. */

/* user menu */
.avatar{width:28px;height:28px;border-radius:var(--r-circle);border:1px solid var(--rule2);background:var(--wash);color:var(--accent-ink);font-size:11.5px;font-weight:650;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:var(--sh);letter-spacing:.02em}
.avatar:hover{border-color:var(--accent);box-shadow:var(--sh-raise)}
.avatar:active{transform:translateY(.5px)}
.umenu{position:fixed;top:52px;right:14px;width:264px;background:var(--card);border:1px solid var(--rule2);border-radius:var(--r-xl);box-shadow:var(--sh-pop);z-index:90;opacity:0;pointer-events:none;transform:translateY(-5px) scale(.985);transform-origin:top right;visibility:hidden;transition:opacity var(--dur-med) linear,transform var(--dur-med) var(--ease-exit),visibility 0s linear var(--dur-med);overflow:hidden;font-size:13px}
.umenu.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity var(--dur-med) linear,transform var(--dur-med) var(--ease),visibility 0s linear 0s;transform:none}
.umenu .who{display:flex;gap:11px;align-items:center;padding:14px 16px;border-bottom:1px solid var(--rule)}
.umenu .who .avatar{width:34px;height:34px;font-size:12.5px;cursor:default}
.umenu .who .nm{font-weight:600;line-height:1.3}
.umenu .who .em{font-size:11.5px;color:var(--muted)}
.umenu .who .mock{margin-left:auto;font-family:var(--font-mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 35%,transparent);background:var(--warn-wash);border-radius:var(--r-xs);padding:1px 5px}
.umenu .row{display:flex;align-items:center;gap:10px;padding:9px 16px;color:var(--body);text-decoration:none;cursor:pointer}
.umenu .row:hover{background:var(--wash);color:var(--text)}
.umenu .row.switchable{cursor:pointer}
.umenu .row.switchable:hover{background:var(--wash)}
.umenu .row .r{margin-left:auto;font-family:var(--font-mono);font-size:10.5px;color:var(--muted)}
.umenu .sec{border-top:1px solid var(--rule);padding:10px 16px 12px}
.umenu .sec .t2{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:7px;font-weight:500}
.umenu .kv{display:flex;justify-content:space-between;font-size:11.5px;color:var(--soft);padding:2px 0}
.umenu .kv .v{font-family:var(--font-mono);font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}

/* phone drawer */
.drawer{position:fixed;top:0;bottom:0;left:0;width:min(320px,84vw);background:var(--paper);border-right:1px solid var(--rule2);z-index:95;transform:translateX(-103%);transition:transform var(--dur-med) var(--ease-exit);overflow-y:auto;padding:18px 0}
body.nav-open .drawer{transform:none;transition:transform var(--dur-med) var(--ease)}

@media (max-width:1200px){
  .shell{grid-template-columns:var(--w-tree-md) minmax(0,1fr)}
  aside.toc{display:none}
  .search{min-width:200px}
}
@media (max-width:768px){
  header{--h-header:50px;height:var(--h-header);padding:0 12px;gap:10px}
  .crumb,.search{display:none}
  .mob{display:inline-flex}
  .shell{grid-template-columns:minmax(0,1fr)}
  aside.tree{display:none}
  .empty{margin-top:12vh}
}

/* ── NEITHER ASIDE IS SHOWN ANYWHERE - UNTIL FURTHER NOTICE ──────────────────
   Natan, 2026-09-18: "just everywhere don't show them".

   IT IS TWO BARE RULES ON PURPOSE, and the first version of this was not - it was
   four view-scoped rules keyed on `.shell[data-view="..."]`, which produced a
   measured 166ms FLICKER on every load: `data-role` becomes `admin` at 63ms and
   unhides both asides under the role rules at the top of this file, and the shell
   does not carry `data-view` until the router writes it at 229ms. For those 166ms
   the page painted a 270px tree and a 240px rail and then took them away. Only a
   rule that needs NO attribute can be right at the first paint, because at the first
   paint there are no attributes. Same lesson as the theme: the fix for a flash is
   never a faster correction, it is not needing one.

   The grid line is not decoration either - hiding an aside does not collapse the
   column it sat in, so without it the base three-column grid (and the two-column one
   at <=1200px) left a 270px empty gutter. It sits here, after every breakpoint block,
   because it has the same specificity as those and source order is what decides;
   `.shell[data-view="admin"]` and `[data-view="home"]` are (0,2,0) and still win, so
   the admin sidebar is untouched.

   THIS IS A WITHDRAWAL, NOT A DELETION. Both asides stay in the markup, still
   populated by the same code, and the region gate still finds them. To restore:
   delete this block, and put `furniture: tree, rail` back in the four view documents
   that record it in their `furniture-reason` (V02, V03, V04, V05). The base grid on
   `.shell` above is unchanged and comes back on its own. Two assertions in
   src/app/tests/verify-shell.mjs asserted the opposite until today and say so; invert
   those first. */
aside.tree,aside.toc{display:none}
.shell{grid-template-columns:minmax(0,1fr)}
/* THE LESSON THOSE RULES LEFT BEHIND, kept because the defect is easy to repeat: a
   class that sets display:inline-flex OUTRANKS the user-agent [hidden] rule, so an
   element with hidden=true stays on screen while every script reading `.hidden` is
   correctly told it is gone. Found on a screenshot, not in the code. `.ab-item[hidden]`
   and `.ab-chip[hidden]` above are that line, paid once for the admin bar. */


/* The machine mark (S06 R-543): a quiet label on every block a human did not
   write, in the Room and at the Desk alike. Tokens only, and it does not move. */
.mmark{display:inline-block;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-ink);background:var(--wash);border:1px solid var(--accent);border-radius:var(--r-sm);padding:1px 6px;margin-left:6px;vertical-align:middle;line-height:1.5}
.prose .markline{margin:-8px 0 14px;font-size:0}
.prose .callout{border:1px solid var(--rule);border-left:2px solid var(--accent);border-radius:var(--r-sm);background:var(--wash);padding:10px 14px;margin:18px 0}
.prose .callout p{margin:0}
.prose .embed-facade{border:1px dashed var(--rule2);border-radius:var(--r-sm);padding:12px 14px;margin:18px 0;color:var(--body);font-size:13.5px}
.prose .embed-kind{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-right:8px}
.prose figure{margin:22px 0}
.prose figure img{display:block;max-width:100%;height:auto;background:var(--wash);background-size:cover;border-radius:var(--r-sm)}
.prose figcaption{font-size:12.5px;color:var(--soft);margin-top:6px}
.prose details{border:1px solid var(--rule);border-radius:var(--r-sm);padding:8px 14px;margin:14px 0}
.prose summary{cursor:pointer;color:var(--accent-ink)}
.prose pre.hatch{border-left:2px solid var(--rule2)}
.pc-runs code{margin-right:4px}
