/* The app shell — every page, both tiers (web-server-guide § The site). Same-origin,
   no @import, no external fonts — CSP-clean. This is the site's only stylesheet:
   the auth pages load it too, so the signed-out and signed-in layouts are the same
   layout. Caddy serves it from /assets before forward_auth, so it needs no session.
   Dark only: there is no light palette and no theme toggle — the dark ground is the
   design, not a preference.
   The four regions fill the viewport exactly; only the panes scroll. */

:root {
  --bg: #0f1115;
  --surface: #171a21;
  --surface-hi: #1f2430;
  --border: #2a2f3a;
  --text: #e5e7eb;
  --muted: #9ca3af;
  --accent: #f97316;
  --accent-ink: #fed7aa;
  --radius: 10px;
  /* per-theme detail tokens (§2): grid-cell heat + file git status */
  --heat-1: color-mix(in srgb, var(--accent) 18%, var(--bg));
  --heat-2: color-mix(in srgb, var(--accent) 34%, var(--bg));
  --heat-3: color-mix(in srgb, var(--accent) 50%, var(--bg));
  --heat-4: color-mix(in srgb, var(--accent) 68%, var(--bg));
  --heat-5: color-mix(in srgb, var(--accent) 88%, var(--bg));
  --heat-ink: #1f2937;
  --git-modified: #fbbf24;
  --git-staged: #34d399;
  --git-untracked: #a78bfa;
  /* tag facets (problem page): brand orange · a sky · the untracked violet. */
  --f-domain: var(--accent);
  --f-technique: #56c7f5;
  --f-takeaway: var(--git-untracked);
  /* Semantic ink, separate from the accent: the auth flashes, the vault/tool
     state pills, and the destructive verbs. --ok doubles as git-staged's green
     because "good" is one idea, not two. */
  --danger: #f87171;
  --ok: var(--git-staged);
  --error-bg: #3a1f22;
  --error-fg: #fca5a5;
  --notice-bg: color-mix(in srgb, var(--accent) 14%, transparent);
  --notice-fg: var(--accent-ink);
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
/* Small-type baseline: 13px is the rem root, so every rem/em-sized rule —
   headings, cells, menus, tables, code — scales down with it. */
html { font-size: 13px; }
body {
  font: 1rem/1.55 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
  color: var(--text);
  background: var(--bg);
  /* the whole shell = exactly one viewport: fixed header + panes + fixed footer */
  height: 100vh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
}
a { color: var(--accent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }
.muted { color: var(--muted); }

/* ── Header: brand │ nav │ Actions │ crumbs …… slider │ user (§1) ─────────── */
.app-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.hsep { width: 1px; height: 1.4rem; background: var(--border); flex: none; }
.brand { display: inline-flex; align-items: center; gap: 0.5rem; text-decoration: none; color: var(--text); }
.brand-glyph { width: 1.7rem; height: 1.15rem; color: var(--accent); }
.brand-name { font-weight: 700; letter-spacing: 0.02em; }
.brand-name sup { font-size: 0.68em; }
.app-nav { display: flex; gap: 0.25rem; align-items: center; }
.app-nav a {
  padding: 0.3rem 0.65rem;
  border-radius: 8px;
  text-decoration: none;
  color: var(--muted);
}
.app-nav a:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.app-nav span.is-locked { padding: 0.3rem 0.65rem; border-radius: 8px; color: var(--muted); }
.app-who { margin-left: auto; display: flex; align-items: center; gap: 0.75rem; }

/* Signed out, the header is present and inert: the visitor sees the shape of the
   place before being let into it. Colour alone would read as "low priority" —
   the opacity reads as "not yours yet". Applies to the nav items, Actions, and
   the start page's cards alike; it never applies to the brand or the user pill,
   which are the two controls that DO work signed out. */
.is-locked { opacity: 0.55; cursor: default; user-select: none; }
.is-locked:hover { background: none; color: var(--muted); }

/* The pane's back arrow (site.js): the page #content showed before this one — not
   the browser's history, which would reload the document and the terminal with it.
   `is-first` = nothing behind it yet (it points at the current page); it stays
   clickable, so the arrow never becomes a control that does nothing on click. */
.nav-back, .nav-refresh {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem;
  border-radius: 8px;
  color: var(--muted);
  text-decoration: none;
}
.nav-back svg, .nav-refresh svg { width: 1.05rem; height: 1.05rem; display: block; }
.nav-back:hover, .nav-refresh:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.nav-back.is-first { color: var(--border); }
.nav-back.is-first:hover { color: var(--muted); background: none; }

/* Bookmarks (_bookmarks.html) is the third mark in that row, so it takes the icon
   button's padding and glyph size over the .menu chassis' label padding — three marks
   of one size read as one control group, where a wider third would read as a fourth
   thing that happens to sit nearby. Everything else about it is the standard menu. */
summary.nav-bookmarks { display: inline-flex; align-items: center; padding: 0.25rem; }
summary.nav-bookmarks svg { width: 1.05rem; height: 1.05rem; display: block; }
/* Off-site rows keep the menu's own type size — `.ext` is sized for a link sitting in
   running prose (the problem page's sources), and a menu whose items were 0.85rem would
   be the one dropdown in the header set in small type. The ↗ stays: it is the marker
   that says a click leaves, and it is the reason these are bookmarks and not nav. */
.bookmarks-panel a.ext { font-size: inherit; border-bottom: 0; }
/* Three groups in one panel, so the labels have to read as headings rather than as the
   single info row `.menu-info` was cut for: no rule under each, mono and quiet like the
   hero's kicker, and a little air above the second and third. */
.bookmarks-panel .menu-info {
  padding: 0.35rem 0.7rem 0.2rem;
  border-bottom: 0;
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.bookmarks-panel .menu-info ~ .menu-info { margin-top: 0.35rem; border-top: 1px solid var(--border); }

/* breadcrumbs */
.crumbs { display: flex; align-items: center; gap: 0.35rem; overflow: hidden; min-width: 0; }
.crumbs a { text-decoration: none; color: var(--muted); }
.crumbs a:hover { color: var(--accent); }
.crumb-sep { color: var(--border); }
.crumb-leaf { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.crumb-home { width: 1.05rem; height: 1.05rem; vertical-align: -0.15em; display: inline-block; }

/* dropdown menus (Actions, user) — native <details>, closed by site.js */
details.menu { position: relative; }
details.menu > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.3rem 0.65rem;
  border-radius: 8px;
  color: var(--muted);
  user-select: none;
}
details.menu > summary::-webkit-details-marker { display: none; }
details.menu[open] > summary,
details.menu > summary:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 12%, transparent); }
/* Actions is always present; when the page has no verbs it reads muted. */
.actions-menu.is-empty > summary { color: var(--border); }
.actions-menu.is-empty > summary:hover { background: none; color: var(--muted); }
.menu-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  min-width: 13rem;
  margin: 0;
  padding: 0.35rem;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.menu-right { left: auto; right: 0; }
.menu-list li { margin: 0; }
.menu-list a, .menu-list button {
  display: block;
  width: 100%;
  padding: 0.4rem 0.7rem;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu-list a:hover, .menu-list button:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.menu-list form { margin: 0; }
.menu-info { padding: 0.4rem 0.7rem 0.5rem; color: var(--muted); font-size: 0.85rem;
             border-bottom: 1px solid var(--border); margin-bottom: 0.3rem; }
/* The group separator inside a panel (`<li class="menu-sep">`): the readout at the top
   of a menu is `.menu-info` and rules itself off; this is the divider BETWEEN groups of
   verbs further down — the terminal chip's session/window pair, and the git and message
   menus' runs of commands. It is an empty li, so it has to be given its own height:
   without one it collapsed to nothing, and three panels had been asking for a line that
   was never drawn. It spans the panel's inner box — the same width as the rule under
   `.menu-info`, because two hairlines of different lengths in one panel read as a
   mistake rather than as two kinds of division. */
.menu-list .menu-sep { height: 1px; margin: 0.35rem 0; background: var(--border); }
/* …except straight after the readout, which already rules itself off: that pair draws
   two hairlines a few pixels apart with nothing between them. Both the git and the
   message panel open exactly that way — readout, then the sep that starts the verbs —
   so the suppression lives here rather than being remembered in each template. */
.menu-list .menu-info + .menu-sep { display: none; }

/* the user glyph: initial in a circle */
summary.user-glyph {
  width: 2rem; height: 2rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent-ink);
}
summary.user-glyph svg { width: 1.15rem; height: 1.15rem; display: block; }
/* Signed out the pill is the only live control in the header, so it must not
   read as disabled — but it must read as empty: dashed ring, muted ink. */
summary.user-glyph.is-anon { border-style: dashed; background: none; color: var(--muted); }
summary.user-glyph.is-anon:hover { color: var(--accent); border-color: var(--accent); }

/* ── the terminal chip (_nav.html) ───────────────────────────────────────────
   A readout, not a control: the acts live on the terminal's own titlebar, and this
   says where the session stands when that titlebar is hidden away. First in
   `.app-who`, before the git chip — both are state about YOUR side of the app, and
   the terminal is the one that is a live connection. Same .dot idiom every terminal
   control wears, so the colour means one thing site-wide.

   It reports BOTH of the terminal's independent states: the session in the dot
   (on/off), and the layout as a slash struck through the glyph while the window is
   hidden to the footer — crossed-out is what hidden looks like everywhere, and it
   costs the header no width. site.js paints both, and the tooltip spells them out. */
.term-chip { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); }
.term-chip.is-on { color: var(--text); }
.term-glyph { width: 0.95rem; height: 0.95rem; display: block; flex: none; }
.term-chip .dot { flex: none; }
.term-slash { display: none; }
.term-chip.is-hidden .term-slash { display: inline; }

/* The chip's panel: one row per state, each offering the act that changes it.
   Hide and show are two single-purpose controls in site.js — deliberately, so neither can
   lie about a state it did not read — and this is what makes the pair READ as one row:
   whichever does not apply is not there. The default is visible, so the show row hides
   until body.ws-hidden says otherwise. */
.term-panel { min-width: 14rem; }
.term-only-hidden { display: none; }
body.ws-hidden .term-only-hidden { display: block; }
body.ws-hidden .term-only-visible { display: none; }

/* ── the message chip (_msg.html) ────────────────────────────────────────────
   Between the terminal and git chips in `.app-who`: all three report state about YOU,
   and this is the only one another person can change.

   QUIET WHEN EMPTY, like the git chip: no unread means a muted envelope and no digit.
   Every mark that appears is something to act on, which is what makes the header worth
   a glance instead of wallpaper. */
.msg-chip { display: inline-flex; align-items: center; color: var(--muted); padding-inline: 0.5rem; }
/* The unread marker sits on the SUMMARY, not on the <details>: the summary is what the
   chip's fetch swaps in (`_msg_menu.html`), and the element above it must survive that
   swap untouched — it carries the open state and the htmx triggers. */
.msg-menu > summary.is-unread { color: var(--text); }
.msg-glyph { width: 1rem; height: 1rem; display: block; flex: none; }
/* The count is the badge: accent-filled, so an unread thread is the one warm mark in
   an otherwise muted header. Tabular digits keep the chip from twitching at 9 → 10. */
.msg-count {
  font-size: 0.7rem; font-weight: 600; font-variant-numeric: tabular-nums;
  min-width: 1.15rem; text-align: center;
  padding: 0.05rem 0.3rem; border-radius: 999px;
  color: var(--accent-ink); background: color-mix(in srgb, var(--accent) 26%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}

/* The chip's own menu: a summary line, then up to five threads, then the verbs. There
   is no messages page — a row types `msg read <id>` into the shell and the thread prints
   there — so this panel is the whole browser-side surface and it stays inside the shared
   `.menu-list` chassis rather than growing a layout of its own. */
.msg-menu > summary { display: inline-flex; align-items: center; gap: 0.35rem; padding-inline: 0.5rem; }
.msg-panel { min-width: 20rem; }
.msg-summary { white-space: nowrap; }
/* A row is a verb button like the others, but with four columns instead of a label:
   the dot holds its place when read, so the list does not shift sideways as you work
   down it. The subject takes the slack and ellipsises; nothing else may wrap. */
.msg-row button {
  display: grid; grid-template-columns: 0.45rem auto 1fr auto auto; align-items: baseline;
  gap: 0.45rem; text-align: left;
}
.msg-row .msg-dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: transparent; }
.msg-row.is-unread .msg-dot { background: var(--accent); }
.msg-row .msg-from { color: var(--muted); font-size: 0.8rem; max-width: 8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-row .msg-subject { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-row.is-unread .msg-subject { color: var(--text); font-weight: 600; }
.msg-row .msg-when { color: var(--muted); font-size: 0.78rem; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
/* A row whose act is not a plain read names it — `save`, `authorize` — in an accent chip
   between the subject and the timestamp. One button, not two: the row already knows which
   verb applies (msg_api decides), so a second control would only ask the reader to route
   what has already been routed. The chip is absent on ordinary rows, which is what makes it
   worth reading when it is there. */
.msg-row .msg-verb {
  flex: none; padding: 0.05rem 0.4rem; border-radius: 999px;
  font-size: 0.68rem; letter-spacing: 0.03em; color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* ── the git chip (_git.html) ────────────────────────────────────────────────
   Inside `.app-who`, before the user pill: state about YOUR CLONE, so it sits on
   the identity side of the header's gap, not with the page's own controls. Same
   <details class="menu"> chassis as Actions and the pill — one menu idiom here,
   not three — with a summary that carries state instead of a label.

   The chip is QUIET WHEN CLEAN: an in-sync branch with nothing uncommitted is a
   glyph and a name in --muted, no counts and no tick. Every mark that appears is
   something you could act on, which is what makes it worth a glance rather than
   wallpaper. */
.git-menu > summary { display: inline-flex; align-items: center; gap: 0.4rem; padding-inline: 0.5rem; }
.git-glyph { width: 1rem; height: 1rem; display: block; flex: none; }
.git-branch {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.82rem;
  max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The state classes sit on the SUMMARY, not on the <details>: the chip refreshes by
   swapping its own contents (_git.html), which cannot reach the outer element's
   attributes — a class left up there would freeze at whatever the page load read. */
.git-menu > summary.is-clean { color: var(--muted); }
.git-menu > summary.is-dirty { color: var(--text); }
/* Ahead/behind are the SHAPE of the divergence, so they stay two glyphs with
   tabular digits, never one blended number. Ahead is --git-staged: work that is
   yours and ready to go. Behind is the accent ink: the pull you owe — attention,
   not alarm. */
.git-counts { display: inline-flex; align-items: center; gap: 0.3rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.git-count { display: inline-flex; align-items: center; gap: 0.1rem; }
.git-count.ahead { color: var(--git-staged); }
.git-count.behind { color: var(--accent-ink); }
.git-count svg { width: 0.7rem; height: 0.7rem; }
/* The dirty mark, in the same --git-modified amber the file tree already means
   "modified" with, so the colour says one thing site-wide. A RING, not a disc: a
   filled dot two controls from the user menu's terminal dot would read as the
   same kind of state, and it is not. */
.git-dirty { width: 0.5rem; height: 0.5rem; border-radius: 50%;
  border: 1.5px solid var(--git-modified); flex: none; }
/* Worktree not yet readable (a just-logged-in vault still locked): a muted hollow
   dot, not the amber ring — "don't know yet", not "dirty". The auto-unlock refreshes
   it away a beat later. */
.git-pending { width: 0.5rem; height: 0.5rem; border-radius: 50%;
  border: 1.5px solid var(--muted); flex: none; }
/* Signed out, or a tier with no clone to read: inert, never absent — the header shows
   the shape of the place either way, and a chip that says nothing is honest where one
   that says "clean" would not be. */
.git-inert { display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.3rem 0.5rem; color: var(--muted); }
/* A clone that IS there but would not answer: a live menu (it still carries the verbs
   that diagnose it) wearing a branch name that reads as absent rather than as a name. */
.git-menu > summary.is-unknown .git-branch { color: var(--border); }
.git-panel { min-width: 20rem; }
/* The file counts in their own inks. A zero is dropped by the template, not greyed
   here: the line lists what IS, so it stays short. */
.git-stat { display: flex; gap: 0.9rem; padding: 0.15rem 0.7rem 0.55rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem; font-variant-numeric: tabular-nums; }
.git-stat span { display: inline-flex; align-items: center; gap: 0.3rem; }
.git-stat .n { color: var(--text); }
.git-stat .modified .n { color: var(--git-modified); }
.git-stat .staged .n { color: var(--git-staged); }
.git-stat .untracked .n { color: var(--git-untracked); }
.git-stat .k { color: var(--muted); }
/* Locked private solutions: not a verb (the chip has no self-service unlock — access
   is granted on /account, § _git.html), a guiding line that leads there. */
.git-locked-hint { padding: 0.15rem 0.7rem 0.5rem; color: var(--muted); font-size: 0.82rem; }
.git-locked-hint a { color: var(--accent); text-decoration: none; white-space: nowrap; }
.git-locked-hint a:hover { text-decoration: underline; }
.git-track { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem; color: var(--muted); }
.git-track b { color: var(--text); font-weight: 500; }
/* Every verb types its command into the shell, so every verb names it — in the
   terminal's own mono, one pane to the right. The menu is a way INTO the shell,
   not around it: click Sync today, type `git-sync` next week.
   The row is a flex pair, not a float: a floated command drops to its own line as
   soon as label + command outgrow the panel, and rows of two different heights read
   as a rendering accident rather than a list. Flex keeps one row per verb and lets
   the label take the slack. */
.git-panel .verb button {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1.25rem;
}
.menu-list .cmd { color: var(--muted); flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.75rem; }
/* A verb above your floor is shown, not hidden (the ladder is part of what the page
   teaches, and a verb that vanishes makes two collaborators' screens differ
   silently). The margin names the profile it wants. */
.menu-list li.is-locked button { color: var(--muted); cursor: default; }
.menu-list li.is-locked button:hover { background: none; }
/* The margin still names the profile the verb wants, so it has to be READ: the
   dimmed label already says "not yours", and a hint at --border on --surface-hi is
   not a subtle hint, it is an invisible one. The row reads locked because both
   halves are muted, not because half of it is unreadable. */
.menu-list li.is-locked .cmd { color: var(--muted); }
/* Terminal down: the verbs have nowhere to land (terminal.js's send() drops the
   frame on a closed socket), so site.js paints .term-offline from the same
   `termConnected` every other control reads. Without this a click would silently
   do nothing — the one outcome a control must never have.

   Named for the TERMINAL, not for whichever menu was first to need it: any menu whose
   verbs type commands carries `.term-menu` and gets this for free (the git chip and the
   message chip today). The rule this replaced was `.git-*`, which meant the second such
   menu had to edit the JS to be included. */
.term-need { display: none; margin: 0.25rem 0.35rem 0.1rem; padding: 0.45rem 0.55rem;
  border: 1px dashed var(--border); border-radius: 6px; color: var(--muted); font-size: 0.82rem; }
.term-offline .term-need { display: block; }
.term-need button { display: inline; width: auto; padding: 0; margin-top: 0.15rem;
  border: 0; background: none; color: var(--accent); font: inherit; cursor: pointer;
  text-decoration: underline; }
.term-need button:hover { background: none; color: var(--accent-ink); }
.term-offline .verb button { color: var(--muted); cursor: default; }
.term-offline .verb button:hover { background: none; }

/* Terminal state, read at a glance inside the user menu. The dot is the state;
   the verb on the item is the act it offers (§ user pill). */
.menu-state { display: inline-flex; align-items: center; gap: 0.35rem; float: right; }
.dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--muted); }
.dot.on { background: var(--ok); }
.dot.off { background: var(--danger); }

/* The account onboarding checklist (F9): live next steps, computed from the credential
   signals already in the panel. Reuses the .dot on/off idiom for done/pending. */
.onboard { margin: 0 0 1.4rem; }
.onboard-head { display: flex; align-items: baseline; gap: 0.7rem; margin-bottom: 0.6rem; }
.onboard-title { font-size: 1.12rem; font-weight: 600; color: var(--text); }
.onboard-count { font-size: 0.78rem; color: var(--muted); }
.onboard-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.onboard-step { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.onboard-step .dot { flex: none; align-self: center; }
.onboard-label { color: var(--text); }
.onboard-step.is-done .onboard-label { color: var(--muted); }
.onboard-hint { font-size: 0.82rem; }
.onboard-hint code { color: var(--accent); }
.onboard-note { margin: 0.7rem 0 0; font-size: 0.82rem; }

.pill {
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.12rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* ── The two equal panes (§1): independent scroll containers ─────────────── */
.app-panes {
  display: grid;
  grid-template-columns: 1fr 1fr;            /* equal width */
  gap: 0.8rem;
  padding: 0.8rem 1rem;
  min-height: 0;                             /* let the panes, not the page, scroll */
}
.pane { min-width: 0; min-height: 0; }
/* Both axes when content overflows — and the vertical gutter is reserved whether or not
   a scrollbar is in it. Without that, every page that grows past the pane (or shrinks
   back under it, as the filter narrows a grid) takes the scrollbar's width away from the
   content and re-flows it: the tiles shift sideways, and a grid that packs by available
   width can drop a column, under a reader who only typed a letter. The gutter costs the
   same handful of pixels on every page; the jump costs them their place. */
.pane-content { overflow: auto; scrollbar-gutter: stable; }
/* The terminal pane is a little window: a titlebar over an iframe that scrolls itself.
   The pane owns the frame (border, radius, fill) and clips the iframe's corners. */
.pane-ws {
  overflow: hidden;
  display: flex; flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.terminal-frame { flex: 1; width: 100%; border: 0; background: var(--surface); }
/* The titlebar: the terminal's name and, opposite it, the hide button. */
.ws-bar {
  flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.2rem 0.35rem 0.2rem 0.7rem;
  border-bottom: 1px solid var(--border);
}
.ws-bar-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem; letter-spacing: 0.04em; color: var(--muted);
}
/* The window's own controls, in titlebar order: session (connect/disconnect), then
   layout (hide). */
.ws-bar-controls { display: inline-flex; align-items: center; gap: 0.25rem; }
/* Connect/disconnect lives on the terminal it acts on, and it is just the dot: state
   and act in one mark. No verb beside it — the titlebar is 1.6rem tall and a word there
   competes with the terminal's own name for a control that has exactly two states a
   colour already tells apart. What it will DO is its title (§ base.html), which site.js
   flips with the state. Sized to match the hide button next to it. */
.ws-conn {
  width: 1.6rem; height: 1.6rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 6px;
  background: none; cursor: pointer;
}
.ws-conn:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.ws-conn .dot { flex: none; }

/* ── Hide the terminal to the footer: layout only, socket untouched ─────────── */
/* Hidden: the panes become a single column and the terminal pane is not rendered, so
   #content fills the width; the footer's hidden window appears. */
body.ws-hidden .app-panes { grid-template-columns: 1fr; }
body.ws-hidden .pane-ws { display: none; }
/* The hide button in the titlebar. */
.ws-hide {
  width: 1.6rem; height: 1.6rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 6px;
  background: none; color: var(--muted); cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.ws-hide:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.ws-hide svg { width: 1rem; height: 1rem; }
/* The hidden terminal, in the footer's right-hand slot: a window collapsed to its
   titlebar — window figure · name in the terminal's mono · show glyph — so what it
   is and what clicking does are the same picture. It is not a link and not a strip of
   text: the pane it stands for is a window, and this is that window, small.

   It is the last thing in the footer's right-aligned row and takes no space until it is
   there: `display: none` while the terminal is visible, so nothing is reserved and the
   documents hold the right edge on their own. */
.ws-show {
  display: none; align-items: center; gap: 0.4rem;
  padding: 0.12rem 0.3rem 0.12rem 0.4rem;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--surface-hi); color: var(--muted); cursor: pointer;
  font: inherit; font-size: 0.78rem;
}
body.ws-hidden .ws-show { display: inline-flex; }
.ws-show:hover { color: var(--accent); border-color: var(--accent); }
/* The window figure: a box whose top edge is its titlebar. */
.ws-show-figure {
  width: 0.8rem; height: 0.62rem; flex: none;
  border: 1px solid currentColor; border-top-width: 0.2rem; border-radius: 2px;
}
.ws-show-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem; letter-spacing: 0.03em;
}
.ws-show-glyph { display: inline-flex; }
.ws-show-glyph svg { width: 0.9rem; height: 0.9rem; display: block; }

/* /terminal — the iframe's own document: the terminal fills it edge to edge
   (the pane's own border and radius come from .pane-ws in the shell). */
body.terminal-doc {
  display: block;
  height: 100vh;
  overflow: hidden;
}

/* ── Panels & shared content styling ─────────────────────────────────────── */
.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.3rem 1.5rem;
  margin-bottom: 0.8rem;
}
.panel h1 { margin: 0 0 0.5rem; font-size: 1.35rem; }
.panel h2 { margin: 0 0 0.6rem; font-size: 1.12rem; }

/* Fold: a panel section that opens in place (the account page's Password form). The
   summary carries the heading, so it reads as a panel title until clicked; the ▸
   marker rotates on open. Closed by default (no [open] on the <details>). */
.fold > summary.fold-summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}
.fold > summary.fold-summary::-webkit-details-marker { display: none; }
.fold > summary.fold-summary::before {
  content: "\203A";  /* › — rotates to ⌄ when open */
  color: var(--muted);
  transition: transform 0.12s ease;
  transform: translateY(-0.05em);
}
.fold[open] > summary.fold-summary::before { transform: rotate(90deg) translateX(-0.05em); }
.fold .fold-title { font-size: 1.12rem; font-weight: 600; color: var(--text); }
.fold[open] > summary.fold-summary { margin-bottom: 0.8rem; }

/* A topic article's generated Problems list, folded closed (click to expand). The
   heading doubles as the fold summary, so its default margins are dropped; the count
   rides alongside as a muted pill so the summary says how many without opening. */
.topic-problems { margin: 1.2rem 0; }
.topic-problems > summary .fold-title { margin: 0; }
.topic-problems .problems-count {
  margin-left: 0.5rem;
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 500;
  white-space: nowrap;
  color: var(--muted);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.topic-problems[open] > summary .problems-count { color: var(--text); }
.lead { font-size: 1rem; }
.meta { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.pill-solved { color: var(--accent-ink); }

/* Off-site source links (problem page: projecteuler.net · github). The ↗ says
   "this leaves the site" — site.js has already made every such link open in a
   new tab, and the marker is what tells the reader so before they click. */
.meta .ext { margin-left: 0.35rem; }
.ext { font-size: 0.85rem; text-decoration: none; border-bottom: 1px solid transparent; }
.ext::after { content: ' ↗'; font-size: 0.8em; opacity: 0.75; }
.ext:hover { border-bottom-color: currentColor; }

/* hero + card grid — the landing / docs / topics visual system (§3/§7) */
.hero { margin: 0.4rem 0 1rem; }
.hero .kicker {
  margin: 0 0 0.3rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
.hero h1 { margin: 0 0 0.4rem; font-size: 1.6rem; letter-spacing: -0.01em; }
.hero .lede { margin: 0; color: var(--muted); }
/* ── The tile (§7) ────────────────────────────────────────────────────────────
   ONE object, four pages: home, docs, topics, and the head of every century grid
   on solutions. Four to a row, and inside each tile the same three slots:

     glyph · title · state  the mono mark, the name, and the corner slot — ONE line
     supporting line        ONE line about it

   Two lines, because that is what the tile has to say: what it is, and one fact about
   it. The glyph sits with the name it marks rather than on a line of its own, so a
   16-tile grid is 16 short blocks rather than a column of floating glyphs.

   The supporting line is the rule that makes the four pages one system: **mono
   when it is a number, body face when it is a sentence**. Topics and centuries
   carry a count (`95 of 240 solved`); home and docs carry prose. Nothing carries
   both — a tile says one thing about itself.

   The corner slot holds AFFORDANCE or STATE, never decoration: the arrow fades in
   on hover/focus (sixteen resting grey arrows in a grid this dense is noise, and a
   bordered box on a card grid already reads as clickable), while the padlock
   (signed out) and the `final` pill are state and stay put. */
.cards {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.6rem;
  /* A little right padding so the rightmost tile's hover (a 2px lift + the accent
     border) and its keyboard focus ring (a 2px outline at 2px offset) are not clipped
     by the content pane's overflow edge. Left stays flush, so the grid still lines up
     with the hero above it. */
  padding-right: 0.5rem;
}
/* The breakpoints are the VIEWPORT's, but the grid lives in a half-viewport pane, so
   they are set by what the pane holds with the terminal open — the normal case. Below
   ~150px a tile truncates its own title, so it drops to two, then to one. */
@media (max-width: 1200px) { .cards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 700px) { .cards { grid-template-columns: 1fr; } }
.card {
  position: relative;                 /* the progress hairline pins to its bottom edge */
  display: flex;
  flex-direction: column;
  padding: 0.7rem 0.75rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  overflow: hidden;                   /* the hairline follows the rounded corners */
  transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease;
}
.card:hover { border-color: var(--accent); background: var(--surface-hi); transform: translateY(-2px); }
/* The tile's first line: glyph · title · corner slot. `flex-start` rather than centre,
   so the glyph and the corner mark stay on the title's FIRST line when a long name takes
   a second one. */
.card-head { display: flex; align-items: flex-start; gap: 0.45rem; }
.card-ic {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.05rem; line-height: 1.3; color: var(--accent);
}
.card-title {
  flex: 1; min-width: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  font-weight: 600; line-height: 1.3; color: var(--text);
}
/* The data line: mono, because it is data (§ the tile). `margin-top: auto` drops it to
   the tile's foot, so every count in a row sits on the same baseline directly above its
   hairline however many lines the titles above them took. */
.card-meta {
  display: block; margin-top: auto; padding-top: 0.3rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem; letter-spacing: 0.06em; color: var(--muted);
}
.card-meta b { color: var(--text); font-weight: 600; }
/* The prose line: the page's own words, in the body face, clamped so a long one
   cannot make its tile taller than the row. */
.card-desc {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  margin-top: 0.15rem; font-size: 0.74rem; line-height: 1.35; color: var(--muted);
}
.card-desc code { color: var(--accent); }
/* The progress hairline, welded to the tile's bottom edge and filled to the fraction
   solved. It appears ONLY where a fraction is a real fact about the thing — a topic's
   problems, a century's hundred — so its absence on a guide or the start page's tiles
   says "not a progress thing" rather than "zero". The width is the one inline style in
   the app: it is the datum itself, and there is no class-per-percent worth having. */
.card-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--border); }
.card-bar i { display: block; height: 100%; background: var(--accent); }
.card-go {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem; line-height: 1.65; color: var(--accent);
  opacity: 0; transition: opacity 0.15s ease;
}
.card:hover .card-go, .card:focus-visible .card-go { opacity: 1; }
/* A draft is a page that exists but is not written yet — readable, not advertised. */
.card.is-draft .card-ic, .card.is-draft .card-title { color: var(--muted); }
.card .pill-final { flex: none; font-size: 0.6rem; padding: 0.05rem 0.4rem; }
@media (prefers-reduced-motion: reduce) {
  .card, .card:hover { transition: none; transform: none; }
  .card-go { transition: none; }
}
/* A locked card keeps its box and its copy — the visitor should see exactly what
   is behind the login — and puts a padlock in the corner slot. It is a <span>, not
   an <a>, so the element-typed hover rules above never reach it. */
.card.is-locked .card-ic, .card.is-locked .card-title { color: var(--muted); }
.card-lock { flex: none; width: 0.9rem; height: 0.9rem; margin-top: 0.2rem; color: var(--muted); display: block; }

/* ── The section strip (_sections.html) ──────────────────────────────────────
   The same tile, in the QUIET register. Every one of the five index pages opens with
   this row and then shows a grid of its own tiles below it; two grids of the identical
   object would read as one long field of eight. So the strip is drawn flat on the page
   ground — no fill, and no lift on hover — while the page's own tiles keep the surface
   and the lift. Chrome does not move; content does.

   The whole row is quiet except the one tile you are standing on, which is the only
   filled box on the strip and carries an accent edge. Its corner slot holds the
   you-are-here mark in place of the arrow: there is no journey to offer to the page
   already in the pane. */
.cards.sections { margin-bottom: 0.2rem; }
.sections .card { background: transparent; }
.sections .card:hover { background: var(--surface); transform: none; }
.sections .card.is-here {
  background: var(--surface-hi);
  border-color: var(--border);
  /* inset, not a border: a real left border would widen the box by 2px and knock this
     tile out of step with the three beside it. The radius clips it either way. */
  box-shadow: inset 3px 0 0 var(--accent);
}
.sections .card.is-here:hover { background: var(--surface-hi); border-color: var(--border); }
.card-here {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem; line-height: 1.65; color: var(--accent);
}
/* The terminal tile's data line is a live state, not a count: the dot site.js paints
   from the iframe's report, then the word. */
.sections .card-meta .dot { margin-right: 0.4rem; vertical-align: baseline; }

/* ── The page divider (_divider.html) ────────────────────────────────────────
   The boundary between the strip and the page's own content, on all five pages. It is
   the `.subrule` device — a small mono label trailed by a hairline — with the one extra
   job this layout needs: the rule runs to the FILTER, and the count at its end is the
   count of what survived. Name what is below, narrow it, say how much is left.

   The count sits in the slot `.subrule` was already using for a number, so a folder
   heading inside the region and the region's own divider read as the same kind of break
   at two levels. */
/* The right-hand end keeps clear of the pane's scrollbar. #content is itself the scroll
   container and carries no padding, so the rule runs flush to the pane edge — where an
   overlay scrollbar lies straight over the count, the one thing on the rule that has to
   stay readable while the reader types. The hairline flexes, so this padding costs the
   rule nothing: it simply moves the filter and the count in off the edge. */
.pagerule {
  display: flex; align-items: center; gap: 0.7rem;
  margin: 1.1rem 0 0.7rem; padding-right: 1.1rem;
}
.pagerule-label {
  margin: 0; white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem; font-weight: 500; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--muted);
}
.pagerule-hair { flex: 1 1 1rem; min-width: 1rem; height: 1px; background: var(--border); }
/* The count is data, so it is mono and quiet; site.js rewrites it to `n / total` while a
   query is live. Fixed minimum width so the rule beside it does not twitch per keystroke. */
.pagerule-count {
  flex: none; min-width: 4ch; text-align: right;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem; letter-spacing: 0.04em; color: var(--muted);
}
/* Where there is nothing below the rule to narrow (the start page), the filter's place
   is taken by the fact, not by a dead input: a greyed box invites a click that does
   nothing, and a statement does not. */
.pagerule-note {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem; letter-spacing: 0.06em; color: var(--muted);
}
/* The filter, styled to the shell in the other pane: mono, dark ground, sized to sit on
   the rule rather than to fill it. It shrinks before the hairline does on a narrow pane. */
.pane-filter {
  flex: 0 1 16rem; min-width: 5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem; color: var(--text);
  background: var(--bg);
  /* A hairline warmed towards the accent rather than the plain --border the rule beside
     it wears: the box is the one thing on this rule you can type into, and at rest it
     should look it. Well short of the focus state, which takes the accent outright. */
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
  border-radius: 6px;
  padding: 0.28rem 0.6rem;
}
.pane-filter:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.pane-filter::placeholder { color: var(--muted); }
.pane-filter:focus { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 700px) { .pagerule { flex-wrap: wrap; } .pane-filter { flex: 1 1 8rem; } }

/* ── The README, below the strip on the start page ───────────────────────── */
/* The divider above it already opens the section, so the block starts flush. */
.readme { margin-top: 0; }
/* The problem page's workbench eyebrow — an eyebrow, not a heading: the workbench is
   the same page continuing, not a new section competing with the hero. The five index
   pages use the fuller `.pagerule` for the same job, since theirs carries a filter. */
.workbench-eyebrow {
  display: flex; align-items: center; gap: 0.7rem;
  margin: 0 0 0.9rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted);
}
.workbench-eyebrow::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}
/* The "full README →" link closing the summary — a quiet footer, not a button. */
.readme-more { margin: 1rem 0 0; font-size: 0.9rem; }
.readme-more a { color: var(--accent); text-decoration: none; font-weight: 500; }
.readme-more a:hover { text-decoration: underline; }

/* ── Solutions century grids: square, as many per row as fit (§7) ────────── */
/* auto-fill packs as many ~17rem tracks as the pane allows — ~3–4 on a normal
   window, fewer when narrow, more when wide (the old-web-server flex-wrap,
   in grid form). Each century is square because its 10 columns are equal and
   the cells carry aspect-ratio: 1. */
.grid-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: 1.1rem 1.2rem;
  justify-content: center;
  align-items: start;
}
.century { margin: 0; min-width: 0; }
/* Each century is HEADED BY THE TILE (§ the tile): the range where the glyph goes, the
   count on the data line, and the progress hairline on the bottom edge pointing straight
   into the grid it measures. That is what puts this page in the same system as topics —
   a century is a topic you have or have not worked through.

   It is a label, not a link: there is no per-century page, and a tile that looks
   clickable and is not would be the one thing a control must never be. So it keeps the
   chassis and drops the hover — no lift, no border flare, no arrow. */
.century > .card {
  margin: 0 0 0.45rem; padding: 0.55rem 0.7rem 0.6rem;
  font-size: 1rem; font-weight: 400; cursor: default;
}
.century > .card:hover {
  border-color: var(--border); background: var(--surface); transform: none;
}
/* The range is the century's name, so it takes the title's weight in the glyph's slot —
   nowrap because "NNNN–NNNN" is one word however narrow the track gets. */
.century .card-ic { font-size: 0.86rem; letter-spacing: 0.08em; white-space: nowrap; }
.century-grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  grid-template-rows: repeat(10, 1fr);
  aspect-ratio: 1;                            /* the grid box is square: its height
                                                 follows its width, so every grid is
                                                 identical (all widths are equal) —
                                                 the cells fill it via the 1fr rows,
                                                 immune to per-cell rounding drift. */
  gap: 2px;
}
.cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 0.7rem;
  line-height: 1;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  text-decoration: none;
  color: var(--muted);
  background: var(--bg);
}
.cell.empty { border-color: transparent; background: transparent; }
a.cell:hover { border-color: var(--accent); color: var(--text); }
.cell.solved { color: var(--text); }
.cell.heat-1 { background: var(--heat-1); }
.cell.heat-2 { background: var(--heat-2); }
.cell.heat-3 { background: var(--heat-3); }
.cell.heat-4 { background: var(--heat-4); color: var(--heat-ink); }
.cell.heat-5 { background: var(--heat-5); color: var(--heat-ink); }

/* ── Problem page ────────────────────────────────────────────────────────── */
.statement, .notes, .doc-body { overflow-wrap: break-word; }
.statement img, .notes img, .doc-body img { max-width: 100%; }

/* The statement is foreign content: projecteuler.net's own markup, written for their
   light page. Two things in it assume that ground, and both are met here rather than by
   giving the statement a light panel of its own — the dark ground is the design, and a
   bright block in the middle of the page would be the more visible bug.

   1 · EMPHASIS COLOURS. Their `class="red"` spans get the same values
   `content.STATEMENT_COLOURS` maps the LaTeX `\color{…}` and inline `style="color:…"`
   forms onto, so a statement reads the same however it asked. Scoped to `.statement`:
   these are generic words, and a `.red` loose in the sheet would eventually collide. */
.statement .red { color: #ff8781; }
.statement .blue { color: #7cc0ff; }
.statement .green { color: #7ee787; }
.statement .orange { color: #ffb26b; }

/* 2 · TRANSPARENT DIAGRAMS. A see-through image takes the page's ground, and theirs is
   drawn for a white one: dark line art on `--bg` is a black diagram on a near-black
   field. `content.plate_statement_images` reads the pixels and marks the ones that need
   a ground of their own; this gives them one. Not a bare white box — a plate with the
   page's own radius, inset so the art is not flush to the edge, so it reads as a figure
   rather than as a rendering accident.

   Only `ink-dark` is plated. Light ink is what a dark page wants (28 of the cached
   diagrams are drawn that way), and opaque images bring their own background — both are
   left exactly as they were. */
.statement img.ink-dark {
  background: #fbfbfb;
  padding: 0.6rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

/* The reading document: statement, then notes, as one flow. The rule marks where
   the problem ends and the analysis begins — the notes carry their own headings,
   so no "Notes" label is needed. */
.doc-rule { border: 0; height: 1px; background: var(--border); margin: 1.6rem 0; }
.notes h2, .notes h3, .notes h4 { margin: 1.5rem 0 0.5rem; font-size: 1.05rem; }
.notes > :first-child { margin-top: 0; }

/* The workbench: the solving artifacts (test cases, results, files) grouped under
   one eyebrow, set denser than the reading panel above so the page reads as two
   zones — the read, then the work. */
.workbench { margin-top: 1.4rem; }
.workbench .panel { padding: 1rem 1.25rem; }
.workbench .panel h2 { font-size: 1rem; }
/* A problem resource opened in the pane (media.html): fit the pane, keep aspect. */
.resource-img { max-width: 100%; height: auto; display: block; }

/* files flow horizontally, plain names, coloured by git status (§7) */
.file-flow { display: flex; flex-wrap: wrap; gap: 0.35rem 1.3rem; padding: 0; margin: 0; list-style: none; }
.file-flow a { text-decoration: none; color: var(--text); }
.file-flow a:hover { color: var(--accent); }
.file-flow a.git-modified { color: var(--git-modified); }
.file-flow a.git-staged { color: var(--git-staged); }
.file-flow a.git-untracked { color: var(--git-untracked); }

.pre-line { white-space: pre-line; }
.table-scroll { overflow-x: auto; }
table.results, table.test-cases { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
table.results th, table.results td,
table.test-cases th, table.test-cases td {
  border: 1px solid var(--border);
  padding: 0.3rem 0.6rem;
  text-align: left;
}
/* Column headers read as a data readout: mono, quiet, on a faint band. */
table.results th, table.test-cases th {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.76rem; font-weight: 600; letter-spacing: 0.02em; color: var(--muted);
  background: color-mix(in srgb, var(--surface-hi) 55%, transparent);
}
table.results tbody tr:hover, table.test-cases tbody tr:hover {
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
table.results td { white-space: nowrap; }
tr.verdict-correct td:nth-child(4) { color: var(--accent); font-weight: 600; }

.code-block {
  margin: 0;
  padding: 0.9rem 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  font-size: 0.86rem;
  line-height: 1.45;
}

/* ── Docs / topics / about body ──────────────────────────────────────────── */
.doc-body pre {
  padding: 0.8rem 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  font-size: 0.86rem;
}
.doc-body table { border-collapse: collapse; display: block; overflow-x: auto; }
.doc-body th, .doc-body td { border: 1px solid var(--border); padding: 0.3rem 0.6rem; }
.doc-body blockquote {
  margin: 0.8rem 0;
  padding: 0.1rem 1rem;
  border-left: 3px solid var(--accent);
  color: var(--muted);
}
.readme .doc-body h2, .readme .doc-body h3 { margin: 1.4rem 0 0.5rem; font-size: 1.05rem; }
.readme .doc-body > :first-child { margin-top: 0; }
.readme .doc-body img { display: none; }   /* the badges: GitHub furniture, not site content */
/* The thesis line — "The point never is to get an answer but to understand why it is
   the answer." Bold does not carry it: the summary's own opening sentence is bold, and
   the Highlights below it are a column of bold labels, so the one line that is the point
   of the place was set in the same weight as its furniture. It takes the accent instead
   — the orange every kicker, rule and lit tile already uses for "this is the one that
   matters" — and the emphasis is then a colour nothing else in the prose is wearing.

   Keyed on the SHAPE of a pull-quote, not on the words, so it survives a rewrite: the
   README gives the line a paragraph of its own, and `render_markdown` marks a paragraph
   that is nothing but a bold sentence `class="pull"` (it has to be done there — CSS's
   `:only-child` counts elements, so the text after a lead-in bold does not disqualify
   it, and the summary's opening sentence matched too). Scoped to `.readme`, so the
   ~120 `**usage**` labels the guides carry stay as they are.

   Home and the login page render the same `_home.html` from the same packaged
   home-summary.md, so this is one rule for both; site.css is served ahead of
   forward_auth, so the signed-out page gets it too. */
.readme .doc-body p.pull > strong { color: var(--accent); }

/* ── Editors ─────────────────────────────────────────────────────────────── */
.editor-buffer {
  width: 100%;
  min-height: 22rem;
  resize: vertical;
  padding: 0.9rem 1rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.86rem;
  line-height: 1.45;
  tab-size: 4;
  white-space: pre;
}
.editor-buffer:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The edit page fills the pane: a flex column of head · editor (grows) · hint,
   so the editor occupies all remaining height and scrolls internally. */
.editor-fill { display: flex; flex-direction: column; height: 100%; margin-bottom: 0; }
.editor-head { flex: none; }
.editor-fill .editor-form { flex: 1; display: flex; min-height: 0; }
.editor-fill .editor-form .editor-buffer { flex: 1; }   /* no-JS fallback fills */
.editor-fill .cm-host { flex: 1; }
.editor-hint { flex: none; margin: 0.6rem 0 0; font-size: 0.85rem; }

/* CodeMirror mounts here (site.js/editor.js), replacing the textarea on JS.
   .cm-editor is the flex child that fills the host; its own scroller lays the
   gutter beside the content. */
.cm-host {
  min-height: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.cm-host .cm-editor { height: 100%; }
.btn {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 8px;
  padding: 0.35rem 0.9rem;
  font: inherit;
  cursor: pointer;
}
.btn:hover { border-color: var(--accent); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #1f2937; font-weight: 600; }
.btn-primary:hover { filter: brightness(1.08); }
/* A public key is nothing to guard — copying it is always the safe, right act — so
   the Copy button wears the ok green, not the neutral border. */
.btn-ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }
.btn-ok:hover { border-color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, transparent); }
/* A .btn sized to the account panel's action column: the same width and type scale as
   the state-action buttons, so Copy and Save secret line up with Create identity and
   the tool rows in one right-hand column. */
.btn-col { min-width: 9rem; font-size: 0.78rem; }

/* ── The auth dialogue (right pane, signed out) ──────────────────────────── */
/* Same surface, border and radius as the terminal it stands in for: the pane
   does not change character when you sign in, only its occupant. The auth pages
   are standalone public documents on their own routes — they render this shell
   and fill this pane, which is the whole of "one layout, pre and post auth". */
.auth-pane {
  width: 100%; height: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  display: flex; align-items: center; justify-content: center;
  padding: 1.5rem;
  overflow: auto;
}
.auth-card { width: 100%; max-width: 24rem; }
.auth-card h1, .auth-card h2 { margin: 0 0 0.3rem; font-size: 1.25rem; }
.auth-card .lead { margin: 0 0 1.1rem; color: var(--muted); font-size: 0.92rem; }

/* The one form style, for the dialogue and the shell alike (change password
   renders into the left pane). */
.auth-form { display: flex; flex-direction: column; gap: 0.4rem; max-width: 26rem; }
.auth-form label { font-size: 0.9rem; color: var(--muted); margin-top: 0.3rem; }
.auth-form input:not([type="checkbox"]) {
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.7rem;
}
.auth-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.auth-form label.check {
  display: flex; align-items: center; gap: 0.5rem;
  color: var(--text); font-size: 0.95rem; margin: 0.5rem 0 0;
}
.auth-form .btn { margin-top: 0.7rem; align-self: flex-start; }
.auth-form .btn-primary { padding: 0.6rem 1rem; }
.auth-form button:disabled { opacity: 0.6; cursor: wait; }
/* Shared auth-tier links — one style for every signed-out page (forgot, request
   invite, back / go to sign in, the OTP "send a fresh code"). Keycap chips: no
   underline, mono, a button feel that twins with the "show" toggle and the ↵
   keycap on the same cards. Works on <a> and <button> alike. */
.auth-links {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem;
  margin: 1rem 0 0;
}
.auth-links.spread { justify-content: space-between; }
.auth-link {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem; line-height: 1; letter-spacing: 0.02em;
  color: var(--accent); text-decoration: none; cursor: pointer;
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 0.34rem 0.6rem;
}
.auth-link:hover {
  color: var(--accent-ink); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* ── The auth window (_authwin.html) ─────────────────────────────────────── */
/* Every signed-out card dressed as the solver shell it stands in for: terminal
   chrome (a title bar + an optional status bar) wrapping the page's form. Shared
   by sign in, register/reset, forgot and request-invite. It sits in the same
   --surface pane the terminal will, one shade lighter (--surface-hi) with its
   own border, so it reads as the window about to become the shell.
   The mono utility face is a repeated literal here, matching the rest of the
   file (no --mono token exists). */
.authwin {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-hi);
  box-shadow: 0 22px 60px -26px rgba(0, 0, 0, 0.8);
}
.authwin-bar {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.55rem 0.8rem;
  background: color-mix(in srgb, var(--surface) 70%, #000);
  border-bottom: 1px solid var(--border);
}
.authwin-dots { display: flex; gap: 0.35rem; }
.authwin-dot { width: 0.6rem; height: 0.6rem; border-radius: 50%; flex: none; }
.authwin-dot.r { background: #f87171; }
.authwin-dot.y { background: #fbbf24; }
.authwin-dot.g { background: #34d399; }
.authwin-glyph { width: 1.2rem; height: 0.82rem; color: var(--accent); margin-left: 0.4rem; flex: none; }
.authwin-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.74rem; color: var(--muted); letter-spacing: 0.02em;
}
.authwin-title b { color: var(--accent-ink); font-weight: 600; }

.authwin-body { padding: 1.15rem; }
.authwin-intro {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.76rem; color: var(--muted); line-height: 1.55; margin: 0 0 1.1rem;
}
.authwin-intro .arrow { color: var(--accent); }

/* The forms, terminal-styled: mono labels/inputs, source-ordered after .auth-form
   so these overrides win at equal specificity. */
.authwin-form label {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem; letter-spacing: 0.06em; color: var(--muted); margin-top: 0.5rem;
}
.authwin-form input:not([type="checkbox"]),
.authwin-form textarea {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
}
.authwin-form textarea {
  color: var(--text); background: var(--bg);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 0.5rem 0.7rem; resize: vertical;
}
.authwin-form textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.authwin-form label.check {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem; color: var(--text); margin: 0.6rem 0 0;
}

/* Full-width mono submit, with the ↵ hint and the in-flight "proving…" state. */
.authwin-form .btn {
  align-self: stretch; margin-top: 0.85rem;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 600; letter-spacing: 0.02em; padding: 0.68rem 1rem;
}
.authwin-form .kbd {
  font-size: 0.82em; opacity: 0.7; line-height: 1.3;
  border: 1px solid rgba(31, 18, 6, 0.4); border-radius: 4px; padding: 0 0.28rem;
}
.authwin-form .btn.busy {
  background: color-mix(in srgb, var(--accent) 55%, var(--surface));
  color: var(--accent-ink); cursor: progress;
}

/* The status bar: the page's one standing note, full-bleed under the body like a
   shell footer. */
.authwin-status {
  padding: 0.6rem 1.15rem;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.66rem; color: var(--muted); line-height: 1.5;
  display: flex; align-items: baseline; gap: 0.5rem;
}
.authwin-led {
  width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none;
  align-self: center;
  background: var(--accent-ink); box-shadow: 0 0 6px var(--accent);
}
.authwin-status b { color: var(--accent-ink); font-weight: 600; }

/* Password field + show/hide toggle (the toggle rides inside the input's edge). */
.pw-wrap { position: relative; display: flex; }
.pw-wrap input { flex: 1; padding-right: 3.4rem; }
.pw-toggle {
  position: absolute; right: 0.4rem; top: 50%; transform: translateY(-50%);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem; letter-spacing: 0.04em; color: var(--muted);
  background: none; border: 1px solid var(--border); border-radius: 5px;
  padding: 0.12rem 0.4rem; cursor: pointer;
}
.pw-toggle:hover { color: var(--accent-ink); border-color: var(--accent); }

/* Caps-lock warning — hidden until authform.js sees Caps Lock while a field has
   focus. The [hidden] guard beats the display:flex so the attribute still hides it. */
.caps-warn {
  display: flex; align-items: center; gap: 0.35rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.66rem; color: var(--accent-ink); margin: 0.35rem 0 0;
}
.caps-warn::before { content: "\21EA"; }
.caps-warn[hidden] { display: none; }

.flash { border-radius: 8px; padding: 0.6rem 0.8rem; font-size: 0.92rem; margin: 0.5rem 0; }
.flash-error { background: var(--error-bg); color: var(--error-fg); }
.flash-notice { background: var(--notice-bg); color: var(--notice-fg); }

/* The inline Terms of use on the registration page (strong acceptance: the text
   is read on the page it is accepted on) and the standalone /terms rendering. */
.terms-box {
  max-height: 14rem; overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.25rem 0.9rem;
  font-size: 0.9rem;
}
.terms-box a, .terms-text a { color: var(--accent); }
.back-row { margin: 0 0 0.4rem; }
.back-link { color: var(--muted); font-size: 0.9rem; text-decoration: none; }
.back-link:hover { color: var(--accent); }

.status { padding: 0.4rem 0.8rem; border-radius: 8px; border: 1px solid var(--border); }
.status.ok  { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); color: var(--accent-ink);
              background: color-mix(in srgb, var(--accent) 12%, transparent); }
.status.err { border-color: #b91c1c; color: var(--error-fg); background: color-mix(in srgb, #b91c1c 14%, transparent); }

.diagnostics { list-style: none; padding: 0; margin: 0 0 0.8rem; font-size: 0.86rem; }
.diagnostics li { padding: 0.15rem 0; }
.diag-sev { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.04em; }
.diag-error .diag-sev { color: #f87171; }
.diag-warning .diag-sev { color: var(--accent); }

/* ── Account ─────────────────────────────────────────────────────────────── */
dl.account { display: grid; grid-template-columns: max-content 1fr; gap: 0.55rem 1.2rem; align-items: baseline; }
dl.account dt { color: var(--muted); }
dl.account dd { margin: 0; min-width: 0; }
.hint { display: block; margin-top: 0.15rem; font-size: 0.82rem; color: var(--muted); }

/* The identity block: who you are, at the size the question deserves. */
.acct-head { display: flex; align-items: center; gap: 1rem; }
.acct-glyph {
  width: 3.2rem; height: 3.2rem; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent-ink);
}
.acct-glyph svg { width: 1.8rem; height: 1.8rem; }
.acct-id { min-width: 0; }
.acct-id h1 { margin: 0; font-size: 1.25rem; overflow-wrap: anywhere; }
.acct-sub { margin: 0.25rem 0 0; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

/* The ladder, as a ladder: four rungs, yours lit. This encodes the real shape of
   the model — ordered, and you are at a point on it — which a bare pill does not.
   Rungs below yours are filled because you hold them too: the ladder is
   cumulative, and a reader who sees only their own rung lit would misread it. */
.ladder { display: flex; gap: 2px; margin: 1rem 0 0; }
.rung {
  flex: 1; padding: 0.4rem 0.6rem;
  background: var(--bg);
  border: 1px solid var(--border);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); text-align: center;
}
.rung:first-child { border-radius: 7px 0 0 7px; }
.rung:last-child { border-radius: 0 7px 7px 0; }
.rung.below { background: var(--heat-2); color: var(--text); border-color: transparent; }
.rung.here { background: var(--heat-4); color: var(--heat-ink); font-weight: 700; border-color: transparent; }
.ladder-note { margin: 0.5rem 0 0; font-size: 0.85rem; color: var(--muted); }

/* State as a shape, not a sentence: scannable in one pass. */
.state-row { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.9rem; }
.state-pill {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.78rem; font-weight: 600;
  padding: 0.15rem 0.55rem; border-radius: 999px;
  border: 1px solid var(--border);
  /* A <button> defaults to the UA's black `color`, and `font: inherit` below does
     not touch color — so an UNTONED action pill (Create identity, Sync) would sit
     black on the dark card. This readable default is overridden by .ok / .warn where
     a tone applies. */
  color: var(--text);
}
.state-pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }
.state-pill.warn { color: var(--accent-ink); border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
/* A pill that is also the verb (the tool rows, Create identity): it must read as a
   real, clickable button — the same shape as Save Secret (.btn), a rounded rectangle
   with the surface fill and button padding, not a status chip. The tone (.ok/.warn)
   still tints the border and the dot carries the state; disabled = nothing here would
   change it (the tool is not installed, or this profile may not run it). */
.state-pill.state-action {
  background: var(--surface);
  border-radius: 8px;
  padding: 0.35rem 0.9rem;
  font: inherit; font-size: 0.78rem; font-weight: 600;
  cursor: pointer;
  /* One shared width + centred content, so every action button (Create identity and
     the tool rows) lines up in a single right-hand column rather than sizing to its
     own label. */
  min-width: 9rem;
  justify-content: center;
}
.state-pill.state-action:hover:not(:disabled) { border-color: var(--accent); background: var(--surface-hi); }
.state-pill.state-action:disabled { cursor: default; color: var(--muted); }
/* The action sits opposite its label — the tool rows push it right already; the
   Create-identity row (a muted note + the button) does the same. */
.state-row .state-action { margin-left: auto; }

.pubkey {
  display: block; padding: 0.4rem 0.6rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 7px;
  font-size: 0.78rem; overflow-x: auto; white-space: nowrap;
}
/* The key scrolls; the Copy button beside it must not scroll away with it. The button
   is sized to the action column (btn-col) and the key takes the rest (flex:1, min 0). */
.pubkey-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.pubkey-row .pubkey { flex: 1; min-width: 0; }
.pubkey-row .btn { flex: none; font-size: 0.78rem; padding: 0.35rem 0.9rem; min-width: 9rem; }
.secret-list { list-style: none; padding: 0; margin: 0 0 0.6rem; display: flex; flex-direction: column; gap: 0.3rem; }
.secret-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  padding: 0.3rem 0.6rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 7px;
}
.secret-list code { color: var(--accent-ink); }
.btn-danger { color: var(--danger); border-color: transparent; background: none; font-size: 0.85rem; }
.btn-danger:hover { border-color: var(--danger); }
.vault-form { display: flex; gap: 0.4rem; flex-wrap: wrap; }
/* min-width 6rem (was 8): with the Save button now sized to the action column, the two
   secret fields shrink to keep the button on the same line rather than wrapping it. */
.vault-form input {
  flex: 1; min-width: 6rem;
  font: inherit; color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 0.4rem 0.6rem;
}
.vault-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.vault-form.inline { display: inline-flex; }
/* The vault-unlock failure (site.js fills #vault-recover-error). It sits inside
   the form's flex row, so it reads as that form's answer rather than the panel's. */
.form-error { align-self: center; font-size: 0.85rem; color: var(--danger); }

.tool-row {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.55rem 0; border-top: 1px solid var(--border);
}
.tool-row:first-of-type { border-top: 0; }
.tool-name { font-weight: 600; }
/* The tool names the row; its state — and the act that changes it — sits opposite. */
.tool-row .state-action { margin-left: auto; }

/* ── The terminal (right pane) ───────────────────────────────────────────── */
/* xterm renders into #term. The host is pinned to the literal dark --surface
   rather than the token: it shows the shell's own output, and the shell paints
   with absolute xterm-256 indices picked for a dark terminal (see terminal.js).
   The site is dark-only now, so the two agree — but the terminal's darkness is
   the shell's constraint, not the site's choice, and it must not drift if the
   palette ever moves. The small pad keeps the first glyph column off the pane
   border. */
.term-host {
  height: 100%;
  padding: 0.35rem 0.2rem 0.2rem 0.45rem;
  background: #171a21;
}
.term-host .xterm { height: 100%; }
.term-host .xterm-viewport { background: transparent !important; }
/* xterm's viewport is the scroll container: give it the site's scrollbar look
   rather than the platform default sitting on a themed surface. */
.term-host .xterm-viewport::-webkit-scrollbar { width: 10px; }
.term-host .xterm-viewport::-webkit-scrollbar-thumb {
  background: var(--border); border-radius: 6px;
}

/* ── Footer (fixed) ──────────────────────────────────────────────────────────
   Right-aligned: © · the documents, and — only while the terminal is hidden — its
   window at the very end (§ .ws-show). Nothing is reserved for it: with the terminal
   visible the documents hold the right edge themselves, and hiding it pushes them left
   by exactly the window's width. */
.app-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.6rem;
  padding: 0.55rem 1rem;
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: 0.85rem;
}
.app-footer a { color: var(--muted); text-decoration: none; }
.app-footer a:hover { color: var(--accent); }
.fsep { color: var(--border); }

/* ── problem-page tags & topics (A: header chip row · B: workbench panel) ─────────── */
.tagbar { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.75rem; }

.tag-chip {
  --fc: var(--muted);
  display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; line-height: 1;
  padding: 0.26rem 0.6rem; border-radius: 999px; text-decoration: none;
  color: color-mix(in srgb, var(--fc) 75%, var(--text));
  background: color-mix(in srgb, var(--fc) 12%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--fc) 26%, transparent);
  transition: border-color 0.14s ease, background 0.14s ease;
}
a.tag-chip:hover { border-color: var(--fc); background: color-mix(in srgb, var(--fc) 18%, var(--bg)); }
.tag-chip .tc-dot { width: 0.42rem; height: 0.42rem; border-radius: 50%; background: var(--fc); flex: none; }
.tag-chip.domain { --fc: var(--f-domain); }
.tag-chip.technique { --fc: var(--f-technique); }
.tag-chip.takeaway { --fc: var(--f-takeaway); }

.tag-panel .facet-row { display: flex; align-items: flex-start; gap: 0.7rem; padding: 0.4rem 0; }
.tag-panel .facet-row + .facet-row { border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
.facet-label {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted); min-width: 5.6rem; padding-top: 0.28rem; flex: none;
}
.facet-label .idx { color: var(--f-technique); }
.taglist { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* A section divider — a small mono label trailed by a hairline. Shared: the problem
   page's tag facets and the topics index's folders read as the same kind of break. */
/* The filter hides via the [hidden] attribute, which the display rules on these
   elements would otherwise override — so [hidden] wins here explicitly. */
.card[hidden], nav.cards[hidden], .subrule[hidden], .century[hidden] { display: none !important; }
.catalogue tr[hidden], .catalogue table[hidden] { display: none !important; }

/* ── The command catalogue, below the shelf on the terminal page ─────────────
   `docs/commands-catalogue.md` rendered in place. The file's own `#` heading and its
   explanatory prose are for a reader who opened it as a document; here the page has
   already said where it is and the sub-heading above it says what this is, so only the
   table is shown. The rule lives on the class, not in the template, because the body is
   generated markdown — the template gets one blob, and what it drops is a display
   decision. */
.catalogue > h1, .catalogue > p, .catalogue > ul { display: none; }
/* Everything the table itself brings stays — the legend line that explains the glyphs in
   its Description column is part of the table, and sits after it. */
.catalogue > table ~ p { display: block; }
.catalogue > table { margin-top: 0; }
/* A century cell the filter did not match is DIMMED, never hidden: a cell's position in
   its grid is what tells you which problem it is, so taking the misses out would
   renumber the grid under the reader (site.js § the pane filter). It stays a link — the
   grid is still the map, the query is only a light on part of it. */
.cell.is-dim { opacity: 0.22; }
@media (prefers-reduced-motion: no-preference) { .cell { transition: opacity 0.12s ease; } }
.subrule { display: flex; align-items: center; gap: 0.6rem; margin: 0.95rem 0 0.6rem; }
.subrule h3 {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.7rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}
.subrule .count { font-size: 0.7rem; color: var(--muted); }
.subrule::after { content: ""; flex: 1; height: 1px; background: var(--border); }

.topic-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }
@media (max-width: 700px) { .topic-cards { grid-template-columns: 1fr; } }
.tcard {
  display: flex; align-items: center; gap: 0.6rem; text-decoration: none; padding: 0.55rem 0.7rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg);
  transition: border-color 0.14s ease, transform 0.14s ease;
}
.tcard:hover { border-color: var(--accent); background: var(--surface-hi); transform: translateX(2px); }
.tcard .lam { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--accent); font-size: 1rem; flex: none; }
.tcard .tc-body { min-width: 0; }
.tcard .tc-name { display: block; color: var(--text); font-size: 0.86rem; }
.tcard .tc-facet {
  display: block; font-size: 0.7rem; color: var(--muted);
  font-family: ui-monospace, Menlo, Consolas, monospace; overflow: hidden; text-overflow: ellipsis;
}
.tcard .go { margin-left: auto; color: var(--muted); font-family: ui-monospace, Menlo, Consolas, monospace; }
.tcard:hover .go { color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .tag-chip, .tcard { transition: none; }
  .tcard:hover { transform: none; }
}
