/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE CIRCLE HUB STYLESHEET — ONE COPY, TWO DELIVERY PATHS
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   Every surface that draws the orb hub reads THIS file and only this file:

     - the React app chrome  → `@import "../styles/hub.css"` at the top of src/app/globals.css
     - every vanilla surface → <link rel="stylesheet" href="/hub.css">, served from this same file
                               on disk by src/app/hub.css/route.ts

   That is the whole point. The hub previously existed as three hand-maintained copies (React,
   public/orb/orb.js, the inline IIFE in demos/book-reader/reader.html) and they drifted, which is
   how the production reader ended up showing a design nobody had shipped for weeks. Two renderers
   are a fact of life here (React needs server actions for invite accept/decline; the reader is a
   verbatim HTML document with no build step) — but two STYLESHEETS were never a fact of life, only
   a copy nobody deleted. If a rule below needs to change, it changes once.

   NOTHING WAS COPIED HERE. These rules were CUT out of globals.css, comments and all, and the
   places they left carry a breadcrumb pointing at this file.

   ── THE TOKEN PREAMBLE ─────────────────────────────────────────────────────────────────────────
   The app defines its design tokens on :root. The reader and /z do not: they carry a DIFFERENT,
   much smaller vocabulary (--fd/--ui/--oats/--sh), so a rule below asking for var(--font-mono) or
   var(--sp-md) over there resolves to nothing — fallback fonts and collapsed spacing, which is
   exactly the "it looks broken outside the app" failure this file exists to end.

   So the tokens are re-declared HERE, scoped to the three roots the hub can be mounted under.
   SCOPED, not :root, on purpose: this stylesheet is loaded into documents that own their own
   palette (the immersive dark reader), and a :root block would reach out of the hub and repaint
   them. Custom properties inherit, so declaring them on the root of each hub subtree covers every
   descendant rule in this file.

   The values are copied verbatim from globals.css :root — that file remains the source of truth for
   the brand. If a token changes there, change it here too (the pair is small and deliberate; the
   alternative is shipping the whole app palette to a reader that needs nine of them). */
.orb-panel, .chal-list, .navmenu, .navmenu-sheet, .msrow {
  /* .navmenu-sheet and .msrow are listed ALONGSIDE .navmenu, not merely under it, because the reader
     renders the sheet OUTSIDE any .navmenu wrapper: #menuSheet is a sibling of the tool row (its chip
     is .tool:nth-child(4), and wrapping that would break the wabi-sabi rotation chain). A sheet that
     inherited nothing would draw in fallback fonts on the one surface this design came from. */

  /* Brand palette (canonical hex, digital/RGB) */
  --color-grass-field: #1cab5b;
  --color-oats: #fef6e4;
  --color-sambas: #414d57;
  --color-colorado: #f75458;
  --color-lazuli: #2496c9;
  --color-school-bus: #ecaf21;
  --color-happy: #f1dd01;

  /* Semantic */
  --bg: var(--color-oats);
  --paper: #fffdf6;            /* a touch lighter than Oats — printed leaf */
  --oats: var(--color-oats);   /* cream alias (matches --ink/--coral/--brand); several rules use var(--oats) */
  --ink: var(--color-sambas);  /* primary text + borders */
  --text: var(--color-sambas);
  --text-muted: rgba(65, 77, 87, 0.75); /* muted = Sambas INK faded (never an off-palette grey) */
  --graphite: #5f6873;         /* PENCIL — "a person wrote this and could rub it out". Opaque + AA; never disabled state. */
  --brand: var(--color-grass-field);
  --grass-deep: #0c6e39;       /* PRESSED grass, for cream text on a green fill. --brand #1cab5b is a
                                  FIELD colour: cream on it is 2.78:1 and fails outright, which is how
                                  the Circle-Time takeover button shipped illegible (it asked for an
                                  undefined `var(--grass)`, got nothing, and put cream on cream). This
                                  is 5.91:1 and is the same green the reader's own join button already
                                  used privately — now one token, so the button cannot differ by surface. */

  --brand-press: #14803f;      /* the same hue laid down heavier — oats-on-Grass at label size is under AA */
  --danger: var(--color-colorado);
  --info: var(--color-lazuli);
  --coral: var(--color-colorado); /* brand "Colorado" red — alias kept; older rules say --coral */
  --tape-lazuli: #1a7099;      /* the sanctioned cool Dymo roll (resource links) */
  --tape-lazuli-ink: #f2fbff;

  /* Status semantics — ONE meaning per colour, every surface. HAPPY is reserved, exclusively, for
     "Circle Time is live AND ≥1 person is here"; SCHOOL BUS marks "the door's open". See DESIGN.md. */
  --sig-live: var(--color-happy);
  --sig-open: var(--color-colorado);
  --sig-makers: var(--color-grass-field);
  --sig-developing: var(--color-lazuli);
  --sig-archive: #9b8a63;
  /* the .is-live halo, hoisted with the rule below it (globals.css :root, line 66) */
  --live-glow: 0 0 0 2px var(--bg), 0 0 16px 2px rgba(241,221,1, 0.6);

  /* Spacing (8px base) */
  --sp-2xs: 2px; --sp-xs: 4px; --sp-sm: 8px; --sp-md: 16px;
  --sp-lg: 24px; --sp-xl: 32px; --sp-2xl: 48px; --sp-3xl: 64px;

  /* Radius — paper is barely rounded; only stickers/pills curve hard. */
  --r-sm: 3px; --r-md: 4px; --r-lg: 6px; --r-full: 9999px;

  /* HARD registration shadows — solid ink offset, never blurred, never gradient. */
  --shadow-press: 3px 3px 0 var(--ink);
  --shadow-sheet: 6px 7px 0 var(--ink);
  --shadow-sheet-hi: 9px 11px 0 var(--ink);
  --shadow-sheet-nested: 3px 4px 0 var(--ink);
  --shadow-lift: 4px 5px 0 rgba(26, 14, 4, 0.42);
  /* paper RESTING/pinned on the cork wall → a deeper warm HARD offset */
  --shadow-cork-rest: 5px 7px 0 rgba(26, 14, 4, 0.5);
  /* domed thumbtack: specular inset + inset core for the dome, plus a short HARD cast offset so the
     pin reads as pushed THROUGH the sheet. No gaussian anywhere. */
  --pin-shadow: 0 5px 7px rgba(0,0,0,0.45), inset -1px -2px 3px rgba(0,0,0,0.34), inset 1px 1px 2px rgba(255,255,255,0.55), 2px 3px 0 rgba(26,14,4,0.30);

  /* Motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-short: 140ms;

  /* Type — see DESIGN.md typography. Licensed brand face first, Google-Fonts substitute next. */
  --font-display: "Pitch", "Courier Prime", "Courier New", monospace; /* Display — Pitch Semibold Italic */
  --font-mono: "Pitch Sans", "DM Mono", "Courier Prime", monospace;   /* Headings/subheads — Pitch Sans */
  --font-body: "Bitter", Georgia, serif;                              /* Body/intro — Bitter Light 300 */
  --font-hand: "Birdie", "Patrick Hand", "Bradley Hand", cursive;     /* Accent/technical — Birdie */

  /* THE READER'S OWN TWO-LETTER TOKENS. The menu rules below were cut out of reader.html, where the
     document's :root defines --ui and --sh-sm (reader.html:49,53). In the APP no such token exists, so
     the rules would have resolved to a fallback font and no shadow — the exact "looks broken outside
     the reader" failure this preamble exists to prevent, just pointing the other way. Values verbatim
     from reader.html; --ui is deliberately NOT aliased to --font-mono, which leads with Pitch Sans and
     would silently restyle the reader's menu the first time this file loaded there. */
  --ui: "DM Mono", ui-monospace, monospace;
  --sh-sm: 4px 4px 0 0 var(--ink);
}
/* The HUD mounts its cards inside the panel, so .chal-list would inherit the panel's copy anyway —
   it is listed above for the bulletin board, where .chal-list-board is the outermost hub element. */

/* ── The hub's desktop anchor above the bottom-centre docked orb (+ its pop keyframe)  [was globals.css:309–324] ─────────────────────────── */
/* Desktop: anchor the hub above the bottom-centre orb. GUARDED to ≥561px so it can't fight the mobile
   bottom-sheet rule (@media max-width:560). Without the guard, this rule's higher specificity leaked
   its left:50%/transform/top:auto onto the narrow-width sheet and shoved the 340px panel off-screen
   into an illegible sliver. Below 561px the clean bottom-sheet rule now stands alone. */
@media (min-width: 561px) {
  .hud-orb .orb-panel {
    top: auto; bottom: calc(100% + 12px); right: auto; left: 50%; transform: translateX(-50%);
    animation: orbpop-up 0.16s var(--ease-out);
  }
  .hud-orb .orb-panel::before { /* beak now points DOWN at the orb */
    top: auto; bottom: -7px; right: auto; left: 50%; margin-left: -6px;
    border-left: none; border-top: none;
    border-right: 2.5px solid var(--ink); border-bottom: 2.5px solid var(--ink);
  }
}
@keyframes orbpop-up { from { opacity: 0; transform: translate(-50%, 6px); } to { opacity: 1; transform: translate(-50%, 0); } }

/* ── Cards — torn cut-paper sheets  [was globals.css:350–377] ─────────────────────────── */
/* ---- Cards: REAL torn cut-paper. The border + ruled fill + offset shadow live on a ::before
   "sheet" layer that's run through the #torn displacement filter, so the edges are genuinely
   ragged. Content sits above it (z-index 1) and stays crisp — the two-layer trick from the
   landing. No markup change needed anywhere .card is used. ---- */
.card {
  position: relative; color: var(--text);
  background: transparent; border: 0;
  padding: var(--sp-lg);
  transform: rotate(-0.5deg);
  transition: transform var(--dur-short) var(--ease-out);
}
.card::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: var(--paper) repeating-linear-gradient(180deg, transparent 0 27px, rgba(65, 77, 87, 0.05) 27px 28px);
  background-size: 100% 28px;
  border: 2.6px solid var(--ink); border-radius: var(--r-lg);
  filter: url(#torn);
  box-shadow: var(--shadow-sheet);
  transition: box-shadow var(--dur-short) var(--ease-out);
}
.card > * { position: relative; z-index: 1; }
.card > :last-child { margin-bottom: 0; }
.card.straight { transform: none; }
a.card, .card.hoverable { cursor: pointer; }
a.card:hover, .card.hoverable:hover { transform: rotate(0deg) translate(-1px, -2px); text-decoration: none; }
a.card:hover::before, .card.hoverable:hover::before { box-shadow: var(--shadow-sheet-hi); }
a.card:active, .card.hoverable:active { transform: translate(2px, 2px); }
a.card:active::before, .card.hoverable:active::before { box-shadow: 2px 2px 0 var(--ink); }

/* ── Buttons — rubber stamps  [was globals.css:410–433] ─────────────────────────── */
/* ---- Buttons: stuck-on rubber-stamp labels, press-down ---- */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-sm);
  min-height: 44px;
  font-family: var(--font-mono); font-weight: 500; font-size: 0.82rem;
  text-transform: uppercase; letter-spacing: 0.06em;
  border: 2.5px solid var(--ink); border-radius: var(--r-md);
  padding: var(--sp-sm) var(--sp-lg); cursor: pointer;
  background: var(--brand); color: var(--color-oats);
  box-shadow: var(--shadow-press);
  transform: rotate(-1deg);
  transition: transform 80ms var(--ease-out), box-shadow 80ms var(--ease-out), filter var(--dur-short);
}
.btn:hover { text-decoration: none; filter: brightness(1.05); transform: rotate(0deg) translate(-1px, -1px); box-shadow: 4px 4px 0 var(--ink); }
.btn:active { transform: translate(3px, 3px); box-shadow: 0 0 0 var(--ink); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; transform: rotate(-1deg); filter: grayscale(0.3); }
/* ActionBtn while its form action is pending (also disabled) — the plate presses IN like :active and
   dims to read as "working…", not clickable. Hard-edged (no soft blur). Placed after :disabled so it
   wins on equal specificity and the pressed look beats the disabled tilt. */
.btn[aria-busy="true"] { cursor: progress; opacity: 0.62; filter: grayscale(0.25); transform: translate(3px, 3px); box-shadow: 0 0 0 var(--ink); }
.btn-ghost { background: var(--paper); color: var(--ink); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-ct { background: var(--color-school-bus); color: var(--ink); }  /* Circle Time CTA — School Bus amber fill ("come in, the door's open"), dark ink text (amber-on-light needs dark text). NOT Happy: Happy is the reserved live beacon only. */

/* ── Scrap — the torn status chip  [was globals.css:726–742] ─────────────────────────── */
/* Torn green construction-paper SCRAP — the business-card signature. White mono caps on a hand-torn
   grass scrap (crisp text above a #torn'd ::before). The role/status label motif. Color variants. */
.scrap {
  position: relative; display: inline-block; z-index: 0;
  padding: 0.5em 0.95em;
  font-family: var(--font-mono); font-weight: 500; font-size: 0.72rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: #fff; line-height: 1.15;
  transform: rotate(-2deg);
}
.scrap::before {
  content: ""; position: absolute; inset: -3px -2px; z-index: -1;
  background: var(--brand); filter: url(#torn); box-shadow: 2px 3px 0 rgba(65, 77, 87, 0.2);
}
.scrap-coral::before { background: var(--color-colorado); }
.scrap-bus::before { background: var(--color-school-bus); }
.scrap-lazuli::before { background: var(--color-lazuli); }
.scrap-ink::before { background: var(--ink); }

/* ══ THE HAMBURGER — chip, sheet and rows, for every surface ═════════════════════════════════════
   [chip/sheet were globals.css:2251–2322; the sheet + row rules below were CUT from
    demos/book-reader/reader.html:255–273 and :469–470, comments and all]

   THE ROWS WERE UNIFIED FIRST (src/app/nav/NavMenuView.tsx), AND THAT LEFT THE MENU HALF-DONE: one
   list of options rendered by one component, drawn in two completely different costumes — the reader's
   folded cream sheet with hand-drawn glyphs, and a white text-only dropdown in the app chrome. Same
   words, two designs, so "the menu" was still two things to anyone using the product rather than
   reading the source. The founder picked the reader's, so the reader's rules moved HERE, which is the
   move this file was created to make (see the header): one copy, two delivery paths.

   reader.html now carries NO menu CSS. It links /hub.css already (for the orb panel) and its #menuSheet
   wears .navmenu-sheet.navmenu-sheet--fixed. The standalone file:// demo therefore draws an unstyled
   sheet, exactly as it already draws an unstyled .orb-panel — a trade this file's header made and
   documented long before the menu joined it. */
.navmenu { position: relative; }
/* ☰ CHIP — the reader's 46px cream tool stamp (reader.html's .tool), defined here in full so that a
   surface with no chip row of its own still draws a real hamburger. The ledger and the diary reader are
   exactly that case: they ship .stamp and .brandMark and no .tool at all, so a chip that inherited its
   costume from "whatever tool row it is standing in" would have been an unstyled naked <button> on two
   of the five surfaces this change exists to reach.

   Every host ALSO renders `class="tool navmenu-btn"`, and that is not belt-and-braces. Where a tool row
   does exist, its rules are loaded after this file (globals.css @imports hub.css at the top; the vanilla
   surfaces link it above their inline <style>) at equal specificity, so the row wins — the hamburger
   ends up matching the chips it is standing beside rather than being the one that is subtly wrong.
   Default here, deferring to the neighbours there, and no surface left without a chip.

   (It is a plain <button>, never <details>/<summary> — a restyled <summary> double-fires its native
   toggle on iOS WebKit, which flickered the menu open→closed and left the chip stuck pressed.) */
.navmenu-btn {
  appearance: none; -webkit-appearance: none; cursor: pointer; position: relative;
  width: 46px; height: 46px; min-width: 44px; min-height: 44px; padding: 0;
  display: grid; place-items: center; -webkit-tap-highlight-color: transparent;
  background: var(--oats); color: var(--ink);
  border: 2.2px solid var(--ink); border-radius: 6px; box-shadow: var(--sh-sm); box-sizing: border-box;
  transform: rotate(var(--tr, 3deg)) translateY(var(--ty, -1px));
  transition: transform .12s ease, box-shadow .09s ease;
}
.navmenu-btn::before {
  content: ""; position: absolute; inset: -2px; z-index: -1;
  background: var(--oats); filter: url(#roughsm); border-radius: 7px;
}
.navmenu-btn svg { width: 23px; height: 23px; pointer-events: none; }
@media (hover: hover) and (pointer: fine) { .navmenu-btn:hover { transform: rotate(0); } }
.navmenu-btn:focus-visible { outline: 3px solid var(--color-lazuli); outline-offset: 2px; }
/* Pressed look = momentary tap OR the sheet actually being open, so the chip stays visibly held down
   for as long as the menu is up. Keyed off aria-expanded as well as [data-open] so it is true on the
   vanilla surfaces too, and never off :hover, so iOS can't leave it stuck after a tap. */
.navmenu-btn:active,
.navmenu[data-open] .navmenu-btn,
.navmenu-btn[aria-expanded="true"] { transform: rotate(0) translate(4px, 4px); box-shadow: 0 0 0 0 var(--ink); }
/* Logged-out: the chip grows a label, because "Sign in" is the one thing a stranger must be able to
   read without opening anything. The ::before torn edge takes the coral fill too, or the rough halo
   paints a cream shadow around a red chip.
   COMPOUND SELECTORS, NOT `.navmenu-btn-cta` ALONE, and this is load-bearing: the chip also carries
   `.tool`, whose rules are declared LATER in every host (globals.css @imports this file at its top;
   the vanilla surfaces link it above their inline <style>). At equal single-class specificity `.tool`
   therefore wins — and it sets `width:46px`, cream background and ink text, which is exactly right for
   a square icon chip and exactly wrong for this one. Deferring to the row is the intent for the SHAPE
   of the button (see the chip note above); it was never the intent for the one variant whose whole job
   is to look different. `.tool.navmenu-btn-cta` is (0,2,0) and settles it in both directions. */
.navmenu-btn-cta,
.tool.navmenu-btn-cta { width: auto; padding: 0 13px; gap: 7px; grid-auto-flow: column;
  background: var(--color-colorado, #f75458); color: #fff; }
.navmenu-btn-cta::before,
.tool.navmenu-btn-cta::before { background: var(--color-colorado, #f75458); }
.navmenu-cta-txt { font-family: var(--ui); font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; }
.navmenu-handle { font-weight: 500; }
.navmenu-tag { font-size: 0.6rem; opacity: 0.7; }
/* the sheet's identity header — avatar + @handle + level. It was the app's alone; the reader's ☰ now
   renders it too, so the top of the menu answers "who am I signed in as" on every surface. */
.navmenu-head { display: flex; align-items: center; gap: 10px; padding: 12px 15px; margin: -6px -6px 1px; border-bottom: 2px dashed rgba(65,77,87,.18); text-decoration: none; color: var(--ink); }
.navmenu-head:hover { background: rgba(65,77,87,.05); }
.navmenu-head-txt { display: flex; flex-direction: column; line-height: 1.2; }
.navmenu-head-txt strong { font-family: var(--ui); font-size: .8rem; }
.navmenu-head-lv { font-family: var(--ui); font-size: .58rem; opacity: .6; }

/* THE SHEET — a folded cream card, torn at the edge and pinned slightly askew. Cut verbatim from
   reader.html's #menuSheet (:256–261). Two things were kept from the app's version rather than the
   reader's, both of them fixes the reader never had:
     · position:absolute by DEFAULT (the reader opts into fixed below), so a sheet in a scrolling
       masthead travels with its chip;
     · max-height + overflow-y, because on a short landscape phone the last row (Sign out) sat below
       the fold of a sheet hanging off a fixed header and could not be reached at all. */
.navmenu-sheet {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 50;
  display: none; flex-direction: column; gap: 1px; min-width: 198px; padding: 6px;
  background: var(--oats); border: 2.2px solid var(--ink); border-radius: 9px;
  box-shadow: 5px 6px 0 rgba(65,77,87,.9);
  transform: rotate(-1deg) translateY(-7px); opacity: 0;
  transition: opacity .14s ease, transform .14s ease;
}
/* THE SCROLL CAP IS SCOPED TO SHORT VIEWPORTS, and that is not timidity — the two fixes are in direct
   conflict and each only matters in one place.
   · The cap exists because on a short landscape phone the sheet hangs off a fixed header, page scroll
     can never reach its bottom, and the last row (Sign out) is simply unreachable.
   · `overflow` on the sheet clips its own ::before, which sits at inset:-2px OUTSIDE the padding box.
     That pseudo-element is the torn paper edge — the thing that makes this a folded card rather than a
     rounded rectangle, and a hard DESIGN.md rule. An unconditional cap silently traded the craft on
     every screen for reachability on the few that need it.
   So: full torn edge everywhere the sheet fits, scrolling where it cannot.

   THE BREAKPOINT IS SIZED FOR THE TALLEST MENU, WHICH IS THE READER'S. Count it there, not in the app
   chrome: the reader's ☰ carries three device rows (Fullscreen / Sound / Install) ABOVE the account
   rows, so a signed-in admin editor sees 3 device + 8 account + the identity head. At 46px a row and
   ~58px for the head that is ~564px of content in 12px of padding, and the sheet starts 66px down —
   so it needs ~700px of viewport to fit whole. The old 620px was measured against the app's shorter
   menu and left a band (roughly 621–677dvh: short desktop windows, iPad Stage Manager) where the sheet
   overflowed the screen AND the cap had not engaged, which puts Sign out somewhere you cannot reach.
   Adding the "Coming up" row widened that band, which is what surfaced it.
   If a row is ever added again, move this number with it. */
@media (max-height: 700px) {
  .navmenu-sheet {
    max-height: calc(100dvh - 90px); overflow-y: auto;
    /* overflow-x MUST be explicit: setting only overflow-y makes the browser compute overflow-x to auto
       too (CSS spec), and the tilted rows then spawn a spurious horizontal scrollbar. */
    overflow-x: hidden; scrollbar-width: none;
  }
  .navmenu-sheet::-webkit-scrollbar { display: none; }
}
/* TWO show-states, both shipping, and that is deliberate rather than untidy: the app toggles
   [data-open] on the wrapper (NavMenuShell owns React state), while the reader's own vanilla
   openMenu()/closeMenu() add .show to the sheet itself and predate all of this. Honouring both means
   neither host had to adopt the other's control flow to share a costume. */
.navmenu-sheet.show,
.navmenu[data-open] .navmenu-sheet { display: flex; opacity: 1; transform: rotate(-1deg) translateY(0); }
.navmenu-sheet::before {
  content: ""; position: absolute; inset: -2px; z-index: -1;
  background: var(--oats); filter: url(#roughsm); border-radius: 11px;
}
/* The reader (and the other vanilla surfaces) hang the sheet off a position:fixed tool row, clear of
   the safe-area insets — it is not positioned relative to its chip at all. */
.navmenu-sheet--fixed {
  position: fixed; right: max(12px, env(safe-area-inset-right));
  top: max(66px, calc(env(safe-area-inset-top) + 56px)); z-index: 40;
}

/* THE ROWS. `.msrow` won the name over `.navmenu-link` for one blunt reason: reader.html hardcodes it
   on the three device rows (Fullscreen / Sound / Install), and renaming those means editing a verbatim
   3300-line template where a slip ships a blank homepage. The app's `.navmenu-link` had no such
   hostage and is gone. */
/* [hidden] MUST beat .msrow's display:flex — an author rule outranks the UA sheet, so without this the
   install row ships VISIBLE and, when install is impossible (demo build, already installed, a browser we
   can say nothing useful to), it is a menu row that does nothing. Same gotcha, same fix as z/[id]/render.ts. */
.msrow[hidden] { display: none; }
.msrow {
  display: flex; align-items: center; gap: 11px; width: 100%; min-height: 46px; box-sizing: border-box;
  padding: 9px 12px; background: transparent; border: 0; border-radius: 6px; cursor: pointer;
  color: var(--ink); text-decoration: none;
  font-family: var(--ui); font-weight: 500; font-size: .84rem; letter-spacing: .01em; text-align: left;
}
.msrow:hover { background: rgba(65,77,87,.09); }
.msrow:focus-visible { outline: 3px solid var(--color-lazuli); outline-offset: -2px; }
.msrow svg { width: 21px; height: 21px; flex: 0 0 auto; pointer-events: none; }
.msrow.on { color: var(--coral); }            /* fullscreen active = the same "live tool" coral as .tool.on */
.msrow.off svg { opacity: .36; }              /* sound muted = dimmed speaker (mirrors the old .tool.off) */
.msrow .mssub { margin-left: auto; font-family: var(--ui); font-size: .6rem; text-transform: uppercase; letter-spacing: .11em; opacity: .55; }
.msrow .msname { margin-left: auto; max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .65; font-size: .74rem; }

/* The signed-out sheet's single row. `:hover` is spelled out because `.msrow:hover` above is (0,2,0)
   and would otherwise repaint this row's background with the generic ink tint — leaving the row's
   white text on near-cream, which is not a weaker hover, it is unreadable. */
.navmenu-cta,
.navmenu-cta:hover { background: var(--color-colorado, #f75458); color: #fff; font-weight: 700; }
.navmenu-cta:hover { filter: brightness(0.95); }
.navmenu-signout { color: var(--color-colorado, #f75458); }
@media (prefers-reduced-motion: reduce) {
  .navmenu-btn:active, .navmenu[data-open] .navmenu-btn { transform: none; }
  .navmenu-sheet { transition: none; }
}

/* ── The orb hub panel — popover on desktop, bottom sheet on mobile  [was globals.css:2489–2613] ─────────────────────────── */
/* The flyout hub — desktop popover hanging from the orb (beak), mobile bottom sheet. */
.orb-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 60; width: 340px; max-width: calc(100vw - 24px);
  background: var(--paper); border: 2.5px solid var(--ink); border-radius: var(--r-lg);
  box-shadow: 5px 6px 0 var(--ink), 0 2px 6px rgba(65,77,87,0.18);
  display: flex; flex-direction: column; max-height: min(70vh, 560px); overflow: hidden;
  animation: orbpop 0.16s var(--ease-out);
}
.orb-panel::before { /* the beak pointing up at the orb */
  content: ""; position: absolute; top: -7px; right: 12px; width: 12px; height: 12px;
  background: var(--paper); border-left: 2.5px solid var(--ink); border-top: 2.5px solid var(--ink);
  transform: rotate(45deg);
}
@keyframes orbpop { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .orb-panel { animation: none; } }
.orb-panel-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm);
  padding: 11px 14px; border-bottom: 2px solid rgba(65,77,87,0.18);
}
.orb-panel-close {
  display: none; /* desktop dismisses via outside-click/Esc; explicit close shows on mobile sheet */
  background: none; border: 0; color: var(--ink); font-size: 1rem; cursor: pointer; line-height: 1; padding: 4px;
}
/* overflow-x MUST be explicit: setting only overflow-y:auto makes the browser compute overflow-x to
   `auto` too (CSS spec), so a sub-pixel-wide child (a verb button / the resume band) spawns a spurious
   horizontal scrollbar — which then steals height and triggers a vertical one. Clip x; scroll y only. */
.orb-panel-body { overflow-y: auto; overflow-x: hidden; padding: 6px 8px 10px; scrollbar-width: none; }
/* Hide the scrollbar chrome — a visible bar reads as web-y clutter in the paper panel; content
   still scrolls. scrollbar-width:none (Firefox) + ::-webkit-scrollbar display:none (Chromium/Safari). */
.orb-panel-body::-webkit-scrollbar { display: none; }
.orb-group { margin-top: 6px; }
.orb-group:first-child { margin-top: 0; }
.orb-group-label {
  margin: 8px 8px 4px; font-family: var(--font-mono); font-weight: 600; font-size: 0.62rem;
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-muted);
}
.orb-rows { list-style: none; margin: 0; padding: 0; }
.orb-row { display: flex; align-items: stretch; gap: 4px; }
.orb-row-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 7px 10px; border-radius: var(--r-md); color: var(--ink); text-decoration: none;
}
.orb-row-main:hover, .orb-row-main:focus-visible { background: rgba(65,77,87,0.07); text-decoration: none; outline: none; }
.orb-row-main:focus-visible { box-shadow: inset 0 0 0 2px var(--color-lazuli); }
.orb-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--ink); }
.dot-live { background: var(--sig-live); box-shadow: 0 0 7px 1px rgba(241,221,1,0.6); }
.dot-door { background: var(--color-school-bus); }   /* Circle Time window open, nobody here yet — calm School Bus, no glow (the door's open) */
.dot-making { background: var(--sig-makers); }
.dot-issue { background: var(--sig-open); }
.dot-invite { background: var(--color-lazuli); }   /* a personal summons waiting on your answer — lazuli, no other signal's colour */
.orb-row-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.orb-row-title { font-family: var(--font-mono); font-weight: 500; font-size: 0.82rem; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.orb-row-sub { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.orb-faces { flex: 0 0 auto; display: inline-flex; align-items: center; }
.orb-faces .orb-face { width: 22px; height: 22px; border-radius: 50%; border: 1.5px solid var(--paper); margin-left: -6px; object-fit: cover; box-shadow: 0 0 0 1px rgba(65,77,87,0.25); }
.orb-faces .orb-face:first-child { margin-left: 0; }
.orb-faces .orb-face-g { box-shadow: 0 0 0 1.5px var(--sig-makers); }   /* makers faces ringed green */
.orb-face-more { margin-left: 4px; font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-muted); }
/* per-row verb buttons — one door per row (Pick up / Join / Sit down / Open) */
.orb-verb {
  flex: 0 0 auto; align-self: center; margin-right: 6px; font-family: var(--font-mono); font-weight: 600;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink); text-decoration: none;
  border: 2px solid var(--ink); border-radius: var(--r-md); padding: 6px 9px; box-shadow: 1.5px 1.5px 0 var(--ink); white-space: nowrap;
}
.orb-verb.join { background: var(--sig-live); }
.orb-verb.door { background: var(--color-school-bus); }   /* be the first into Circle Time — School Bus "come in" */
.orb-verb.sit { background: var(--sig-makers); color: var(--color-oats); }
.orb-verb.open { background: var(--paper); }
.orb-verb.pick { background: var(--color-school-bus); }
.orb-verb.accept { background: var(--color-lazuli); color: var(--color-oats); }
/* decline is deliberately quiet next to accept — same hit target, ghost weight */
.orb-verb.decline { background: transparent; border-color: rgba(65,77,87,0.45); box-shadow: none; color: var(--text-muted); }
.orb-verb.decline:active { transform: none; }
.orb-verb:hover { text-decoration: none; filter: brightness(1.04); }
.orb-verb:active { transform: translate(1.5px, 1.5px); box-shadow: 0 0 0 var(--ink); }
.orb-verb:focus-visible { outline: 3px solid var(--color-lazuli); outline-offset: 2px; }
/* "where you left off" resume band — your own bench, pinned on top */
.orb-resume {
  display: flex; align-items: center; gap: 10px; margin: 4px 6px 6px; padding: 9px 11px; border-radius: var(--r-md);
  color: var(--ink); text-decoration: none; border: 2px solid var(--ink); box-shadow: 2px 2px 0 var(--ink);
  background: repeating-linear-gradient(135deg, rgba(236,175,33,0.13) 0 9px, rgba(236,175,33,0.04) 9px 18px);
}
.orb-resume:hover { text-decoration: none; filter: brightness(1.02); }
.orb-resume:focus-visible { outline: 3px solid var(--color-lazuli); outline-offset: 2px; }
.orb-resume-k { font-family: var(--font-mono); font-size: 0.54rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
.orb-resume .orb-row-title { font-weight: 600; }
.orb-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px; padding: 22px 16px 18px; }
.orb-empty-mark { width: 26px; height: 26px; border-radius: 50%; border: 2.4px solid var(--text-muted); opacity: 0.5; }
.orb-empty-line { margin: 0; font-family: var(--font-body); font-size: 0.92rem; color: var(--ink); }
/* the upcoming Circle Time's viewer-local time under "Next up:" — calm/secondary, never a live signal */
.orb-empty-next { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }
.orb-empty-cta { font-family: var(--font-mono); font-size: 0.72rem; color: var(--brand); text-decoration: none; }
.orb-empty-cta:hover { text-decoration: underline; }
/* ── The lower "Notifications" split (below the presence/status groups) ── */
/* a scissors "cut here" divider separating presence (above) from notifications (below) */
/* "See all →" — quiet link to the full /notifications page so nothing's missed */
@media (max-width: 560px) {
  .orb-panel {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0; width: 100%; max-width: 100%;
    border-radius: var(--r-lg) var(--r-lg) 0 0; max-height: 78vh; box-shadow: 0 -4px 0 var(--ink), 0 -2px 10px rgba(65,77,87,0.25);
    animation: orbsheet 0.2s var(--ease-out);
  }
  .orb-panel::before { display: none; }
  .orb-panel-close { display: inline-grid; place-items: center; }
}
@keyframes orbsheet { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (max-width: 560px) and (prefers-reduced-motion: reduce) { .orb-panel { animation: none; } }

/* ── Pushpin — the domed thumbtack + its colour vocabulary  [was globals.css:2907–2932] ─────────────────────────── */
/* PUSHPIN — domed thumbtack, consistent across the board */
.pin {
  position: absolute; z-index: 7;
  top: -11px; left: 50%; transform: translateX(-50%);
  width: 19px; height: 19px; pointer-events: none;
}
.pin::before {
  content: ""; position: absolute; top: 11px; left: 52%;
  width: 2px; height: 10px; border-radius: 1px;
  background: rgba(40, 22, 6, 0.45);
  transform: translateX(-50%) rotate(9deg);
}
.pin::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--pin, var(--ink));
  border: 2px solid rgba(0, 0, 0, 0.26);
  box-shadow: inset 4px 4px 0 rgba(255, 255, 255, 0.42), 1px 2px 3px rgba(0, 0, 0, 0.3);
}
.pin-coral { --pin: var(--color-colorado); }
.pin-grass { --pin: var(--color-grass-field); }
.pin-lazuli { --pin: var(--color-lazuli); }
/* Sambas ink as a tack — the settled/resting pin, and the third BRAND colour available here (grass
   and both yellows stay reserved, so coral + lazuli + sambas is the whole legal palette). Already
   precedent: ShelfBook's seeded PINS array carries the same #414D57. */
.pin-sambas { --pin: var(--color-sambas); }
.pin-bus { --pin: var(--color-school-bus); }

/* ── Pinup — a thumbtack on any sheet  [was globals.css:3208–3222] ─────────────────────────── */
/* A pinned sheet: a thumbtack coin on any surface (the .card torn sheet lives on
   ::before, so the pin takes ::after — no markup change beyond the class). The pin
   is static (no transform animation), so it needs no reduced-motion handling. */
.pinup { position: relative; }
.pinup::after {
  content: ""; position: absolute; top: -9px; left: 24px; z-index: 5;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--pin, var(--coral)); /* flat fill (no gradient); inset shadows dome the thumbtack */
  box-shadow: var(--pin-shadow); /* one thumbtack shadow, shared with .pushpin */
}
.pinup.pin-center::after { left: 50%; transform: translateX(-50%); }
.pin-lazuli { --pin: var(--color-lazuli); }
.pin-kraft  { --pin: #b08c5a; }   /* neutral kraft pin — replaces the retired yellow pin-bus */
.pin-coral  { --pin: var(--color-colorado); }
/* NOTE: no grass pushpin (grass = live/makers/brand) and no yellow pushpin (yellow = Circle things only). */

/* ── btn-linky  [was globals.css:3882–3883] ─────────────────────────── */
.btn-linky { border: 0; background: none; padding: 0; font: inherit; color: var(--brand); text-decoration: underline; cursor: pointer; }
.btn-linky:disabled { opacity: .55; cursor: default; }

/* ── Dymo — embossed label tape  [was globals.css:3917–3940] ─────────────────────────── */
/* DYMO — embossed red label tape (flat matte red plastic + raised white letters), for text on cork */
.dymo{display:inline-block;font-family:var(--font-mono);font-weight:500;font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;
  color:#fdefef;background:#bd3a33;border-radius:2px;padding:8px 16px;
  box-shadow:2px 3px 0 rgba(65,77,87,.32);text-shadow:0 -0.5px 0 rgba(255,255,255,.35), 0 1.5px 1px rgba(46,6,5,.65);
  transform:rotate(var(--dr,-1.5deg));position:relative;line-height:1;}
.dymo b{color:#fff;font-weight:500;}
.dymo .accent{color:#ffe08a;}
/* The dymo PRIMITIVE is always red (Sue) — the old grass/ink color variants now no-op to the base red,
   and that axis stays closed: there is no .dymo--bus. The School-Bus set-tape in the circle hero
   (.ct-set-tape) is a separate STAMP built on the same roll, not a recolour of this one — it needs an
   ink keyline this plate does not (dymo red on the marquee paper is 5.40:1 and carries its own
   silhouette; School Bus is 1.92:1 and carries none) and dark brown letters instead of this cream
   (cream on School Bus is 1.75:1). Amended 2026-08-04. */
.dymo--grass{background:#bd3a33;} .dymo--ink{background:#bd3a33;}
.dymo--sm{font-size:.56rem;padding:6px 13px;letter-spacing:.17em;}
/* ...with ONE sanctioned exception (Sue, 2026-08-04): a LAZULI roll, for the challenge's resource
   links. It exists because a RUN of labels is only possible in a cool hue — six red strips under the
   brief is a wall of alarm, and red is spoken for besides (Colorado = open issue / error /
   destructive, which reading material is none of). Lazuli already means INFO, which is what a
   resource link is. Deliberately a pressed-DARKER lazuli than the #2496C9 token: the raised letters
   have to clear AA, and #f2fbff on #2496C9 measures only 3.35:1 (fail) versus 5.3:1 here. The emboss
   shadow goes cool for the same reason the red roll's is warm — it is the tape's own colour, bruised. */
.dymo--lazuli{background:var(--tape-lazuli,#1a7099);color:var(--tape-lazuli-ink,#f2fbff);
  text-shadow:0 -0.5px 0 rgba(255,255,255,.35), 0 1.5px 1px rgba(4,28,42,.6);}

/* ── btn-coral  [was globals.css:5352–5352] ─────────────────────────── */
.btn-coral{background:var(--coral);color:#fff;border-color:var(--ink);}

/* ── Deadline countdown ticket — shares the .dl-* state vocabulary with the bar below  [was globals.css:6091–6116] ─────────────────────────── */
/* ── Deadline countdown ticket (working issue) ────────────────────────────────────────────────────
   A torn admission stub on the current issue: "get it done before we next meet." Adaptive unit
   (Circle Times → days → live h:m) + a runway that fills as the deadline nears; escalates ink →
   School-Bus amber → Colorado red, then a quiet archive-sepia "was due" once it passes (SOFT — the
   deadline never closes the issue). Flat colour + hard offset shadow, no gradients (DESIGN.md).
   Component: IssueCountdown.tsx. Happy yellow is intentionally absent (reserved for live). */
.dl-ticket { display: inline-flex; align-items: stretch; margin: 4px 0 8px 10px; border: 2.5px solid var(--ink);
  border-radius: 5px; overflow: hidden; background: var(--paper); box-shadow: 3px 3px 0 rgba(65,77,87,.8);
  transform: rotate(-1deg); max-width: 264px; font-family: var(--font-mono); }
.dl-stub { display: flex; flex-direction: column; justify-content: center; text-align: center; padding: 6px 10px; min-width: 70px; color: #fff; }
.dl-n { font-family: var(--font-display); font-style: italic; font-weight: 700; font-size: 1.5rem; line-height: .95; }
.dl-u { font-size: .5rem; letter-spacing: .1em; text-transform: uppercase; margin-top: 2px; }
.dl-body { position: relative; padding: 6px 12px; border-left: 2.5px dashed var(--ink); display: flex; flex-direction: column; justify-content: center; gap: 1px; }
.dl-body::before, .dl-body::after { content: ""; position: absolute; left: -7px; width: 11px; height: 11px; border-radius: 50%; background: var(--oats); border: 2.5px solid var(--ink); }
.dl-body::before { top: -8px; } .dl-body::after { bottom: -8px; }
.dl-t { font-size: .5rem; letter-spacing: .14em; text-transform: uppercase; color: #8f8a79; }
.dl-dt { font-family: var(--font-display); font-style: italic; font-weight: 700; font-size: .95rem; color: var(--ink); line-height: 1; }
.dl-sub { font-size: .52rem; letter-spacing: .03em; color: #8f8a79; }
.dl-run { display: block; height: 5px; margin-top: 3px; border: 1.5px solid var(--ink); border-radius: 4px; background: var(--paper); overflow: hidden; }
.dl-run i { display: block; height: 100%; background: var(--ink); }
.dl-scrawl { font-family: var(--font-hand); color: var(--sig-archive); font-size: .64rem; margin-top: 1px; }
.dl-calm .dl-stub { background: var(--ink); } .dl-calm .dl-run i { background: var(--ink); }
.dl-amber .dl-stub { background: var(--color-school-bus); color: var(--ink); } .dl-amber .dl-run i { background: var(--color-school-bus); }
.dl-red { border-color: var(--color-colorado); } .dl-red .dl-stub { background: var(--color-colorado); } .dl-red .dl-run i { background: var(--color-colorado); }
.dl-past { opacity: .68; } .dl-past .dl-stub { background: var(--sig-archive); } .dl-past .dl-run i { background: var(--sig-archive); }
@media (prefers-reduced-motion: reduce) { .dl-ticket { transform: none; } }

/* ── The deadline, drawn as a marker line  [was globals.css:6875–6919] ─────────────────────────── */
/* ── THE DEADLINE, drawn as a marker line ───────────────────────────────────────────────────
   Replaces the torn ticket stub. Founder's spec: a marker-drawn bar, waypoint dots for the Circle
   Times left, a due-date label, days-left in words, small vertical ticks for days, going RED as it
   closes. It sits UNDER the button because it's consequence, not instruction. */
.dl-bar{display:block;margin:12px 0 2px;max-width:520px;}
.dl-cap{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:7px;}
.dl-days{font-family:var(--font-mono);font-weight:500;font-size:.78rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink);}
.dl-due{font-family:var(--font-hand);font-size:.98rem;color:var(--ink);opacity:.85;}
/* the track is a hand-drawn stroke, not a ruled rectangle */
.dl-track{position:relative;display:block;height:16px;
  background:repeat-x left center/200px 7px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='7'%3E%3Cpath d='M0 3.6 C 24 2, 48 5.2, 72 3.6 C 96 2.1, 120 5.1, 144 3.6 C 168 2.2, 184 5, 200 3.6' fill='none' stroke='%23414d57' stroke-width='1.8' stroke-linecap='round' opacity='.34'/%3E%3C/svg%3E");}
/* the elapsed run: the same stroke, inked in — colour escalates with state */
.dl-fill{position:absolute;left:0;top:0;height:16px;
  background:repeat-x left center/200px 7px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='7'%3E%3Cpath d='M0 3.6 C 24 2, 48 5.2, 72 3.6 C 96 2.1, 120 5.1, 144 3.6 C 168 2.2, 184 5, 200 3.6' fill='none' stroke='%23414d57' stroke-width='3.4' stroke-linecap='round'/%3E%3C/svg%3E");}
/* days */
.dl-tick{position:absolute;top:4px;width:0;height:8px;border-left:1.6px solid var(--ink);opacity:.3;}
/* Circle Times — the waypoints. Hollow ahead, inked once they've gone by. */
.dl-way{position:absolute;top:2px;width:12px;height:12px;margin-left:-6px;border-radius:50%;
  background:var(--paper);border:2.2px solid var(--ink);box-shadow:1px 1px 0 rgba(65,77,87,.35);}
.dl-way.is-gone{background:var(--ink);}
.dl-now{position:absolute;top:0;width:0;height:16px;border-left:2.4px solid var(--ink);margin-left:-1px;}
.dl-scrawl{display:block;margin-top:5px;font-family:var(--font-hand);font-size:.95rem;opacity:.85;}
/* RED RED RED as it closes */
.dl-amber .dl-fill{filter:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='7'%3E%3Cpath d='M0 3.6 C 24 2, 48 5.2, 72 3.6 C 96 2.1, 120 5.1, 144 3.6 C 168 2.2, 184 5, 200 3.6' fill='none' stroke='%23ecaf21' stroke-width='3.8' stroke-linecap='round'/%3E%3C/svg%3E");}
.dl-red .dl-fill{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='7'%3E%3Cpath d='M0 3.6 C 24 2, 48 5.2, 72 3.6 C 96 2.1, 120 5.1, 144 3.6 C 168 2.2, 184 5, 200 3.6' fill='none' stroke='%23f75458' stroke-width='4.2' stroke-linecap='round'/%3E%3C/svg%3E");}
.dl-red .dl-days,.dl-red .dl-now{color:var(--coral);border-color:var(--coral);}
.dl-red .dl-way{border-color:var(--coral);}
.dl-past{opacity:.72;}
.dl-past .dl-fill{filter:grayscale(.7);}
/* Founder's colour pass on the deadline bar. School Bus on the waypoints is the strongest call
   here: School Bus already MEANS Circle Time in this system (it's the door's-open hue), so the
   dots say "these are gatherings" without inventing a signal. The due date goes Lazuli, which
   DESIGN.md scopes to info — which is exactly what a due date is — while the days-left count
   stays Sambas ink so the urgent number keeps the strongest contrast. Red still overrides all
   of it as the deadline closes; the escalation must not be out-shouted by the resting palette. */
.dl-due{color:var(--color-lazuli);opacity:1;}
.dl-days{color:var(--color-sambas);}
.dl-way{border-color:var(--color-school-bus);background:var(--paper);}
.dl-way.is-gone{background:var(--color-school-bus);}
.dl-red .dl-due{color:var(--coral);}
.dl-red .dl-way{border-color:var(--coral);}
.dl-red .dl-way.is-gone{background:var(--coral);}

/* ── dl-fill: day-1 stub  [was globals.css:6922–6924] ─────────────────────────── */
/* Day 1 of a fresh issue is a genuinely empty bar, which reads as broken rather than as "just
   started". A short stub keeps the stroke legible from the first day without lying about progress. */
.dl-fill{min-width:14px;}

/* ── Deadline bar, founder's palette  [was globals.css:6944–6951] ─────────────────────────── */
/* ── deadline bar, founder's palette ─────────────────────────────────────────────────────────
   Track + past waypoints in Sambas (the structure), elapsed run in Grass (progress made),
   waypoints still ahead in School Bus (they're Circle Times). Red still overrides everything as
   the deadline closes — the escalation must out-shout the resting palette, not sit inside it. */
.dl-fill{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='7'%3E%3Cpath d='M0 3.6 C 24 2, 48 5.2, 72 3.6 C 96 2.1, 120 5.1, 144 3.6 C 168 2.2, 184 5, 200 3.6' fill='none' stroke='%231cab5b' stroke-width='3.6' stroke-linecap='round'/%3E%3C/svg%3E");}
.dl-way{border-color:var(--color-school-bus);}
.dl-way.is-gone{background:var(--color-sambas);border-color:var(--color-sambas);}
.dl-now{border-color:var(--color-sambas);}

/* ── The bar ends in the deadline  [was globals.css:7036–7049] ─────────────────────────── */
/* ── the bar ends in the deadline ────────────────────────────────────────────────────────────
   Founder: "shouldn't the end of the due date bar have something symbolizing due date like an
   exclamation mark?" Right — an unterminated line just stops, which reads as unfinished rather
   than as a wall you run into. A hand-drawn "!" on a post at the end, reddening with the bar. */
.dl-bar{max-width:none;}                    /* founder: "make sure it's 100% width" */
.dl-track{padding-right:22px;}              /* room for the terminal so it can't clip */
.dl-end{position:absolute;right:0;top:-5px;width:22px;height:26px;text-decoration:none;
  display:grid;place-items:center;font-family:var(--font-hand);font-weight:700;font-size:1.25rem;
  line-height:1;color:var(--color-sambas);transform:rotate(4deg);}
.dl-end::before{content:"";position:absolute;left:50%;top:6px;bottom:-4px;
  border-left:2.4px solid var(--color-sambas);transform:translateX(-50%);}
.dl-amber .dl-end,.dl-amber .dl-end::before{color:var(--color-school-bus);border-color:var(--color-school-bus);}
.dl-red .dl-end{color:var(--coral);transform:rotate(4deg) scale(1.18);}
.dl-red .dl-end::before{border-color:var(--coral);}

/* ── Deadline bar, final pass  [was globals.css:7074–7087] ─────────────────────────── */
/* ── deadline bar, final pass ────────────────────────────────────────────────────────────────
   "you are here" gets thicker and goes Grass — it's the one live thing on the track and it was
   the same weight and colour as the day ticks. The due date needs no post at the end: the
   deadline always lands ON a Circle Time, so the LAST waypoint IS the due date. Filling it says
   "this is the one" without adding a second mark for the same fact. Only "due by x" is Lazuli. */
.dl-now{border-left-width:4px;border-color:var(--color-grass-field);height:20px;top:-2px;}
.dl-red .dl-now{border-color:var(--coral);}
.dl-end{display:none;}
.dl-track{padding-right:8px;}
.dl-way.is-due{background:var(--color-school-bus);border-color:var(--color-sambas);
  width:15px;height:15px;top:1px;margin-left:-7px;box-shadow:1px 2px 0 rgba(65,77,87,.45);}
.dl-red .dl-way.is-due{background:var(--coral);border-color:var(--coral);}
.dl-days{color:var(--color-sambas);}
.dl-due{color:var(--color-lazuli);}

/* ── btn-go  [was globals.css:7089–7092] ─────────────────────────── */
/* the hero's one primary, in brand Grass. --brand-press is the same hue laid down heavier, used
   only because oats-on-Grass at this label size measures 2.78:1 — under AA. */
.btn-go{background:var(--brand-press,#14803f);color:var(--color-oats);border-color:var(--ink);}
.btn-go:hover{background:#1cab5b;}

/* ── THE CHALLENGE CARD  [was globals.css:7312–7465] ─────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE CHALLENGE CARD + THE BULLETIN BOARD (2026-08-04)
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   One card object, two homes: the orb HUD's lead section (.chal-hud) and the bulletin board's lead
   band (.chal-board). Both render <ChallengeCardView> from the same payload, so anything that makes
   them LOOK like different objects here is a bug, not a variant.

   Everything below composes existing primitives — .card/.pinup (torn sheet + thumbtack), .dymo
   (embossed structural label), .scrap (status chip), .btn (rubber stamp), .dl-* (the hand-marker
   deadline line). No new material is invented. */

/* ── The open-ended ruler ──────────────────────────────────────────────────────────────────────
   Almost every real challenge has NO deadline (issues.deadline is nullable, migration 0053 nulled
   every working issue's, and only an editor of a circle WITH a Circle Time can even set one). So the
   default line has no end: the stroke dissolves under an arrow instead of hitting the deadline's "!"
   post, and there are no waypoint discs, because a disc is a landmark on the way to somewhere and
   there is nowhere. It must never draw a finish line the product does not enforce. */
.dl-open .dl-track { padding-right: 26px; }
/* The track's own stroke dissolves toward the open end, so the line does not simply STOP (a stop is
   a wall, which is the one thing there isn't). The fill is capped short of this in JS, so there is
   always visible empty track between the ink and the arrow. */
.dl-open .dl-track::after {
  content: ""; position: absolute; right: 20px; top: 0; bottom: 0; width: 54px;
  background: linear-gradient(90deg, transparent, var(--paper) 92%);
  pointer-events: none; z-index: 1;
}
.dl-open .dl-now { display: none; }   /* no playhead: a playhead implies a track it is playing along */
.dl-arrow {
  position: absolute; right: -2px; top: 50%; transform: translateY(-54%);
  font-family: var(--font-hand); font-size: 1.3rem; line-height: 1;
  color: var(--color-sambas); opacity: .8; z-index: 2;
}
/* On the HUD the card sits on the panel's tint, not on the cream sheet — fade to that instead, or
   the dissolve paints a pale block over the panel's own ground. */
.chal-hud .dl-open .dl-track::after { background: linear-gradient(90deg, transparent, #fdfbf3 92%); }
.chal-hud .dl-arrow { font-size: 1.1rem; }

/* ── The compact line (HUD) ───────────────────────────────────────────────────────────────────
   Same stroke, less furniture: the caption folds onto one line, day ticks go (at this width they
   read as noise, not as days), and the whole unit loses its bottom margin. */
.dl-compact { margin: 7px 0 0; }
.dl-compact .dl-cap { margin-bottom: 4px; gap: 7px; }
.dl-compact .dl-days { font-size: .68rem; letter-spacing: .06em; }
.dl-compact .dl-due { font-size: .88rem; }
.dl-compact .dl-track { height: 13px; }
.dl-compact .dl-fill { height: 13px; }
.dl-compact .dl-now { height: 13px; }
.dl-compact .dl-way { top: 1px; width: 10px; height: 10px; margin-left: -5px; border-width: 2px; }

/* ── The card ────────────────────────────────────────────────────────────────────────────────── */
.chal-list { display: flex; flex-direction: column; gap: var(--sp-lg); }
.chal-list-board { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: var(--sp-xl) var(--sp-lg); }
.chal-list-hud { gap: var(--sp-sm); margin-top: var(--sp-sm); }

.chal { position: relative; }
.chal-board { padding: var(--sp-lg) var(--sp-md) var(--sp-md); }
/* Cards settle at a tiny tilt — the anti-design seasoning, alternating so a grid never lines up. */
.chal-board:nth-child(even) { --dr: 1.2deg; transform: rotate(.5deg); }
.chal-board:nth-child(odd) { transform: rotate(-.45deg); }
.chal-board:nth-child(3n) { transform: rotate(.8deg); }

.chal-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-sm); margin-bottom: var(--sp-sm); }
.chal-status { flex: 0 0 auto; }

/* THE SPINE LABEL — the circle's name as a vertical Dymo strip down the card's left edge. It does the
   job the coloured rule used to do (say which circle this is, at a glance, without reading) AND the
   job the header row used to do, for the price of neither.
   `vertical-rl` + 180deg reads bottom-to-top, which is how a strip on a LEFT edge is read.
   The strip is clipped rather than allowed to grow: a long circle name must not set the card's
   height, and the name is never load-bearing here — the title carries the meaning and the whole card
   links to the circle anyway. */
/* THE SPINE IS THE DYMO'S STANDING RED — until Circle Time, when it turns School Bus (founder,
   2026-08-05). That is the whole point of the colour change: yellow MEANS Circle Time in this system,
   so a permanently yellow tape would say "the room is open" on every card forever and the signal would
   stop meaning anything. Red is the label-maker's default and stays the resting state; the tape only
   goes warm while the window actually is.
   THE TEXT MUST GO DARK WITH IT. The dymo's near-white #fdefef measures 1.75:1 on #ECAF21 —
   unreadable — and even var(--ink) is 4.42:1, which fails at this size. #4a3410 is 5.99:1, the same
   pairing the dormant Circle-Time tape already uses. The emboss shadow flips light for the same reason,
   and the tape's cast shadow goes School-Bus-toned so the whole strip reads as one warm object.
   Both states apply on the HUD and the bulletin board alike — one rule, both surfaces, because
   `.chal` is the same component in both. */
.chal.is-live .chal-spine,
.chal.is-door .chal-spine {
  background: var(--color-school-bus);
  color: #4a3410;
  text-shadow: 0 1px 0 rgba(255,255,255,.3), 0 -1px 1px rgba(74,52,16,.5);
  box-shadow: 2px 3px 0 rgba(160, 112, 12, .55);
}
.chal-spine {
  position: absolute; left: 0; top: 12px; bottom: 12px; z-index: 3;
  writing-mode: vertical-rl; transform: rotate(180deg);
  padding: 10px 5px; letter-spacing: .14em;
  /* nowrap + clip, never wrap: a long circle name wrapping inside a vertical strip turns the label
     into a fat block and each card grows a different-width spine, which is the opposite of the
     lined-up look the strip is for. The name is not load-bearing here — the title carries the
     meaning and the whole card links to the circle — so clipping it is a fair trade for a strip that
     is the same shape on every card in the row. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  box-shadow: 2px -3px 0 rgba(65, 77, 87, .32);   /* the offset follows the rotation */
}
.chal { padding-left: 30px; }
.chal-board { padding: var(--sp-lg) var(--sp-md) var(--sp-md) 34px; }
.chal-hud { padding: 10px 12px 11px 32px; }

.chal-title { font-family: var(--font-display); font-style: italic; font-weight: 700; font-size: 1.12rem;
  line-height: 1.2; margin: 0 0 var(--sp-xs); letter-spacing: 0; }
.chal-title a { color: inherit; text-decoration: none; }
.chal-title a:hover { text-decoration: underline; text-decoration-style: wavy; text-underline-offset: 3px; }

/* ── The fixed slot: is my page in? ───────────────────────────────────────────────────────────
   A hand-drawn checkbox and a WORD, always both, always present. The slot never disappears, so its
   emptiness can't be mistaken for having nothing to say — and it never scolds: "no page from you
   yet" is a fact about a box, not a judgement about a person. */
.chal-tick { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-hand);
  font-size: .98rem; line-height: 1.15; color: var(--ink); }
.chal-tick::before {
  content: ""; flex: 0 0 auto; width: 15px; height: 15px;
  border: 2px solid var(--ink); border-radius: 2px; background: var(--paper);
  filter: url(#roughsm);                       /* the box is drawn by hand, like everything else */
}
/* Ticked — a marker check that overshoots the box, the way a real one does. */
.chal-tick[data-my="in"]::before {
  background: var(--paper) center/13px 13px no-repeat
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M2.5 8.4 L6.2 12.4 L14 2.6' fill='none' stroke='%231cab5b' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  border-color: var(--brand);
}
/* Started — one pencil stroke, not a tick. Half-finished is a real state, not a failed one. */
.chal-tick[data-my="started"]::before {
  background: var(--paper) center/11px 11px no-repeat
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M3 9.6 C 6 8.2, 9 7.4, 13 6.2' fill='none' stroke='%235f6873' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
  border-color: var(--graphite);
}
.chal-s-closed .chal-tick, .chal-s-published .chal-tick { color: var(--text-muted); }

.chal-count { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--graphite); white-space: nowrap; }

.chal-foot { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-sm) var(--sp-md);
  margin-top: var(--sp-md); }
.chal-cta { margin-left: auto; min-height: 40px; padding: var(--sp-sm) var(--sp-md); font-size: .74rem; }

/* ONE filled button per card, and the fill says WHICH kind of thing it is:
     live / door  → School Bus, the sanctioned Circle-Time CTA hue ("come in, the door's open").
                    Happy is NOT used here — it stays on the CircleMark beside it, where the beacon
                    is gated on someone actually being present.
     open / read  → grass, the brand's primary action.
     quiet        → not filled at all. Nothing is being asked of you, so nothing shouts. */
.chal-cta-live, .chal-cta-door { background: var(--color-school-bus); color: #4a3410; }
/* #4a3410, never var(--ink): Sambas on School Bus measures 4.42:1 and FAILS AA at this size. */
.chal-cta-open, .chal-cta-read { background: var(--brand); color: var(--color-oats); }
.chal-cta-quiet { background: var(--paper); color: var(--ink); }

/* The editor's aside — the ONLY person who can answer it is the only person who sees it. Hand type,
   because it is a note in the margin, not a control. */
.chal-nodue { display: inline-block; margin-top: var(--sp-sm); font-family: var(--font-hand);
  font-size: .95rem; color: var(--graphite); text-decoration: none; }
.chal-nodue::before { content: "↳ "; opacity: .7; }
.chal-nodue:hover { color: var(--coral); text-decoration: underline; }

/* ── The HUD variant ──────────────────────────────────────────────────────────────────────────
   Inside the orb panel the card must NOT be another sheet of paper — the panel is already a
   surface, and paper-on-same-paper is a standing prohibition. So it is a flat block with a coloured
   spine down its left edge (coral = an open issue wanting pages), separated by hairlines. */
.chal-hud { border-radius: 0 var(--r-sm) var(--r-sm) 0; background: rgba(255, 253, 246, .55); }
.chal-hud + .chal-hud { margin-top: var(--sp-sm); }
.chal-hud .chal-title { font-size: 1rem; }
.chal-hud .chal-head { margin-bottom: 5px; gap: 6px; }
.chal-hud .chal-foot { margin-top: 9px; gap: 6px var(--sp-sm); }
.chal-hud .chal-tick { font-size: .9rem; }
.chal-hud .chal-cta { min-height: 34px; padding: 6px 11px; font-size: .66rem; transform: rotate(-.6deg); }
.chal-hud .chal-count { font-size: .62rem; }

/* ── The HUD's challenge section rhythm  [was globals.css:7609–7610] ─────────────────────────── */
/* The HUD's challenge section keeps the panel's own rhythm. */
.orb-challenges .orb-rows { margin-bottom: var(--sp-sm); }

/* ── The card on a phone  [was globals.css:7613/7614/7617, inside a shared @media] ─────────────
   Split out of the bulletin board's own phone block (which kept .nb-logs, the .log- rows and
   .nb-band-h): those are board furniture, these three are the CARD, and the card is the thing every
   surface shares. */
@media (max-width: 559px) {
  .chal-list-board { grid-template-columns: 1fr; }
  .chal-board { transform: none; }          /* tilts stack badly in a single column */
  .chal-cta { margin-left: 0; width: 100%; }
}

/* ── Reduced motion note (challenge card)  [was globals.css:7621–7622] ─────────────────────────── */
/* Reduced motion: the tilts are static transforms, not animation, so they stay — but nothing here
   may hover-animate. (.btn's press-in is already inside the global reduced-motion guard.) */

/* ── chal-more  [was globals.css:7633–7637] ─────────────────────────── */
/* The HUD's "there are more of these" line. Named, not silent: a truncated list that doesn't admit
   it reads as a complete one. Hand type, because it is an aside about the list, not an item in it. */
.chal-more { display: block; margin-top: var(--sp-sm); padding: 6px 4px 2px 14px;
  font-family: var(--font-hand); font-size: .95rem; color: var(--graphite); text-decoration: none; }
.chal-more:hover { color: var(--coral); text-decoration: underline; }

/* ── orb-panel-out  [was globals.css:7658–7662] ─────────────────────────── */
/* The panel header's way out to the bulletin board — it replaced the panel's own title, so it takes
   that title's weight rather than looking like a stray link parked next to the close button. */
.orb-panel-out { font-family: var(--font-mono); font-weight: 500; font-size: .72rem;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink); text-decoration: none; }
.orb-panel-out:hover { color: var(--coral); text-decoration: none; }
/* ── THE PIP: unread bulletin news, as one object ────────────────────────────────────────────────
   A flat coral scrap with an ink keyline and an ink numeral — no blur, no gradient (DESIGN.md).
   THIS IS THE MATERIAL ONLY. A host adds its own positioning and nothing else: the app orb pins it to
   the orb's top-right (.orb-pip), the reader does the same (.circ-pip), /z likewise (.co-pip), and the
   panel's "Circle bulletin" row simply lets it sit inline. Same fact, same object, everywhere it
   appears — which is the point, because the orb's pip and the panel's pip are one statement seen twice
   on the way to the same page.
   Ink on coral, never cream: coral is a 10%-budget attention colour and cream on it is under AA at
   this size. */
.pip {
  display: inline-grid; place-items: center; box-sizing: border-box;
  min-width: 16px; height: 16px; padding: 0 3px; vertical-align: -3px;
  font-family: var(--font-mono); font-weight: 600; font-size: 0.58rem; line-height: 1;
  color: var(--ink); background: var(--color-colorado);
  border: 1.5px solid var(--ink); border-radius: 999px;
  box-shadow: 1px 1px 0 var(--ink); flex: 0 0 auto;
}
/* the row is uppercase mono with wide tracking; the pip must not inherit either */
.orb-panel-out .pip { text-transform: none; letter-spacing: 0; }
/* The second way out — the planner. It sits BEFORE the ✕ and is pushed off the bulletin link rather
   than centred, so the two doorways read as a pair with the close button held apart from them.
   `margin-right:auto` on the FIRST child would have done it too, but that couples the head's layout
   to child order; an explicit gap here survives someone adding a third thing. */
.orb-panel-out--next { margin-left: auto; opacity: .78; }
.orb-panel-out--next:hover { opacity: 1; }
/* The panel head is a flex row (see .orb-panel-head); at the narrowest phone width two uppercase
   mono links plus the ✕ stop fitting, so the planner link drops its arrow-tail spacing first. */
@media (max-width: 380px) { .orb-panel-out { font-size: .66rem; letter-spacing: .05em; } }

/* NEXT UP, as a footer line rather than a card — the fact at the bottom of a panel full of errands.
   Ink and mono, never an accent: every hue in this panel is an orb signal, and Happy specifically
   means live-AND-present, which a scheduled window is not. */
.orb-nextline {
  display: block; margin: 8px 4px 2px; padding: 9px 4px 3px;
  border-top: 1px dashed rgba(65, 77, 87, 0.22);
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .04em;
  color: var(--text-muted); line-height: 1.35; text-decoration: none;
}
/* A LINE THAT IS A DOOR. It stays a line — no chip, no button costume, no arrow tail — because
   everything above it in this panel is an errand and this is a fact. The only tell that it is
   tappable is the hover, which is the same tell .orb-panel-out uses. */
.orb-nextline:hover { color: var(--ink); text-decoration: none; background: rgba(65, 77, 87, 0.05); }
.orb-nextline:hover .orb-nextline-when { color: var(--coral); }
.orb-nextline:focus-visible { outline: 3px solid var(--color-lazuli); outline-offset: -2px; border-radius: 4px; }
.orb-nextline-when { color: var(--ink); font-weight: 500; }

/* ── The face primitive  [was globals.css:593–600] ──────────────────────────
   Shared <Avatar> (src/app/Avatar.tsx): the one face primitive — real PFP where we have it, an initial
   chip otherwise. Die-cut round sticker treatment so every call site reads consistent. Width/height come
   inline from the size prop.

   IT LIVES HERE because the hub draws faces: the live rows, the making rows and the invitation row all
   carry an .orb-face, and public/orb/hub-html.js emits exactly this markup on the reader and /z. Left in
   globals.css those surfaces got a square, unclipped photo with no cut edge.

   MOVING IT CANNOT CHANGE THE APP. Both selectors are (0,1,0), and every per-surface face rule that
   competes with them (.roster .member img · .here-faces .member img · .wb-face img · .marquee .who
   .member .avatar-blank · .avatar-foil×3) is (0,2,0) or higher, so the winner is decided by specificity
   and never by document order. `.avatar-link` (a face that is a door — the hub never links a face,
   because these clusters are already nested inside a row anchor) and the FOIL RING stayed in globals:
   the ring's (0,3,0) TIES `.marquee .who .member`, and it wins that tie by sitting last in the file. */
.avatar { border-radius: 50%; object-fit: cover; border: 2.5px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  background: var(--bg, #fef6e4); flex: 0 0 auto; display: inline-grid; place-items: center; vertical-align: middle; overflow: hidden; }
.avatar-blank { background: rgba(65,77,87,0.12); color: var(--ink); font-family: var(--font-mono);
  font-weight: 600; text-transform: uppercase; line-height: 1; }

/* ── "the circle time is live"  [was globals.css:1173–1182] ─────────────────
   ONE reusable treatment, reserved for circle-time-live everywhere. A live challenge card carries
   .is-live on its <article>, so hub-html.js emits it on every vanilla surface, and Happy #F1DD01 is
   the reserved ALIVE beacon — losing it on the reader would mean the loudest state in the product
   reads exactly like the quietest. (0,1,0); every competing rule (.circle-pin.is-live, .ct-pad.is-live,
   .marquee .who .member.is-live) is (0,2,0) or higher, so the move is cascade-neutral. */
.is-live { border-color: var(--sig-live) !important; box-shadow: var(--live-glow); animation: livepulse 1.9s ease-in-out infinite; }
@keyframes livepulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--bg), 0 0 9px 1px rgba(241,221,1, 0.5); }
  50% { box-shadow: 0 0 0 2px var(--bg), 0 0 19px 4px rgba(241,221,1, 0.92); }
}
@media (prefers-reduced-motion: reduce) { .is-live { animation: none; } }

/* ── CircleMark  [was globals.css:1186–1217] ────────────────────────────────
   The one stateful Circle glyph (see CircleMark.tsx + DESIGN.md "The Circle mark"). Hand-drawn marker
   ring; phase = ring color, live = Happy-yellow glow + pulse, presence = center dot/count, read = sepia.
   Flat color + solid stroke only.

   IT LIVES HERE because a challenge card renders a CircleMark whenever Circle Time is live or the
   door's open, so hub-html.js emits it on every vanilla surface. Unstyled it is not a faint ring but a
   catastrophe: `.circle-mark > svg` is what constrains the SVG to its box and `.cm-ring` is what sets
   `fill:none`, so without them the browser lays the glyph out at its default replaced-element size and
   fills it solid — a ~300px black disc in the middle of the card.

   `filter: url(#rough)` needs an inline <filter id="rough"> in the host document. Every host already
   ships one: src/app/layout.tsx, src/app/z/desk.ts and demos/book-reader/reader.html.

   Nothing else in globals.css targets .circle-mark or .cm-*, so this move is cascade-neutral. */
.circle-mark { position: relative; display: inline-grid; place-items: center; flex: 0 0 auto; line-height: 0; vertical-align: middle; }
.cm-xs { width: 16px; height: 16px; }
.cm-sm { width: 24px; height: 24px; }
.cm-lg { width: 56px; height: 56px; }
.circle-mark > svg { width: 100%; height: 100%; display: block; overflow: visible; }
.cm-ring { fill: none; stroke: var(--cm-color, var(--ink)); stroke-width: 2.6; stroke-linecap: round; filter: url(#rough); transition: stroke var(--dur-short) var(--ease-out); }  /* #rough boils the true circle into a hand-drawn ring (matches the reader) */
.cm-quiet { --cm-color: rgba(65, 77, 87, 0.5); }   /* ink, faded — nothing happening */
.cm-open { --cm-color: var(--sig-open); }
/* Circle Time window open but nobody else here yet — calm School Bus, no pulse. It gets the same HARD
   offset the live mark gets, in its own colour, so the two Circle-Time states are one object in two
   moods rather than two different drawing styles. Still no blur: hard offsets only. */
.cm-dooropen { --cm-color: var(--color-school-bus); filter: drop-shadow(2px 2px 0 var(--color-school-bus)); }
.cm-developing { --cm-color: var(--sig-developing); }
.cm-published { --cm-color: var(--sig-archive); }
/* A HARD Happy offset, not a halo (founder, 2026-08-05). This was a blurred `drop-shadow(0 0 12px)`
   glow, which is the one thing DESIGN.md's shadow rule forbids outright — "no soft blur, cartoonish
   HARD shadows only", the rule that reverted the soft-cork pass. Every other shadow in the system is a
   flat solid offset; the live beacon was the last blurred thing in the chrome. It still reads as ALIVE
   because the colour is still the reserved Happy and the radiating ping below is still there — and a
   ping is a BORDER, not a blur, so it was always compliant. */
.cm-live { --cm-color: var(--sig-live); filter: drop-shadow(2px 2px 0 var(--sig-live)); }
/* radiating "ping" so live reads as live action, not a faint pulse */
.cm-live::after {
  content: ""; position: absolute; inset: -2px; border-radius: 50%; border: 2px solid var(--sig-live);
  opacity: 0; pointer-events: none; animation: cm-ping 1.8s ease-out infinite;
}
@keyframes cm-ping { 0% { transform: scale(0.85); opacity: 0.7; } 70%, 100% { transform: scale(2.1); opacity: 0; } }
/* busy = more people in Circle Time → faster, more intense */
.cm-busy { animation-duration: 1s; }
.cm-busy::after { animation-duration: 1.05s; border-width: 2.5px; }
/* presence is NEVER a center dot (removed by request) — it only intensifies the live glow (cm-busy).
   Who's in the room is shown by HereNow faces/count, never a dot inside the ring. */
.cm-read { filter: sepia(0.42) saturate(0.8) brightness(0.95); }
@media (prefers-reduced-motion: reduce) { .cm-live { filter: drop-shadow(2px 2px 0 var(--sig-live)); } .cm-live::after { display: none; } }

/* ── The hero's ink floor, the one line of it that was never hero-scoped  [was globals.css:7294] ──
   Part of the "SAMBAS IS THE FLOOR IN THE HERO" block, which flattens decorative opacity on the
   circle marquee. Every other selector in that block is scoped to .marquee / .ct-* ; `.dl-due` is
   not, so it is the FINAL word on the due-date caption on every surface — which makes it a hub rule,
   not a hero rule. It stays LAST in this file for the same reason it was last in globals.css: it
   overrides the lazuli pass above it. */
.dl-due { color: var(--ink); opacity: 1; }

/* ── The Circle-Time blip rides the CORNER  (founder, 2026-08-05) ─────────────────────────────────
   It used to sit in `.chal-head`, which meant a live circle spent a whole ROW on one 14px glyph —
   on a card whose entire budget is vertical space. Absolute top-right costs nothing, and the header
   row now only appears when there is a status scrap to put in it. */
.chal .circle-mark { position: absolute; top: 9px; right: 10px; z-index: 4; }
.chal-hud .circle-mark { top: 7px; right: 8px; }

/* ── PUSH RE-PRIME NOTE (src/app/PushRePrime.tsx) ─────────────────────────────────────────────────
   The second half of the install card's sentence: the member installed, and now the OS has to be
   asked. It lives in hub.css and not globals.css because it renders on BOTH the app shell and the
   reading table — `/` is the manifest's start_url, so the installed icon lands on verbatim HTML that
   never sees globals.css, and this stylesheet is the one file both surfaces already load.

   A PINNED NOTE, NOT THE `.reward` SCRIM it started as: this can appear over someone's page in the
   reader, where guidance is marginalia and never a modal. It deliberately wears the install note's
   own stock (whiter paper than the Oats ground, 2.4px ink keyline, hard 5px offset, kraft washi) —
   same paper, because it is the same conversation. Sits at z-44 like its sibling: under
   .navmenu-sheet (z-50) so an open menu always wins, clear of the orb (z-45) and report tab (z-48). */
.reprime {
  position: fixed; z-index: 44; box-sizing: border-box;
  top: max(74px, calc(env(safe-area-inset-top) + 70px));
  right: max(10px, env(safe-area-inset-right));
  width: min(316px, calc(100vw - 22px)); padding: 15px 17px 16px;
  background: var(--paper); color: var(--ink); font-family: var(--font-body);
  border: 2.4px solid var(--ink); border-radius: 21px 23px 18px 25px;
  box-shadow: 5px 5px 0 var(--ink);
  transform: rotate(-1.4deg) translateY(-6px); opacity: 0;
  transition: opacity .28s ease, transform .28s cubic-bezier(.2,.9,.3,1.25);
}
.reprime.show { opacity: 1; transform: rotate(-1.4deg) translateY(0); }
.reprime * { box-sizing: border-box; }
.reprime-tape {
  position: absolute; top: -10px; left: 24px; width: 58px; height: 19px;
  background: var(--tape-kraft); border: 2px solid var(--ink); border-radius: 2px;
  transform: rotate(-3.5deg); filter: url(#roughsm);
}
.reprime-kick {
  margin: 2px 0 6px; font-family: var(--font-mono); font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink); opacity: .62;
}
.reprime-title {
  margin: 0 0 5px; font-family: var(--font-display); font-style: italic; font-weight: 700;
  font-size: 1.32rem; line-height: 1.12; color: var(--ink);
}
.reprime-sub {
  margin: 0 0 12px; padding-right: 4px; font-family: var(--font-hand); font-size: 1.02rem;
  line-height: 1.34; color: var(--ink);
}
.reprime-acts { display: flex; gap: 10px; flex-wrap: wrap; }
.reprime-acts .btn { min-height: 44px; }
@media (prefers-reduced-motion: reduce) {
  .reprime, .reprime.show { transform: none; transition: opacity .01s linear; }
}
