/* ============================================================================
   noo — the shared design system. ONE source of truth for the palette, fonts,
   reset, scrollbars, the wordmark, the header frame, and the theme toggle.
   Every page links this (via the shared <head> partial) so the style is edited
   in one place, never per file. Page-unique component CSS stays in each page's
   own <style> block; only the commons live here.

   Warm paper & ink, bronze accent. Light + dark, theme-aware through the vars.
   ========================================================================= */

/* -------------------------------------------------------------- fonts (bundled;
   JetBrains Mono for language & data, Hepta Slab for the few display words). */
@font-face { font-family: 'JetBrains Mono'; src: url('/static/fonts/JetBrainsMono-Regular.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/static/fonts/JetBrainsMono-Medium.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/static/fonts/JetBrainsMono-Bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Hepta Slab'; src: url('/static/fonts/HeptaSlab.ttf') format('truetype-variations');
  font-weight: 100 900; font-style: normal; font-display: swap; }

/* ---------------------------------------------------------------- tokens: the
   canonical warm-paper/bronze palette (the values shipped 2026-07-14).

   --muted and --faint are TEXT tones and carry 9.5-12.5px copy (field labels,
   the status bar, crumbs, empty states) — normal-size text to WCAG, so the bar
   is 4.5:1, never the 3:1 large-text exemption. Both are measured against the
   darkest surface they actually sit on (--surface2, which backs .vchip/.rrow/
   .bpart on hover), so a hover state can't drop them under the bar.
   --faint-soft holds the pre-2026-07-19 --faint tone for DECORATION ONLY —
   hairlines, dots, hover rings, disabled marks, map-glyph fallbacks — where no
   text-contrast bar applies. Never put copy on it.

   --err is a TEXT tone on ordinary surfaces (inline field errors, danger links,
   the offline chip) AND the fill behind error toasts / the offline banner. Those
   two jobs pull opposite ways in dark, where --err must stay light enough to read
   on --bg (6.1:1) and is therefore too light for white ink (3.0:1 — the one
   message class that MUST be readable). So the ink that sits ON --err is its own
   paired token, exactly as --net-bg pairs with --net-text: --err-ink is white in
   light (5.7:1) and the dark warm ink in dark (5.5:1). Darkening --err itself was
   the alternative and is wrong — it would drop --err-as-text to 2.9:1. */
:root {
  --bg: #f6f4ef;
  --surface: #fdfcf8;
  --surface2: #f0ede6;
  --ink: #211d17;
  --muted: #6a6152;
  --faint: #726959;
  --faint-soft: #b3aa9a;
  --line: rgba(46, 40, 30, .14);
  --line-soft: rgba(46, 40, 30, .07);
  --accent: #96682f;
  --accent-soft: rgba(150, 104, 47, .09);
  --accent-line: rgba(150, 104, 47, .35);
  --node: #3e9e8b;
  --true: #3e8e57;
  --false: #c2543f;
  --err: #b3402a;
  --err-ink: #fff;
  --shadow-1: 0 1px 2px rgba(40,32,20,.05), 0 4px 16px rgba(40,32,20,.05);
  --shadow-2: 0 2px 6px rgba(40,32,20,.08), 0 16px 48px rgba(40,32,20,.10);
  --glass: rgba(246, 244, 239, .82);
  --net-bg: #f6f4ef; --net-edge: #cfc8ba; --net-text: #211d17;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  --slab: 'Hepta Slab', 'Iowan Old Style', Georgia, serif;
  --r-lg: 16px; --r-md: 11px; --r-sm: 7px; --radius: 12px;
  --hi: #fbf1c2;
  --ease: cubic-bezier(.22, .9, .26, 1);
  --t-fast: 140ms; --t-med: 220ms; --t-slow: 340ms;
}
[data-theme="dark"] {
  --bg: #151310;
  --surface: #1d1a16;
  --surface2: #262119;
  --ink: #eae4d8;
  --muted: #9a8f7e;
  --faint: #918879;
  --faint-soft: #6d6455;
  --line: rgba(234, 228, 216, .14);
  --line-soft: rgba(234, 228, 216, .06);
  --accent: #cfa269;
  --accent-soft: rgba(207, 162, 105, .10);
  --accent-line: rgba(207, 162, 105, .38);
  --node: #63c1ac;
  --true: #63b07e;
  --false: #cf6a55;
  --err: #d97a63;
  --err-ink: #211d17;
  --shadow-1: 0 1px 2px rgba(0,0,0,.28), 0 4px 16px rgba(0,0,0,.22);
  --shadow-2: 0 2px 6px rgba(0,0,0,.35), 0 18px 56px rgba(0,0,0,.4);
  --glass: rgba(21, 19, 16, .82);
  --net-bg: #151310; --net-edge: #4a4335; --net-text: #eae4d8;
  --hi: #3a331e;
}

/* ------------------------------------------------------- the three-name palette.
   noo / look / loom share the oo-in-the-middle stylization; each name owns one
   o-o color (pinned by Felix, 2026-07-14). THIS BLOCK IS THE ONE SOURCE — any
   wordmark, icon, or docs-page work pulls these vars, never a re-derived hex.
   noo follows --node (theme-aware: #3e9e8b light / #63c1ac dark); look and loom
   have no dark variants pinned yet, so they hold one value in both themes. */
:root {
  --brand-noo: var(--node);      /* the app — teal-green */
  --brand-look: #545AA7;         /* the language (arrowlang -> look) — liberty blue */
  --brand-loom: #C46210;         /* the kernel (src/loom) — alloy orange */
}

/* Back-compat aliases: the satellite pages (login/enroll/guide) grew their own
   token names before the palette was unified. Alias them onto the canonical
   values so their existing rules keep working while the values live in one
   place. (Follow-up: sweep these names out of page rules, then delete these.) */
:root {
  --fg: var(--ink);
  --card: var(--surface);
  --panel: var(--surface2);
  --field: var(--surface);
  --box: var(--surface);
  --hover: var(--surface2);
  --line2: var(--line);
  --line3: var(--line-soft);
  --box-line: var(--line);
  --badge-bg: var(--surface2);
  --bad: var(--false);
  --good: var(--true);
}

/* --------------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink); font-family: var(--mono);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
::selection { background: var(--accent-soft); }
a { color: inherit; text-decoration: none; cursor: pointer; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; }
input, select, textarea { font: inherit; color: inherit; }
.muted { color: var(--muted); }
.faint { color: var(--faint); }
.slab { font-family: var(--slab); font-weight: 620; letter-spacing: -.01em; }

/* ----------------------------------------------------------------- scrollbars
   warm-paper/bronze, theme-aware through the vars above. */
html { scrollbar-width: thin; scrollbar-color: var(--accent-line) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background-color: var(--accent-line); border-radius: 6px;
  border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background-color: var(--accent); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ------------------------------------------------------------------ wordmark
   The noo logo (the smallest possible graph: n + o—o). Proportion locked 1:1 —
   an o's diameter equals the n's type size (circle r=11 <-> font-size 22px).
   `r` is an SVG attribute, not CSS, because the slingshot/drag code reads
   getAttribute('r') for hit-testing — keep it authoritative in the markup. */
.nodemark { height: 30px; width: auto; overflow: visible; display: inline-block;
  touch-action: none; -webkit-user-select: none; user-select: none; }
.nm-txt { fill: var(--ink); font-family: var(--slab); font-weight: 640; font-size: 22px; }
.nm-node { fill: var(--node); }
.nm-edge { stroke: var(--net-edge); stroke-width: 1.6; }
.nm-arrow { fill: var(--net-edge); }

/* Inline written-out name — a REAL "n" (inherits the surrounding font + size)
   followed by the two o's drawn as nodes joined by a map-style edge. Same 1:1
   lock as the big mark (o diameter == the n's type size, sized in em). */
.noo { white-space: nowrap; }
.noo-oo { height: 1.25em; width: auto; vertical-align: -.22em; overflow: visible; margin-left: .08em; }
.noo-oo .nm-node { cursor: inherit; }
.noo-oo .nm-edge { stroke: currentColor; stroke-width: 1.6; opacity: .55; }
.noo-oo .nm-arrow { fill: currentColor; opacity: .55; }

/* The look wordmark (the language, ex-arrowlang): l + o—o + k. Same glyph and
   1:1 lock as .noo-oo, sitting BETWEEN letters (margin both sides); the nodes
   take the language's own o-o color (--brand-look), not the app teal. */
.look { white-space: nowrap; }
.look-oo { height: 1.25em; width: auto; vertical-align: -.22em; overflow: visible; margin: 0 .08em; }
.look-oo .nm-node { fill: var(--brand-look); cursor: inherit; }
.look-oo .nm-edge { stroke: currentColor; stroke-width: 1.6; opacity: .55; }
.look-oo .nm-arrow { fill: currentColor; opacity: .55; }

/* The loom wordmark (the kernel, src/loom): l + o—o + m. Same glyph and 1:1 lock
   as .look-oo, sitting BETWEEN letters (margin both sides); the nodes take the
   kernel's own o-o color (--brand-loom, alloy orange), not the app teal. */
.loom { white-space: nowrap; }
.loom-oo { height: 1.25em; width: auto; vertical-align: -.22em; overflow: visible; margin: 0 .08em; }
.loom-oo .nm-node { fill: var(--brand-loom); cursor: inherit; }
.loom-oo .nm-edge { stroke: currentColor; stroke-width: 1.6; opacity: .55; }
.loom-oo .nm-arrow { fill: currentColor; opacity: .55; }

/* --------------------------------------------------------------- header frame
   The shared top bar: a glass strip, wordmark on the left, page-specific
   components filling the slot to its right. Styled after the landing header
   (the canonical treatment). Sticky so it pins without hiding content; a page
   with a full-bleed hero (landing) can promote it to fixed locally. */
.topbar {
  position: sticky; top: 0; z-index: 80;
  display: flex; align-items: center; gap: 16px; height: 58px;
  padding: 0 clamp(16px, 4vw, 40px);
  background: color-mix(in oklab, var(--bg) 62%, transparent);
  backdrop-filter: blur(22px) saturate(1.4);
  -webkit-backdrop-filter: blur(22px) saturate(1.4);
  border-bottom: 1px solid var(--line-soft);
}
.topbar .brand { line-height: 1; display: inline-flex; align-items: center; flex: none; text-decoration: none; }
.topbar .brand .nodemark { display: block; }
/* a flexible spacer for slots that push their components to the right */
.topbar .sp { flex: 1 1 auto; }

/* nav links read as clear pill buttons at rest (not bare text) */
.navlink {
  font-size: 12.5px; color: var(--muted); padding: 7px 15px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface); white-space: nowrap;
  transition: color 140ms var(--ease), background 140ms var(--ease), border-color 140ms var(--ease);
}
.navlink:hover { color: var(--ink); background: var(--surface2); border-color: var(--accent-line); }
/* The app's own header chip (changes / undo / feedback / @handle, and the admin
   console's row). Lives here rather than in a page so that every surface carrying
   the account slot draws the signed-in chip identically to the app's — the point
   of the slot is that you recognise your own header wherever you are. */
.tbtn {
  display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px;
  border-radius: 999px; font-size: 12px; color: var(--muted);
  border: 1px solid transparent; transition: all var(--t-fast) var(--ease);
}
.tbtn:hover { color: var(--ink); background: var(--surface2); }
.tbtn.on { color: var(--accent); background: var(--accent-soft); }
/* Written `a.tbtn` (not `.tbtn`) to outrank a docs page's own prose link styling —
   `a:hover { text-decoration: underline }` — which would otherwise underline the
   chip on hover, where the app's own chip never does. */
a.tbtn, a.tbtn:hover { text-decoration: none; }
/* Sign in — the accent-bordered CTA pill */
.signin {
  color: var(--accent); border: 1px solid var(--accent-line); border-radius: 999px;
  padding: 7px 16px; font-size: 12.5px; font-weight: 500; white-space: nowrap;
  transition: background 160ms var(--ease), box-shadow 160ms var(--ease);
}
.signin:hover { background: var(--accent-soft); box-shadow: var(--shadow-1); }
/* round icon button (the theme toggle, and any icon-only control) */
.iconbtn {
  width: 34px; height: 34px; border-radius: 999px; display: inline-flex;
  align-items: center; justify-content: center; color: var(--muted); cursor: pointer;
  background: none; border: none; flex: none;
  transition: color 140ms var(--ease), background 140ms var(--ease);
}
.iconbtn:hover { color: var(--ink); background: var(--surface2); }

/* theme toggle: both glyphs live in the DOM; CSS shows the one you'd switch TO. */
.theme-toggle .sun { display: none; }
.theme-toggle .moon { display: inline; }
[data-theme="dark"] .theme-toggle .sun { display: inline; }
[data-theme="dark"] .theme-toggle .moon { display: none; }

/* ---------------------------------------------------------------- Docs shelf
   A pill that reads "Docs ▾" and, on click, DROPS DOWN a menu card beneath
   itself holding the three doc wordmarks (no-o · lo-ok · lo-om) stacked. The
   pill itself only ever wraps the button; the menu is an absolutely-positioned
   card, so the shelf never has to widen the header. The word "docs" is dropped
   from each item: the pill already says it. */
.docsshelf {
  position: relative;
  display: inline-flex; align-items: center; flex: none;
  border: 1px solid var(--line); border-radius: 999px; background: var(--surface);
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.docsshelf:hover { border-color: var(--accent-line); }
.docsshelf.open { border-color: var(--accent-line); background: var(--surface2); }
.docstab {
  font: inherit; font-size: 12.5px; cursor: pointer; color: var(--muted);
  background: none; border: 0; padding: 7px 14px; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 5px;
  transition: color 140ms var(--ease);
}
.docsshelf:hover .docstab, .docsshelf.open .docstab { color: var(--ink); }
.docscaret { font-size: .82em; opacity: .7; transition: transform 240ms var(--ease); }
.docsshelf.open .docscaret { transform: rotate(180deg); }
/* the drop-down: a menu card that appears below the pill, fading/sliding in */
.docsshelf-items {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 90;
  display: flex; flex-direction: column; align-items: stretch; gap: 1px;
  min-width: 100%; padding: 4px;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow-2);
  opacity: 0; transform: translateY(-4px); pointer-events: none;
  transition: opacity 180ms var(--ease), transform 180ms var(--ease);
}
.docsshelf.open .docsshelf-items { opacity: 1; transform: translateY(0); pointer-events: auto; }
.docsshelf-items a {
  display: inline-flex; align-items: center; justify-content: center; padding: 8px 14px;
  border-radius: var(--r-sm); color: var(--muted); text-decoration: none;
  white-space: nowrap; transition: color 140ms var(--ease), background 140ms var(--ease);
}
/* text-decoration:none in the hover rule too — a docs page's own prose
   `a:hover { text-decoration: underline }` must not underline the wordmarks. */
.docsshelf-items a:hover, .docsshelf-items a:focus-visible { color: var(--ink); background: var(--surface2); text-decoration: none; }
.docsshelf-items a[aria-current="page"] { color: var(--ink); background: var(--accent-soft); }
@media (prefers-reduced-motion: reduce) {
  .docsshelf-items, .docscaret { transition: none; }
}
