/* 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 IS STANDALONE, and for the same reason, one view later (2026-08-23).
   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. Both are answers to questions nobody has
   while writing.
   Not scoped to a breakpoint, unlike the Lobby rule above: below 781px the phone
   rules already collapse both, and the Desk refuses to render at all under 780px,
   so there is no narrow case for this to fight over. */
.shell[data-view="write"]{grid-template-columns:minmax(0,1fr)}
.shell[data-view="write"] aside.tree,
.shell[data-view="write"] aside.toc{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);
  }
}
.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)}
.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:56px;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)}
.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)}
.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{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 13px;font-size:12.5px;font-weight:550;font-family:var(--font-ui);color:#fff;background:var(--accent);border:none;border-radius:var(--r-md);cursor:pointer;box-shadow:var(--sh)}
.newbtn:hover{background:var(--accent-ink);box-shadow:var(--sh-raise)}
.newbtn:active{transform:translateY(.5px);box-shadow:var(--sh)}
[data-mode="dark"] .newbtn{color:var(--paper)}

/* 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{height:50px;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}
}
/* The editor's New button pushed the avatar past a 390px viewport (stranger finding
   9). Placed at the end of the sheet: the earlier attempt sat above .newbtn's own
   display rule and lost the cascade at equal specificity - found by measuring the
   viewport, not by reading the CSS. */
@media (max-width:640px){.newbtn{display:none}}
/* Edit is the same control as New and must not compete with it: New starts something,
   Edit changes something already published, and only one of them is ever the reason
   a writer came to the page. Same geometry, quiet fill. */
#editdoc{background:var(--card);color:var(--accent-ink);border:1px solid var(--rule)}
/* `.newbtn` sets display:inline-flex, which outranks the user-agent [hidden] rule, so
   the button stayed on screen on every view while its `hidden` property read true.
   The keyboard path checked `hidden` and was correct; the pixels were not. Found by
   asserting both, not by looking - the screenshot showed a button that JS said was
   hidden. Anything that hides by attribute in this sheet needs this line. */
#editdoc[hidden]{display:none !important}
#editdoc:hover{border-color:var(--accent);background:var(--wash)}

