/* Self-hosted fonts (OFL), latin subset only, no external CDN reference at
   runtime (this project is CSP-clean by design). Faces and weights are the
   ones actually used below: Newsreader 600 for headings and 400 italic for
   deks and captions, IBM Plex Sans as a single variable file for running
   text, IBM Plex Mono 400/700 for labels, numbers and technical text. */
@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/newsreader-600.woff2") format("woff2");
}
@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/newsreader-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-variable.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-700.woff2") format("woff2");
}

:root {
  /* Colour tokens: the field-manual direction. Graphite ink on warm paper,
     hairline rules for structure, one redline accent used as punctuation
     (numbering, brackets, signal states), never as a decorative fill. */
  --colour-graphite: #1c1b1a;
  --colour-paper: #fff6e5;
  --colour-white: #ffffff;
  --colour-redline: #b3401d;
  --colour-rule: #c9c5bc;
  /* Derived, not in the poster: secondary text (graphite at ~70% on paper,
     as a solid so it stays legible over any surface) and the pale redline
     wash for the one row highlight that must stay scannable. */
  --colour-muted: #5e5a54;
  --colour-redline-wash: #f8e7df;

  /* Stream hues: categorical, fixed order, never cycled. Validated with the
     dataviz validate_palette.js against the paper surface (#fff6e5), all
     nine together, all pairs. Redline is NOT a stream colour: it stays
     reserved for the gap state.
     The original four (default/business/advanced/practice) hold their
     2026-08-28 result: lightness band, chroma floor, CVD separation
     (worst pair ΔE 8.3 deutan), normal-vision floor (worst 16.4) and
     >= 3:1 contrast all pass on their own.
     The five added for the module-files refactor sit in tighter hue gaps
     between those four (a teal, a magenta, a berry-red, an olive and a
     periwinkle), which pushed three of them (web-dev, student,
     homelab-ops) higher in lightness than the original four to clear the
     normal-vision floor against their nearest neighbours; that lands their
     contrast against the paper surface in the validator's WARN band
     (2.5-2.6:1, not the FAIL band) rather than clearing 3:1 outright. The
     validator's own rule is that a contrast WARN is legal only with
     secondary encoding (visible labels, not colour alone) - every place a
     stream colour renders (`.graph-key__dot--`, `.stream-section__swatch`,
     `.landing-streams__name`) sits directly beside the stream's name as
     text, so that condition is met by construction, not waived. Command:
     `node scripts/validate_palette.js "<all nine hexes>" --mode light
     --surface "#fff6e5" --pairs all` (dataviz skill). */
  --stream-default: #2a78d6;
  --stream-business: #0f8f5f;
  --stream-advanced: #7a3aa7;
  --stream-practice: #9c6a00;
  --stream-web-dev: #00accc;
  --stream-app-dev: #a03648;
  --stream-content-creation: #c838a3;
  --stream-homelab-ops: #9392ff;
  --stream-student: #90a500;

  /* Type roles */
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-body: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* Spacing scale, 8px base unit */
  --space-1: 1rem;    /* 16px */
  --space-2: 1.5rem;  /* 24px */
  --space-3: 2.5rem;  /* 40px */
  --space-4: 4rem;    /* 64px */
  --space-5: 5rem;    /* 80px */
  --space-6: 6.5rem;  /* 104px */
  --space-7: 7.5rem;  /* 120px */
}

* {
  box-sizing: border-box;
  border-radius: 0 !important;
}

body {
  margin: 0;
  background: var(--colour-paper);
  color: var(--colour-graphite);
  font-family: var(--font-body);
  line-height: 1.5;
  /* Footer always at the foot of the viewport, even on a short page like
     sign-in: the body is a column, main takes whatever height is spare. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
body > main { flex: 1 0 auto; }

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-1);
}
h1 { font-size: 2.75rem; line-height: 1; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.2rem; }
p { margin: 0 0 var(--space-1); }

a { color: var(--colour-redline); text-decoration: none; }
a:hover { text-decoration: underline; }
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 2px solid var(--colour-redline);
  outline-offset: 2px;
}

/* Shared typographic roles, lifted from the poster */
.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--colour-redline);
  margin: 0 0 0.875rem;
}
.dek {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: 1.25rem;
  color: var(--colour-muted);
  margin: 0.625rem 0 0;
}
.meta {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.03em;
  color: var(--colour-muted);
  margin: 0.625rem 0 var(--space-2);
}
.rule { border: 0; border-top: 1px solid var(--colour-rule); margin: var(--space-2) 0; }

.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--colour-rule);
}
.nav__brand { display: inline-flex; text-decoration: none; }
.nav__wordmark { display: block; height: 1.375rem; width: auto; transition: opacity 80ms ease-out; }
.nav__brand:hover .nav__wordmark { opacity: 0.75; }
.nav__links { display: flex; align-items: center; gap: 1.25rem; margin-left: auto; margin-right: 1.5rem; }
.nav__link { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-graphite); text-decoration: none; }
.nav__link:hover { text-decoration: underline; text-decoration-color: var(--colour-redline); }
.nav__link--admin { color: var(--colour-redline); }
.nav__logout { display: flex; align-items: center; gap: var(--space-1); }
.nav__user { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-muted); }

/* Mobile nav toggle + menu (#209 stage 1). .nav__menu is `display: contents`
   above the breakpoint, so its children (.nav__links, .nav__logout) sit
   directly in the .nav flex row exactly as they did before this partial
   grew a wrapper - the desktop layout below is unchanged pixel-for-pixel.
   .nav__toggle stays display:none there; only the @media block below turns
   it into a working disclosure. */
.nav__toggle {
  display: none;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--colour-graphite);
  background: var(--colour-white);
  border: 1px solid var(--colour-graphite);
  padding: 0.4rem 0.75rem;
  line-height: 1;
  cursor: pointer;
}
.nav__toggle:hover, .nav__toggle[aria-expanded="true"] { background: var(--colour-graphite); color: var(--colour-paper); }
/* Three bars drawn with currentColor, so the hover/open colour flip above
   recolours them for free - no separate icon asset, same self-drawn idiom
   as the .btn bracket glyphs. */
.nav__toggle-bars { position: relative; display: inline-block; width: 1rem; height: 2px; background: currentColor; }
.nav__toggle-bars::before, .nav__toggle-bars::after { content: ""; position: absolute; left: 0; width: 1rem; height: 2px; background: currentColor; }
.nav__toggle-bars::before { top: -5px; }
.nav__toggle-bars::after { top: 5px; }
.nav__menu { display: contents; }
@media (max-width: 640px) {
  .nav { flex-wrap: wrap; row-gap: var(--space-1); }
  .nav__toggle { display: inline-flex; }
  .nav__menu {
    display: none;
    flex-basis: 100%;
    border-top: 1px solid var(--colour-rule);
    margin-top: var(--space-1);
    padding-top: var(--space-1);
  }
  .nav__menu.is-open { display: block; }
  .nav__links { flex-direction: column; align-items: flex-start; gap: 0.75rem; margin: 0 0 var(--space-2); }
  .nav__logout { justify-content: space-between; width: 100%; }
}

/* Buttons: the poster's bracket convention, mono uppercase, the literal
   brackets part of the label. They sit ON the paper rather than in it: a
   white face and a 2px solid offset shadow (the print-offset idiom the graph
   nodes use, no blur), lifting a touch on hover. Same-colour-as-page was
   Rodney's flatness complaint. */
.btn {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--colour-white);
  border: 1px solid var(--colour-redline);
  color: var(--colour-redline);
  padding: 0.5rem 1.125rem;
  cursor: pointer;
  text-decoration: none;
  line-height: 1.4;
  white-space: nowrap;
  box-shadow: 2px 2px 0 var(--colour-redline);
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
}
.btn::before { content: "[ "; }
.btn::after { content: " ]"; }
.btn:hover { background: var(--colour-redline); color: var(--colour-paper); text-decoration: none; transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--colour-redline); }
.btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--colour-redline); }
/* Primary CTA gets Concept 1B's lift: the same crisp offset stays, a soft
   diffuse shadow is layered underneath so it reads as sitting proud of the
   page rather than a flat stamp. */
.btn--cta {
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.35) inset,
    2px 2px 0 var(--colour-redline),
    6px 8px 12px rgba(28, 27, 26, 0.1);
}
.btn--cta:hover {
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.35) inset,
    3px 3px 0 var(--colour-redline),
    8px 10px 15px rgba(28, 27, 26, 0.12);
}
.btn--nav { border-color: var(--colour-graphite); color: var(--colour-graphite); box-shadow: 2px 2px 0 var(--colour-graphite); }
.btn--nav:hover { background: var(--colour-graphite); color: var(--colour-paper); box-shadow: 3px 3px 0 var(--colour-graphite); }
.btn--nav:active { box-shadow: 1px 1px 0 var(--colour-graphite); }
/* OAuth buttons carry the provider's brand mark before the label. The mark
   sits inside the brackets with the text. */
.btn--oauth { display: inline-flex; align-items: center; gap: 0.6rem; }
.btn--oauth .brand-icon { flex: none; }
.btn--oauth:hover .brand-icon--github { color: var(--colour-paper); }
@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn:hover, .btn:active { transform: none; }
}

/* Landing page: the poster's masthead, recomposed for width. One container
   (.landing) owns the alignment axis for every section on the page, so the
   masthead, the figure, the stream summary and the sign-in share the same
   left and right edges instead of each choosing its own (the old page had
   a 1200px header, a centred figure and 700px sections, which read as
   left-hugging and ragged at desktop widths). */
.landing { max-width: 1200px; margin: 0 auto; padding: var(--space-3) var(--space-2) 0; }
.landing-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: var(--space-4);
}
.landing-header .eyebrow { grid-column: 1 / -1; }
.landing-header h1 { font-size: clamp(2.25rem, 5vw, 4.25rem); line-height: 1; margin: 0; max-width: 18ch; }
.landing-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin-top: var(--space-2);
}
.landing-actions p { margin: 0; font-family: var(--font-mono); font-size: 0.8125rem; }

/* Drawing-sheet title block beside the headline: the curriculum's vital
   statistics as a specification table, mono on white, sitting on the paper
   with the same 3px offset shadow the graph nodes use. It gives the
   masthead's right side a purpose instead of a void. */
.landing-spec {
  border: 1px solid var(--colour-graphite);
  background: var(--colour-white);
  /* Crisp offset shadow keeps the print-stamp identity; the soft diffuse
     layer under it is what actually reads as lifted off the paper, not
     just a graphic device. Inset hairline catches light from above. */
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.5) inset,
    3px 3px 0 var(--colour-rule),
    8px 10px 16px rgba(28, 27, 26, 0.07);
  min-width: 300px;
}
.landing-spec__title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--colour-redline);
  margin: 0;
  padding: 0.5rem 0.875rem;
  border-bottom: 1px solid var(--colour-graphite);
}
.landing-spec__rows { margin: 0; }
.landing-spec__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  padding: 0.5rem 0.875rem;
}
.landing-spec__row + .landing-spec__row { border-top: 1px solid var(--colour-rule); }
.landing-spec__row dt {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--colour-muted);
}
.landing-spec__row dd { margin: 0; font-family: var(--font-mono); font-size: 0.8125rem; text-align: right; }

.landing .rule { margin: var(--space-3) 0 var(--space-2); }

/* Fig. 1: the real dependency-graph excerpt, full container width in a
   hairline frame. The SVG scales with the page (viewBox); below the
   min-width it scrolls sideways inside the frame, the same convention as
   the authenticated graph's .graph-scroll. */
.landing-figure { margin: 0 0 var(--space-2); }
.landing-figure__frame {
  border: 1px solid var(--colour-rule);
  padding: 18px;
  overflow-x: auto;
  background: var(--colour-white);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.5) inset,
    6px 6px 0 var(--colour-rule),
    10px 14px 20px rgba(28, 27, 26, 0.06);
}
.hero-graph { display: block; width: 100%; height: auto; min-width: 720px; }
.hero-graph__titlebox { display: flex; align-items: center; height: 100%; }
/* px, not rem: foreignObject content scales with the SVG viewBox, so these
   are viewBox units like the node geometry, not screen pixels. */
.hero-graph__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  line-height: 20px;
  color: var(--colour-graphite);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.landing-figure__caption {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--colour-muted);
  margin: 0.75rem 0 0;
}
.landing-figure__key { margin: 0.625rem 0 0; }
.landing-figure__hint {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--colour-muted);
  margin: 0.5rem 0 0;
}

/* Hero module nodes double as tooltip triggers: a plain-looking clickable
   surface (the print-stamp box already reads as an object; a heavier hover
   state would fight that), just a cursor and a faint lift to signal it. */
.graph-node--tappable { cursor: pointer; }
.graph-node--tappable:hover .graph-node__box,
.graph-node--tappable:focus-visible .graph-node__box {
  stroke-width: 1.75;
}
.graph-node--tappable:focus-visible { outline: none; }
.graph-node--tappable:focus-visible .graph-node__box {
  stroke: var(--colour-redline);
}

/* One shared tooltip, positioned in JS per node: same lifted-card language
   as the spec block and hero frame (crisp offset shadow, soft diffuse
   layer, inset highlight), so it reads as part of the same system rather
   than a bolted-on browser tooltip. */
.hero-tooltip {
  position: absolute;
  z-index: 10;
  max-width: 22rem;
  background: var(--colour-white);
  border: 1px solid var(--colour-graphite);
  padding: 0.625rem 0.875rem;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.5) inset,
    3px 3px 0 var(--colour-rule),
    8px 10px 16px rgba(28, 27, 26, 0.1);
}
.hero-tooltip__title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--colour-redline);
  margin: 0 0 0.3rem;
}
.hero-tooltip__desc {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--colour-graphite);
  margin: 0;
}

/* .page and views/skill-tree.ejs's main.skilltree-shell share one gutter
   definition (width + padding) so the two can never drift apart - see
   skilltree-shell's own comment below for why that page needs a second,
   uncapped class at all. The cap itself (max-width + centring) is also
   shared with .skilltree-measure, the narrow-band wrapper that same page
   uses: same reasoning, one value, nothing to keep in sync by hand. */
.page,
main.skilltree-shell {
  width: 100%;
  padding: var(--space-3) var(--space-2);
}
.page,
.skilltree-measure {
  max-width: 1200px;
  margin: 0 auto;
}
/* main.skilltree-shell stays uncapped - that is the whole point of the
   selective-width layout below. */
/* #209 stage 2: `width` was implicit (auto), relying on body's flex
   stretch (body is `display: flex; flex-direction: column`, see below) to
   fill the line. That holds everywhere except module.ejs at narrow
   widths: the snippet's .code-window__body is `white-space: pre` so its
   one long unwrapped line has no shrinkable min-content, and Chromium's
   stretch computation for `.page` (a flex item with the default
   `overflow: visible`) let that line's width win over the container's
   cross size instead of clipping it - .page rendered ~95px wider than
   the viewport at 375px, cutting text off at the right edge on every
   paragraph (not a scrollable overflow, a hard clip: scrollLeft did not
   move it). .code-window__body's own overflow-x: auto was never the
   problem; explicit `width: 100%` here removes the auto-stretch
   ambiguity outright, giving `.page` a definite size regardless of what
   a descendant's unbreakable content asks for. Confirmed live in Chrome:
   before, main.page measured 469.7px at a 375px viewport with a
   claude.ai-basics-module code snippet; after, 375px exactly, with no
   change to any page that does not hit this content shape. */

/* Empty states and the 404 carry one spot illustration each (the `empty`
   asset class, 800px, under 25KB). The art sits BESIDE the text, never
   above it, so the sentence that actually says what is going on stays at
   the top of the reading order; mix-blend-mode: multiply lets the render's
   own cream ground disappear into the paper instead of showing as a card.
   The wide variant is the 404, where there is room for a bigger picture
   and the text is a full heading block rather than one line. */
.empty-figure { display: flex; align-items: center; gap: var(--space-2); }
.empty-figure__art { flex: none; width: 200px; height: auto; mix-blend-mode: multiply; }
.empty-figure__text { margin: 0; }
.empty-figure--wide { align-items: center; gap: var(--space-3); }
.empty-figure--wide .empty-figure__art { width: min(380px, 40%); }
@media (max-width: 640px) {
  .empty-figure, .empty-figure--wide { flex-direction: column; align-items: flex-start; }
  .empty-figure__art, .empty-figure--wide .empty-figure__art { width: 160px; }
}

/* Narrower measure for body prose within the wider .page container. */
.prose { max-width: 70ch; }

.progress-bar {
  border: 1px solid var(--colour-graphite);
  height: 0.75rem;
  background: var(--colour-white);
  max-width: 32rem;
}
.progress-bar__fill { height: 100%; background: var(--colour-graphite); }
.progress-bar__label { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-muted); }

/* Rodney's width decision (2026-08-31): views/skill-tree.ejs goes
   SELECTIVELY wide - the stream-chip row and the module rows span the
   full viewport with a small gutter, headings and prose keep the 1200px
   measure. <main> on that page carries skilltree-shell instead of .page
   (grouped with .page above, so the gutter is one definition, not two in
   sync by hand) - a SEPARATE class, not a modifier, because the whole
   point is to NOT carry .page's max-width. A narrow band on that page
   nests .skilltree-measure (also grouped with .page above: same cap,
   same reason), which centres and caps itself with no padding of its own
   - the padding already lives on <main>. A wide band needs no wrapper at
   all: it already fills <main>'s full, already-gutter-padded width.

   This deliberately avoids the classic `margin-left: calc(50% - 50vw)`
   full-bleed trick: 100vw does not subtract the scrollbar's own width, so
   that trick gives a horizontal scrollbar on any page that already has a
   vertical one. Wide bands are direct children of an uncapped <main>
   instead of breaking out of a capped one, which avoids the hazard
   structurally rather than by care - confirmed at 1280/1440/1920px, see
   the PR body for the measured numbers. */

/* Per-stream progress (#215): one row per stream, the same graph-key__dot
   swatch the graph's own key and the list's stream headings already use,
   so a stream's colour means the same thing everywhere on the page. Counts
   only, never a percentage (D16-R13: some streams have well under 10
   modules, where a percentage would be noise a raw count is not). */
.stream-progress { margin: 0.75rem 0 1.25rem; }
.stream-progress__heading { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--colour-muted); margin: 0 0 0.4rem; }
.stream-progress__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem; }
.stream-progress__item { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-graphite); }
.stream-progress__count { color: var(--colour-muted); }

/* Module list: the poster's section rows. Number, title and description,
   state, trailing icon, one hairline between rows, no boxes. */
.module-list {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  border-top: 1px solid var(--colour-rule);
}
.module-row { border-bottom: 1px solid var(--colour-rule); }
.module-row a,
.module-row__body {
  display: grid;
  grid-template-columns: 3.25rem 1fr auto 1.875rem;
  column-gap: 0.875rem;
  align-items: center;
  padding: 1.25rem 0;
  color: inherit;
  text-decoration: none;
}
.module-row a:hover .module-row__title { text-decoration: underline; text-decoration-color: var(--colour-redline); }
.module-row a:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: 4px; }
.module-row__number { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-redline); }
.module-row__title { font-family: var(--font-display); font-weight: 600; font-size: 1.3125rem; line-height: 1.2; margin: 0; }
.module-row__optional {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-muted);
  margin-left: 0.5rem;
  vertical-align: 0.15em;
}
.module-row__description { font-size: 0.9375rem; line-height: 1.5; color: var(--colour-muted); margin: 0.25rem 0 0; }
.module-row__note { font-family: var(--font-mono); font-size: 0.75rem; color: var(--colour-muted); margin: 0.375rem 0 0; }
.module-row__state {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--colour-graphite);
}
/* State is always icon + literal text; colour only reinforces. Gap is the
   one state that needs attention, so it alone takes the redline. Locked
   recedes. */
.module-row__state--gap { color: var(--colour-redline); }
.module-row__state--locked, .module-row--locked .module-row__title, .module-row--locked .module-row__icon, .module-row__state--restricted, .module-row--restricted .module-row__title, .module-row--restricted .module-row__icon { color: var(--colour-muted); }
.module-row__state--complete { color: var(--colour-graphite); }
.module-row__icon { display: flex; justify-content: center; color: var(--colour-graphite); }
.module-row__icon svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 2.2; fill: none; stroke-linecap: round; stroke-linejoin: round; }

.status-icon { display: inline-block; vertical-align: -0.2em; }

/* Skill-tree graph (6c). Layout logic untouched, palette only. */

/* Rodney's width decision (2026-08-31): unlike the list view (see
   skilltree-shell's own comment above), the graph page goes fully wide,
   not selectively - the graph itself is what needs the room. A modifier
   ON .page, not a separate class: .page's own max-width (line 443) stays
   the site-wide default for every other page, this page alone overrides
   it. Everything else .page already provides (width: 100%, the
   var(--space-2)/var(--space-3) padding) is exactly the gutter Rodney
   asked for, unchanged - so this is the one property that needs undoing,
   nothing else. */
.page--wide { max-width: none; }

.graph-scroll { overflow-x: auto; margin-top: var(--space-2); }
/* No visible scrollbar: wheel, trackpad and drag-to-pan (graph-highlight.js)
   move the map, the bar was noise under the picture. */
.graph-scroll { scrollbar-width: none; cursor: grab; }
.graph-scroll::-webkit-scrollbar { display: none; }
.graph-scroll--panning { cursor: grabbing; user-select: none; }
/* Issue #15: now a tab stop (tabindex="0" in skill-tree-graph.ejs) so it
   needs its own focus-visible ring - the global `a, button, input`
   selector above does not cover a plain <div>. */
.graph-scroll:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: 2px; }
.graph-svg { display: block; font-family: var(--font-mono); }
/* Edges are orthogonal paths from src/graph-edges.js, not lines: fill must
   be none or the elbows would paint a shape. Into a locked module they go
   muted and dashed, matching the locked box. */
/* One ink for every line, whatever stream it enters. Stream colour lives on
   the card plate, glyph and label; a coloured line stopped meaning anything
   once lines share a trunk before splitting. The chain view is the only
   coloured line state, in the redline accent with its own arrowhead. */
.graph-edge { fill: none; stroke: var(--colour-muted); stroke-width: 1.5; stroke-linejoin: round; marker-end: url(#graph-arrow); }
/* Colour follows the TARGET stream: a cross-stream edge takes the hue of
   the module it unlocks, the same hue as that node's plate. Into a locked
   module it keeps the hue and goes dashed; most of the tree is locked from
   cold, so muting those would erase the colour. */
.graph-edge--to-locked { stroke-dasharray: 5 3; }
/* Click-to-highlight (public/js/graph-highlight.js): the selected module
   and its whole prerequisite chain stay at full strength, everything else
   fades to a trace so the chain reads as one shape. Opacity rather than
   display so the fade animates and nothing reflows; the stream colours
   and lock dashes on chain members are untouched, only the rest recede. */
.graph-svg--highlighting .graph-node--dimmed { opacity: 0.22; transition: opacity 0.15s; }
.graph-svg--highlighting .graph-edge--dimmed { opacity: 0.12; transition: opacity 0.15s; }
/* A prerequisite more than one column away is hidden until it is part of
   a clicked module's chain (Rodney's call, 2026-08-29). Drawn, it is a
   flat run through every column in between, and nine of them turned a
   hand-placed layout into spaghetti; moving cards cannot shorten them
   because each target has a second parent in a far column. The chain
   view draws them in full, so the information is one click away, and
   the key says so. display, not opacity: a hidden long edge must not
   catch a click either. */
.graph-edge--long-range { display: none; }
.graph-svg--highlighting .graph-edge--long-range.graph-edge--in-chain { display: inline; }
/* The cue on a node whose incoming prerequisite is hidden: a short dashed
   stub ending in the edges' own arrowhead, at the exact left-centre join
   point a real line would use. Dashed and muted so it reads as "a line
   belongs here" and not as a line. Gone the moment a chain view is up,
   because then the real line is drawn and the stub would double it. */
.graph-node__hidden-badge-box { fill: var(--colour-paper, #fff); stroke: var(--colour-muted); stroke-width: 1; stroke-dasharray: 3 2; }
.graph-node__hidden-badge-text { fill: var(--colour-muted); font-family: var(--font-mono, monospace); font-size: 10px; }
.graph-svg--highlighting .graph-node__hidden-badge { display: none; }
.graph-node--selected .graph-node__box { stroke: var(--colour-redline); stroke-width: 2; }
.graph-node--in-chain .graph-node__box { stroke: var(--colour-graphite); stroke-width: 1.75; }
.graph-edge--in-chain { stroke: var(--colour-redline); stroke-width: 2.25; marker-end: url(#graph-arrow-chain); }
.graph-arrow { fill: var(--colour-muted); }
.graph-arrow--chain { fill: var(--colour-redline); }
.graph-key__stream { display: inline-flex; align-items: center; gap: 0.4rem; }
.graph-key__dot { width: 10px; height: 10px; flex: none; border: 1px solid var(--colour-graphite); }
.graph-key__dot--default { background: var(--stream-default); }
.graph-key__dot--business { background: var(--stream-business); }
.graph-key__dot--advanced { background: var(--stream-advanced); }
.graph-key__dot--practice { background: var(--stream-practice); }
.graph-key__dot--web-dev { background: var(--stream-web-dev); }
.graph-key__dot--app-dev { background: var(--stream-app-dev); }
.graph-key__dot--content-creation { background: var(--stream-content-creation); }
.graph-key__dot--homelab-ops { background: var(--stream-homelab-ops); }
.graph-key__dot--student { background: var(--stream-student); }
.graph-key { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.03em; color: var(--colour-graphite); margin: 0.4rem 0 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.graph-key__item { display: inline-flex; align-items: center; gap: 0.5rem; }
.graph-key__item--muted { color: var(--colour-muted); }
.graph-key__swatch { flex: none; }
/* #327 follow-up: the edge-style legend is read once, not every visit, so
   it collapses behind a native <details> closed by default on the graph
   page only (landing.ejs's own .graph-key stays a plain always-open
   paragraph - it is not wrapped in <details> there). */
.graph-key-details { margin: 0.6rem 0 0; }
.graph-key-details__summary { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.03em; color: var(--colour-muted); cursor: pointer; }
.graph-key-details__summary:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: 2px; }
.graph-key-details[open] .graph-key-details__summary { color: var(--colour-graphite); margin-bottom: 0.1rem; }
.graph-node__box {
  fill: var(--colour-white);
  stroke: var(--colour-graphite);
  stroke-width: 1;
}
.graph-node--locked .graph-node__box, .graph-node--restricted .graph-node__box { stroke-dasharray: 4 3; stroke: var(--colour-muted); }
.graph-node--locked .graph-node__title-html, .graph-node--locked .graph-node__state, .graph-node--locked .graph-node__icon, .graph-node--restricted .graph-node__title-html, .graph-node--restricted .graph-node__state, .graph-node--restricted .graph-node__icon { color: var(--colour-muted); fill: var(--colour-muted); }
.graph-node--complete .graph-node__box { fill: var(--colour-paper); stroke: var(--colour-rule); }
.graph-node--gap .graph-node__box { stroke: var(--colour-redline); stroke-width: 2; }
.graph-node--gap .graph-node__state, .graph-node--gap .graph-node__icon { color: var(--colour-redline); fill: var(--colour-redline); }
.graph-node__icon { color: var(--colour-graphite); }
/* Module glyph plate on the left of each box, divided from the text by a
   hairline. The same drawings as the list rows (partials/module-icon). */
.module-glyph { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.graph-node__glyph { color: var(--colour-graphite); stroke-width: 1.8; }
.graph-node__divider { stroke: var(--colour-rule); stroke-width: 1; }
/* The plate behind the glyph is a wash of the stream hue; the glyph itself
   is the full hue. Locked keeps the hue (the dashed box carries the lock),
   gap takes redline (reserved), complete fades with the rest of the box. */
.graph-node__shadow { fill: var(--colour-rule); }
.graph-node--locked .graph-node__shadow, .graph-node--restricted .graph-node__shadow { fill: color-mix(in oklab, var(--colour-rule) 45%, var(--colour-paper)); }
.graph-node--complete .graph-node__shadow { fill: transparent; }
.graph-node--gap .graph-node__shadow { fill: var(--colour-redline-wash); }
.graph-node__plate { fill: transparent; }
.graph-node--stream-default .graph-node__plate { fill: color-mix(in oklab, var(--stream-default) 12%, var(--colour-white)); }
.graph-node--stream-business .graph-node__plate { fill: color-mix(in oklab, var(--stream-business) 12%, var(--colour-white)); }
.graph-node--stream-advanced .graph-node__plate { fill: color-mix(in oklab, var(--stream-advanced) 12%, var(--colour-white)); }
.graph-node--stream-practice .graph-node__plate { fill: color-mix(in oklab, var(--stream-practice) 12%, var(--colour-white)); }
.graph-node--stream-web-dev .graph-node__plate { fill: color-mix(in oklab, var(--stream-web-dev) 12%, var(--colour-white)); }
.graph-node--stream-app-dev .graph-node__plate { fill: color-mix(in oklab, var(--stream-app-dev) 12%, var(--colour-white)); }
.graph-node--stream-content-creation .graph-node__plate { fill: color-mix(in oklab, var(--stream-content-creation) 12%, var(--colour-white)); }
.graph-node--stream-homelab-ops .graph-node__plate { fill: color-mix(in oklab, var(--stream-homelab-ops) 12%, var(--colour-white)); }
.graph-node--stream-student .graph-node__plate { fill: color-mix(in oklab, var(--stream-student) 12%, var(--colour-white)); }
.graph-node--stream-default .graph-node__glyph { color: var(--stream-default); }
.graph-node--stream-business .graph-node__glyph { color: var(--stream-business); }
.graph-node--stream-advanced .graph-node__glyph { color: var(--stream-advanced); }
.graph-node--stream-practice .graph-node__glyph { color: var(--stream-practice); }
.graph-node--stream-web-dev .graph-node__glyph { color: var(--stream-web-dev); }
.graph-node--stream-app-dev .graph-node__glyph { color: var(--stream-app-dev); }
.graph-node--stream-content-creation .graph-node__glyph { color: var(--stream-content-creation); }
.graph-node--stream-homelab-ops .graph-node__glyph { color: var(--stream-homelab-ops); }
.graph-node--stream-student .graph-node__glyph { color: var(--stream-student); }
.graph-node--stream-default .graph-node__stream { fill: var(--stream-default); }
.graph-node--stream-business .graph-node__stream { fill: var(--stream-business); }
.graph-node--stream-advanced .graph-node__stream { fill: var(--stream-advanced); }
.graph-node--stream-practice .graph-node__stream { fill: var(--stream-practice); }
.graph-node--stream-web-dev .graph-node__stream { fill: var(--stream-web-dev); }
.graph-node--stream-app-dev .graph-node__stream { fill: var(--stream-app-dev); }
.graph-node--stream-content-creation .graph-node__stream { fill: var(--stream-content-creation); }
.graph-node--stream-homelab-ops .graph-node__stream { fill: var(--stream-homelab-ops); }
.graph-node--stream-student .graph-node__stream { fill: var(--stream-student); }
.graph-node--gap .graph-node__glyph { color: var(--colour-redline); }
.graph-node--complete .graph-node__glyph { opacity: 0.55; }
.graph-node--complete .graph-node__plate { opacity: 0.5; }
.graph-node__title-html {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--colour-graphite);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 22px;
}
.graph-node__state { font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.08em; fill: var(--colour-graphite); }
.graph-nodes a:focus-visible .graph-node__box { stroke: var(--colour-redline); stroke-width: 3; }

/* The native checkbox is kept for keyboard and assistive tech but drawn by
   the label, so input and label are one inline unit and never wrap apart
   (at phone width the box landed on one line and its text on the next).
   The inputs stay direct siblings of .graph-viewport for the :checked ~ rules. */
.graph-toggle-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.graph-toggle-label { font-family: var(--font-mono); font-size: 0.8125rem; display: inline-flex; align-items: center; gap: 0.4rem; margin: var(--space-2) var(--space-2) 0 0; white-space: nowrap; cursor: pointer; }
.graph-toggle-label::before { content: ""; width: 0.85em; height: 0.85em; border: 1px solid var(--colour-graphite); background: var(--colour-white); flex: none; }
.graph-toggle-input:checked + .graph-toggle-label::before { background: var(--colour-graphite); box-shadow: inset 0 0 0 2px var(--colour-white); }
.graph-toggle-input:focus-visible + .graph-toggle-label::before { outline: 2px solid var(--colour-redline); outline-offset: 2px; }
#hide-completed:checked ~ .graph-viewport .graph-node--hideable { display: none; }
#hide-completed:checked ~ .graph-viewport .graph-edge--complete { display: none; }

/* #325: stream chips. One .graph-toggle-label per stream, carrying the
   colour swatch, name and "x of y" that the by-stream progress list and
   the graph-key's plain stream chips used to show separately. The
   checkbox mechanism, focus ring and ::before glyph above are untouched;
   this only styles the label as a bordered chip. "Hide completed" used to
   sit beside "By stream" on the same line, divided by the border-right
   below; #209 stage 2 removed that divider along with it (see
   .stream-chips__heading), since the two no longer share a line. */
.graph-toggle-label--hide-completed { margin-bottom: 0.5rem; }
/* #209 stage 2: was `inline-block`, sitting mid-line between "Hide
   completed modules" and the first chip - correct only while the chip row
   fit on one line. Once it wrapped (a certainty below 640px, and an
   accepted cost above it too while .page's 1200px cap is out of scope -
   see the coordinator note on skill-tree-graph.ejs), the heading read as
   labelling just that first row rather than the whole collection. `block`
   puts it on its own line every time, so it reads as one heading over
   however many rows the chips wrap into. */
.stream-chips__heading { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--colour-muted); display: block; margin: 0.75rem 0 0.5rem; }
.graph-toggle-label--stream { border: 1px solid var(--colour-rule); border-radius: 3px; padding: 0.3rem 0.55rem; }
.stream-chip__dot { width: 10px; height: 10px; flex: none; border: 1px solid var(--colour-graphite); }
.stream-chip__name { font-weight: 600; }
.stream-chip__count { color: var(--colour-muted); }
/* Hidden state must read without colour: this row already spends its one
   colour axis on the nine stream swatches, so "hidden" is dimming, a
   strikethrough on the text, and the checkbox glyph above flipping from
   hollow to filled, together, not a second colour change. */
.graph-toggle-input--stream:checked + .graph-toggle-label--stream { opacity: 0.5; }
.graph-toggle-input--stream:checked + .graph-toggle-label--stream .stream-chip__name,
.graph-toggle-input--stream:checked + .graph-toggle-label--stream .stream-chip__count { text-decoration: line-through; }

/* Admin dashboard sections (#377): plain <details>/<summary>, no JS - the
   same pattern as .graph-key-details in skill-tree-graph.ejs, just heavier
   (a whole dashboard section, not a caption). Default closed - Rodney's
   refinement on the issue: find what you need without scrolling past
   everything, no exception for whichever section happens to load open.
   admin.ejs opens a handful of these itself via a conditional `open`
   attribute when the server has just rendered a fresh confirmation or
   error into that section, so a form submission's feedback (redirects to
   /admin#invitations etc. in src/server.js already depend on this) is
   never hidden behind a closed disclosure - a template decision, not
   something faked here with CSS. The native disclosure marker is kept,
   not hidden or replaced: every browser and screen reader already knows
   it, for free, same reasoning as the existing .graph-key-details. */
.admin-section {
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--colour-rule);
}
.admin-section:last-of-type { border-bottom: none; }
.admin-section > summary {
  cursor: pointer;
  margin-bottom: var(--space-2);
}
.admin-section > summary:focus-visible {
  outline: 2px solid var(--colour-redline);
  outline-offset: 2px;
}
/* The heading sits inside <summary> (not beside it) so heading-based
   screen-reader navigation still finds it - <summary>'s content model
   explicitly allows one heading element, and the disclosure/toggle
   behaviour is unaffected either way. display: inline so it sits on the
   marker's line instead of forcing its own; margin: 0 overrides the
   global h1/h2/h3 rule's bottom margin, which would otherwise show up
   as a gap inside the clickable summary row. */
.admin-section > summary > h2 { display: inline; margin: 0; }
/* Attention badge (#377): a pending/actionable count next to a section's
   name, e.g. "Admin applications 3" - the alternative to auto-expanding
   the section itself, which Rodney's refinement on the issue explicitly
   ruled out ("a badge/count, not full expansion"). Only sections with a
   real pending queue get one (see admin.ejs's comments on which do and,
   just as deliberately, which do not). */
.admin-section__badge {
  display: inline-block;
  margin-left: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--colour-redline);
  background: var(--colour-redline-wash);
  border-radius: 999px;
  padding: 0.05rem 0.55rem;
  vertical-align: middle;
}

/* Admin nudge-triage dashboard (step 7) */
.admin-summary { margin: var(--space-2) 0; max-width: 70ch; }
.admin-filters { display: flex; gap: var(--space-2); margin-bottom: var(--space-2); font-family: var(--font-mono); font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.08em; }
.admin-filters a[aria-current="page"] { color: var(--colour-graphite); border-bottom: 2px solid var(--colour-redline); }
/* Wide tables scroll inside their own container; the page never scrolls
   sideways (the admin dashboard did at phone width before this).
   #209 stage 4: the scroll itself was never the problem on admin.ejs and
   admin-users.ejs - discovering it was. admin-users.ejs's 11-column table
   is 915px wide at 375px viewport, so only its first 3-4 columns show
   and the rest (role, cohort, path, completion, last activity) sit off
   to the right with nothing but a hairline native scrollbar thumb
   hinting more exists - invisible by default on a touch device until
   actively dragged. Confirmed live: Role/Cohort/Approve-Reject/Cancel
   controls on both pages were reachable only by a scroll a first-time
   visitor had no reason to attempt.
   Fixed with the standard no-JS "scroll shadow" trick (four background
   layers: two flat covers that scroll WITH the table content and fade to
   nothing at each edge, two hairline shadows fixed to the box itself
   underneath them) rather than JS or a new wrapper element - it reads
   correctly with zero rows (both covers meet, no shadow shows, matching
   an already-fully-visible table) and updates for free as the user
   scrolls, no scroll listener required. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin-bottom: var(--space-2);
  background-repeat: no-repeat;
  background-color: var(--colour-paper);
  background-image:
    linear-gradient(to right, var(--colour-paper) 30%, rgba(255, 246, 229, 0)),
    linear-gradient(to left, var(--colour-paper) 30%, rgba(255, 246, 229, 0)),
    linear-gradient(to right, rgba(28, 27, 26, 0.18), rgba(28, 27, 26, 0)),
    linear-gradient(to left, rgba(28, 27, 26, 0.18), rgba(28, 27, 26, 0));
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-size: 28px 100%, 28px 100%, 10px 100%, 10px 100%;
  background-attachment: local, local, scroll, scroll;
}
.table-scroll > table { margin-bottom: 0; }
/* Issue #15, 1f: now a tab stop (tabindex="0" in admin.ejs/admin-users.ejs)
   so it needs its own focus-visible ring - the global `a, button, input`
   selector above does not cover a plain <div>. */
.table-scroll:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: -2px; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.9375rem; }
.admin-table th, .admin-table td { border-bottom: 1px solid var(--colour-rule); padding: 0.625rem 0.75rem; text-align: left; vertical-align: top; }
.admin-table th {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--colour-muted);
  border-bottom-color: var(--colour-graphite);
}
.admin-table th a { color: inherit; text-decoration: none; }
.admin-table th a:hover { color: var(--colour-redline); }
.admin-table__num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 0.875rem; }
.admin-table__status { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; }
/* The one row highlight: a pale redline wash, kept because triage has to
   scan for stuck learners at a glance. Icon + text carry the state too. */
.admin-table__row--stuck { background: var(--colour-redline-wash); }
.admin-table__row--stuck .admin-table__status { color: var(--colour-redline); }
.admin-status-icon { margin-right: 0.25rem; }
.admin-pagination { display: flex; gap: var(--space-1); align-items: center; margin-top: var(--space-2); font-family: var(--font-mono); font-size: 0.8125rem; }
#join-code { font-family: var(--font-mono); letter-spacing: 0.1em; }

.back-link {
  display: inline-block;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

/* Module page */
.module__explanation { max-width: 70ch; }
.module__callout {
  max-width: 70ch;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.125rem;
  line-height: 1.45;
  border-top: 1px solid var(--colour-rule);
  border-bottom: 1px solid var(--colour-rule);
  padding: var(--space-1) 0;
  margin: var(--space-2) 0;
}
.module__callout strong { font-family: var(--font-mono); font-style: normal; font-weight: 400; font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--colour-redline); margin-right: 0.5rem; }
/* Reference material, not the philosophical "why it matters" line above
   it: plain roman rather than italic, and its own paragraphs stay
   readable on separate lines instead of running together. Set in the body
   face: the only roman Newsreader shipped is the 600 heading weight, so a
   roman callout in the display face rendered as a block of bold serif. */
.module__other-approaches { font-family: var(--font-body); font-style: normal; font-size: 1rem; line-height: 1.5; }
.module__other-approaches p { margin: 0 0 0.5rem; }
.module__other-approaches p:last-child { margin-bottom: 0; }

/* Learn-more block: the "Want to learn more?" link to Anthropic's own
   page for the module's feature (#219). Sits between the callouts and the
   walkthrough as a quiet aside, mono eyebrow like the callout labels,
   body face for the sentence, so it reads as a pointer rather than a
   third callout. */
.learn-more { max-width: 70ch; margin: var(--space-2) 0; padding: var(--space-1); background: var(--colour-redline-wash); }
.learn-more__title { margin: 0 0 0.25rem; font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--colour-redline); }
.learn-more__body { margin: 0; font-family: var(--font-body); font-size: 1rem; line-height: 1.5; }
.learn-more__link { color: var(--colour-graphite); text-decoration: underline; text-underline-offset: 0.15em; }
.learn-more__link:hover { color: var(--colour-redline); }

/* Walkthrough (a full numbered worked example, distinct from the module's
   single snippet). Steps deliberately reuse .code-window rather than a
   second code-display style. The closing outcome box is boxed on all
   sides with a status-icon checkmark, so it reads as "what you should see"
   rather than a repeat of the italic, rule-bounded .module__callout above. */
.walkthrough { margin: var(--space-3) 0; }
.walkthrough__title { margin-bottom: 0.25rem; }
.walkthrough__intro { color: var(--colour-muted); margin-bottom: var(--space-2); }
.walkthrough__steps { list-style: none; margin: 0; padding: 0; }
.walkthrough__step { margin-bottom: var(--space-2); }
.walkthrough__step-number { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-redline); }
.walkthrough__instruction { max-width: 70ch; margin: 0 0 0.5rem; }
.walkthrough__outcome {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  max-width: 70ch;
  border: 1px solid var(--colour-rule);
  background: var(--colour-white);
  padding: var(--space-1) var(--space-2);
  margin: var(--space-2) 0;
}
.walkthrough__outcome-icon { flex-shrink: 0; margin-top: 0.3rem; color: var(--colour-graphite); }
.walkthrough__outcome-text strong {
  display: block;
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--colour-redline);
  margin-bottom: 0.25rem;
}
.walkthrough__outcome-text p { margin: 0; }

/* Code window. The traffic-light dots are a recognisable convention and
   stay; they are the only non-token colours on the page. Resizable vertically
   with a drag handle in the bottom-right corner; horizontal overflow scrolls
   within the window only, never the page. Short snippets size to content; long
   ones cap at max-height and become resizable. */
.code-window {
  border: 1px solid var(--colour-rule);
  margin: var(--space-2) 0;
  background: var(--colour-white);
  max-width: 70ch;
  overflow: hidden;
  resize: vertical;
  min-height: 150px;
  height: auto;
  max-height: 350px;
  display: flex;
  flex-direction: column;
}
.code-window__header {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--colour-graphite);
  color: var(--colour-paper);
  padding: 0.5rem 0.75rem;
  flex-shrink: 0;
}
.code-window__dots { display: inline-flex; gap: 0.35rem; }
.code-window__dots span {
  width: 0.6rem;
  height: 0.6rem;
  display: inline-block;
  background: var(--colour-paper);
}
.code-window__dots span:nth-child(1) { background: #e5533d; }
.code-window__dots span:nth-child(2) { background: #e0a83a; }
.code-window__dots span:nth-child(3) { background: #6bb08a; }
.code-window__filename {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  flex: 1;
}
.code-window__copy {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: transparent;
  color: var(--colour-paper);
  border: 1px solid var(--colour-paper);
  padding: 0.25rem 0.6rem;
  cursor: pointer;
}
.code-window__copy--done { background: var(--colour-paper); color: var(--colour-graphite); }
.code-window__body {
  margin: 0;
  padding: var(--space-2);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  overflow-x: auto;
  overflow-y: auto;
  white-space: pre;
  flex: 1;
  -webkit-overflow-scrolling: touch;
}
/* Issue #15, 1e: now a tab stop (tabindex="0" in code-window.ejs) so it
   needs its own focus-visible ring - the global `a, button, input`
   selector above does not cover a <pre>. */
.code-window__body:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: -2px; }

.task { margin-top: var(--space-3); border-top: 1px solid var(--colour-rule); padding-top: var(--space-2); }
.task input[type="text"] {
  display: block;
  width: 100%;
  max-width: 32rem;
  font-family: var(--font-mono);
  font-size: 1rem;
  padding: 0.6rem;
  margin: var(--space-1) 0;
  border: 1px solid var(--colour-graphite);
  background: var(--colour-white);
  color: var(--colour-graphite);
}
.task form button[type="submit"] { margin-top: var(--space-1); }
.task__result { font-family: var(--font-mono); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: var(--space-1); }
.task__result--correct { color: var(--colour-graphite); }
.task__result--incorrect { color: var(--colour-redline); }
.task__followup { max-width: 70ch; color: var(--colour-muted); }

.auth-page { max-width: 420px; }
.auth-form { display: flex; flex-direction: column; gap: 0.5rem; }
.auth-form label { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--colour-muted); margin-top: var(--space-1); }
.auth-form input, .auth-form textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.6rem;
  border: 1px solid var(--colour-graphite);
  background: var(--colour-white);
  color: var(--colour-graphite);
}
/* Only the request-access message uses one; vertical-only resize keeps it
   inside the 420px column. */
.auth-form textarea { resize: vertical; }
.auth-form button { margin-top: var(--space-1); align-self: flex-start; }
/* #209 stage 3: apply-admin.ejs's two fieldsets are the only .auth-form
   content that sits inside a <fieldset>, not as a direct child of
   .auth-form itself. Every other field on the site gets its block layout
   for free from flex-item auto-blockification (a direct child of a flex
   container is blockified even if the element - label, in this case - is
   inline by default), which is why a plain `.auth-form label` needs no
   explicit `display` anywhere else. A fieldset's children never see that:
   they are grandchildren of .auth-form, not children, so the radio's
   label and the following field's label stayed genuinely inline and ran
   together as one phrase ("CREATE A NEW ORG" + "ORGANISATION NAME" read
   as "CREATE A NEW ORG ORGANISATION NAME"), and the text input - never
   blockified either - fell back to its ~200px intrinsic width instead of
   matching every other input on the page. Confirmed live at 375px and at
   1280px: the run-together text and the narrow input are identical at
   both, because .auth-page's own 420px cap is what wraps it, not the
   viewport - this is a pre-existing layout bug, not a narrow-width-only
   one, fixed here (not gated behind the 640px breakpoint) because #209's
   fix criteria is "forms that don't lay out sensibly", which this is at
   any width, and it reads worst exactly where this stage is looking. */
.auth-fieldset {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  border: 1px solid var(--colour-rule);
  padding: var(--space-1);
  margin: 0;
}
.auth-fieldset legend {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--colour-muted);
  padding: 0 0.4rem;
}
/* The radio and its own label stay one row (unlike the field label/input
   pair below them, which each get their own line from the flex column
   above) - a direct flex row is a smaller, more targeted fix than
   reworking .auth-form label's shared margin-top rule. */
.auth-radio { display: flex; align-items: center; gap: 0.5rem; }
.auth-radio label { margin-top: 0; }
/* The "Need an account?" / "Already have an account?" line and the OAuth
   buttons under a form get their own room instead of sitting on the
   submit button (Rodney: cramped). */
.auth-page > p { margin-top: var(--space-1); }
.auth-form + p { margin-top: var(--space-2); }
.auth-error {
  border: 1px solid var(--colour-redline);
  color: var(--colour-redline);
  padding: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.auth-notice {
  border: 1px solid var(--colour-graphite);
  color: var(--colour-graphite);
  padding: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.account-methods { list-style: none; padding: 0; margin: var(--space-2) 0; display: flex; flex-direction: column; gap: var(--space-1); }
.account-method { display: flex; align-items: center; justify-content: space-between; gap: var(--space-1); border: 1px solid var(--colour-rule); padding: 0.75rem; }
.account-method__name { display: inline-flex; align-items: center; gap: 0.6rem; }
.account-method__state { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--colour-muted); }
.account-method form { margin: 0; }
.auth-hint { font-family: var(--font-mono); font-size: 0.75rem; color: var(--colour-muted); margin-top: -0.25rem; }
.auth-consent {
  border: 1px solid var(--colour-rule);
  padding: var(--space-1);
  margin: var(--space-1) 0;
}
.auth-consent legend { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--colour-redline); padding: 0 0.4rem; }
.auth-consent__checkbox { display: flex; align-items: flex-start; gap: 0.5rem; margin-top: var(--space-1); }
.auth-consent__checkbox input { margin-top: 0.2rem; }
.auth-consent__checkbox label { font-family: var(--font-body); font-size: 1rem; text-transform: none; letter-spacing: 0; color: var(--colour-graphite); margin: 0; }

/* The masthead's two-column grid needs room for the title block beside the
   headline; below this it stacks, and the block sits under the CTAs. */
@media (max-width: 900px) {
  .landing-header { grid-template-columns: 1fr; }
  .landing-spec { min-width: 0; max-width: 26rem; margin-top: var(--space-2); }
}

@media (max-width: 640px) {
  .page,
  main.skilltree-shell { padding: var(--space-2) var(--space-1); }
  .landing { padding: var(--space-2) var(--space-1) 0; }
  h1 { font-size: 2rem; }
  .module-row a { grid-template-columns: 2.5rem 1fr; row-gap: 0.5rem; }
  .module-row__state { grid-column: 2; justify-self: start; }
  .module-row__icon { display: none; }
}

/* Stream sections on the learner list */
.stream-section { margin-top: var(--space-3); }
.stream-section__heading {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin: 0;
}
/* List view carries the same stream hue: a swatch beside the heading and
   the row glyph in the hue, so the two views read as one system. */
.stream-section__swatch { display: inline-block; width: 0.6em; height: 0.6em; margin-right: 0.45em; vertical-align: 0.05em; border: 1px solid var(--colour-graphite); }
.stream-section--default .stream-section__swatch { background: var(--stream-default); }
.stream-section--business .stream-section__swatch { background: var(--stream-business); }
.stream-section--advanced .stream-section__swatch { background: var(--stream-advanced); }
.stream-section--practice .stream-section__swatch { background: var(--stream-practice); }
.stream-section--web-dev .stream-section__swatch { background: var(--stream-web-dev); }
.stream-section--app-dev .stream-section__swatch { background: var(--stream-app-dev); }
.stream-section--content-creation .stream-section__swatch { background: var(--stream-content-creation); }
.stream-section--homelab-ops .stream-section__swatch { background: var(--stream-homelab-ops); }
.stream-section--student .stream-section__swatch { background: var(--stream-student); }
.stream-section--default .module-row__icon { color: var(--stream-default); }
.stream-section--business .module-row__icon { color: var(--stream-business); }
.stream-section--advanced .module-row__icon { color: var(--stream-advanced); }
.stream-section--practice .module-row__icon { color: var(--stream-practice); }
.stream-section--web-dev .module-row__icon { color: var(--stream-web-dev); }
.stream-section--app-dev .module-row__icon { color: var(--stream-app-dev); }
.stream-section--content-creation .module-row__icon { color: var(--stream-content-creation); }
.stream-section--homelab-ops .module-row__icon { color: var(--stream-homelab-ops); }
.stream-section--student .module-row__icon { color: var(--stream-student); }
.stream-section .module-row--locked .module-row__icon, .stream-section .module-row--restricted .module-row__icon { opacity: 0.55; }
.stream-section__count { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--colour-muted); }
.stream-section .module-list { margin-top: var(--space-1); }

/* Stream tag and per-stream toggles on the graph */
.graph-node__stream { font-size: 0.625rem; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--colour-muted); }
.graph-node--locked .graph-node__stream, .graph-node--restricted .graph-node__stream { fill: var(--colour-muted); }

/* Admin cohort path */
.path-table { width: 100%; border-collapse: collapse; font-size: 0.9375rem; margin-bottom: var(--space-3); }
.path-table th, .path-table td { border-bottom: 1px solid var(--colour-rule); padding: 0.625rem 0.75rem; text-align: left; vertical-align: middle; }
.path-table th {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--colour-muted);
  border-bottom-color: var(--colour-graphite);
}
.path-table__stream td { font-family: var(--font-display); font-weight: 600; font-size: 1.125rem; padding-top: var(--space-2); border-bottom-color: var(--colour-graphite); }
.path-table__num { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-redline); white-space: nowrap; }
.path-table__mode { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; }
.path-table__mode .status-icon { margin-right: 0.25rem; }
.path-table__row--excluded td { color: var(--colour-muted); }
.path-table__row--excluded .path-table__mode { color: var(--colour-redline); }
.path-table__lock-warning { font-size: 0.8125rem; }
.path-table__lock-warning-text { color: var(--colour-redline); }
.path-table__form { display: flex; align-items: center; gap: 0.5rem; }
.path-table__form select {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--colour-graphite);
  background: var(--colour-white);
  color: var(--colour-graphite);
}
.path-table__form .btn { padding: 0.35rem 0.75rem; font-size: 0.75rem; }
.path-saved {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  border: 1px solid var(--colour-graphite);
  padding: 0.5rem 0.75rem;
  margin-bottom: var(--space-1);
  max-width: 70ch;
}
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

/* Post-pass handoff (dispatch slip): the task passed, here is the piece to
   take away and where it goes. One redline rule across the top is the
   only accent; the body stays paper and hairlines like the rest of the
   manual. Closing always goes to "/" (see handoff.js). */
.handoff {
  width: min(44rem, calc(100vw - 2 * var(--space-2)));
  max-height: calc(100vh - 2 * var(--space-2));
  overflow: auto;
  margin: auto;
  padding: var(--space-2) var(--space-3) var(--space-3);
  border: 1px solid var(--colour-graphite);
  border-top: 3px solid var(--colour-redline);
  background: var(--colour-paper);
  color: var(--colour-graphite);
}
.handoff::backdrop { background: rgba(28, 27, 26, 0.55); }
.handoff .eyebrow { margin-bottom: 0.5rem; }
.handoff__title { margin-bottom: 0.5rem; }
.handoff__lede { margin: 0 0 var(--space-1); color: var(--colour-muted); }
.handoff .code-window { max-width: none; }
.handoff__where {
  margin: var(--space-2) 0 0.5rem;
  padding-top: var(--space-1);
  border-top: 1px solid var(--colour-rule);
  max-width: 70ch;
}
.handoff__label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--colour-redline);
  margin-bottom: 0.35rem;
}
.handoff__steps { margin: 0 0 var(--space-2); padding-left: 1.25rem; max-width: 70ch; }
.handoff__steps li { margin-bottom: 0.5rem; }
.handoff__steps li::marker { font-family: var(--font-mono); color: var(--colour-muted); }
.handoff__actions { margin: 0; }
@media (max-width: 640px) {
  .handoff { padding: var(--space-1) var(--space-2) var(--space-2); }
}

/* Skill-tree header with the graph CTA: title left, a small schematic of
   the graph itself on the right. The schematic is the whole pitch, drawn
   in the same hairline-and-elbow language as the real graph. */
.tree-header { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--space-2); flex-wrap: wrap; }
.tree-header .meta { margin-bottom: 0; }
.graph-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.875rem 1.25rem 0.875rem 1rem;
  border: 1px solid var(--colour-graphite);
  color: var(--colour-graphite);
  text-decoration: none;
  margin-bottom: 0.25rem;
}
.graph-cta:hover, .graph-cta:focus-visible { border-color: var(--colour-redline); color: var(--colour-redline); text-decoration: none; }
.graph-cta__schematic { flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; }
.graph-cta__schematic rect { fill: var(--colour-white); }
.graph-cta__tip { fill: currentColor; stroke: none; }
.graph-cta__text { display: flex; flex-direction: column; gap: 0.2rem; }
.graph-cta__label { font-family: var(--font-mono); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
.graph-cta__label::before { content: "[ "; }
.graph-cta__label::after { content: " ]"; }
.graph-cta__hint { font-family: var(--font-display); font-style: italic; font-weight: 400; font-size: 0.9375rem; color: var(--colour-muted); }
.graph-cta:hover .graph-cta__hint { color: inherit; }
@media (max-width: 640px) {
  .tree-header { align-items: flex-start; }
  .graph-cta { width: 100%; justify-content: flex-start; }
}

/* Footer: three-column link block plus a bottom identity bar, on every
   page. Same hairline-rule, mono-label vocabulary as the rest of the
   design system rather than a new visual language. */
.footer {
  margin-top: var(--space-5);
  border-top: 1px solid var(--colour-rule);
  padding: var(--space-3) var(--space-2) var(--space-2);
}
.footer__columns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  max-width: 960px;
  margin: 0 auto;
}
.footer__col { min-width: 160px; }
.footer__heading {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-muted);
  margin: 0 0 0.75rem;
}
.footer__links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.footer__links a { color: var(--colour-graphite); text-decoration: none; font-size: 0.9375rem; }
.footer__links a:hover { color: var(--colour-redline); text-decoration: underline; }
.footer__bottom {
  max-width: 960px;
  margin: var(--space-2) auto 0;
  padding-top: var(--space-1);
  border-top: 1px solid var(--colour-rule);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--colour-muted);
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
}
.footer__bottom p { margin: 0; display: inline-flex; align-items: center; gap: 0.6rem; }
.footer__bottom a { color: inherit; }
.footer__sha { font-variant-numeric: tabular-nums; }
.footer__gh { display: inline-flex; align-items: center; gap: 0.4rem; text-decoration: none; }
.footer__gh:hover { color: var(--colour-redline); text-decoration: underline; }
.footer__gh .brand-icon { width: 16px; height: 16px; }

/* The "what's inside" stream summary and the secondary sign-in section.
   Both sit inside .landing, so their gutters and axis come from the one
   shared container (they used to cap at 700px against a 1200px header,
   one of the misalignments the landing recomposition removed). Counts are
   graphite, not redline: redline stays punctuation and the gap state, and
   each stream's swatch already carries its meaning, tying the summary to
   the figure's key. */
.landing-streams { margin: var(--space-4) 0 0; }
.landing-streams__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin: var(--space-1) 0 var(--space-2);
}
.landing-streams__count {
  font-family: var(--font-mono);
  font-size: 2rem;
  color: var(--colour-graphite);
  margin: 0;
  line-height: 1;
}
.landing-streams__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.0625rem;
  margin: 0.375rem 0 0;
}
.landing-streams__swatch { display: inline-block; margin-right: 0.5rem; }
.landing-streams__desc {
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--colour-muted);
  margin: 0.375rem 0 0;
  max-width: 32ch;
}
.landing-signin { margin: var(--space-4) 0 0; padding: var(--space-2) 0 0; border-top: 1px solid var(--colour-rule); }

.auth-success {
  border: 1px solid var(--colour-graphite);
  color: var(--colour-graphite);
  padding: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  margin-bottom: var(--space-1);
}

.admin-table__row--flagged { color: var(--colour-muted); }

/* Phone width: tables tighten, the path form stays on one line inside its
   scroll container, and the CTA card's schematic gives way to the label. */
@media (max-width: 640px) {
  .admin-table th, .admin-table td, .path-table th, .path-table td { padding: 0.5rem 0.5rem; font-size: 0.875rem; }
  .path-table__form { display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; }
  .path-table__form select { min-width: 6.5rem; }
  .graph-cta__schematic { width: 96px; height: 42px; }
}


/* Path subscription: the bar above the graph and the panel on the list.
   #327 follow-up: a plain line, not a bordered panel - the graph header
   already carries a progress bar, a legend and a chip row in boxes and
   borders of their own; one more box for one sentence read every visit
   was the wrong amount of ceremony for what it says. */
.path-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin: 0.6rem 0; font-family: var(--font-mono); font-size: 0.85rem; }
.path-bar__status { margin: 0; }
.path-bar__label { color: var(--colour-muted); }
.path-bar__form { margin: 0; }
.path-bar__target { font-weight: 400; }
.btn--small { font-size: 0.8rem; padding: 0.3rem 0.6rem; }
.path-panel { margin: 1.5rem 0; padding: 0.9rem 1.1rem; border: 1px solid var(--colour-rule); background: var(--colour-white); }
.path-panel__heading { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--colour-muted); margin: 0 0 0.4rem; }
.path-panel__status, .path-panel__next { margin: 0.25rem 0; }
.path-panel__form { margin-top: 0.6rem; }
.module-row__on-path { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 400; color: var(--colour-redline); margin-left: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase; }
.graph-node--on-path .graph-node__box { stroke: var(--colour-redline); }

/* Scroll cues: a chevron in a gutter either side of the graph, outside the
   picture (Rodney: overlapping it looked wrong), visible only while there
   is more graph that way, keeping its space when not so the graph never
   shifts. Field-manual register: thin muted stroke, no fill, no fade. */
.graph-viewport { display: grid; grid-template-columns: 52px minmax(0, 1fr) 52px; align-items: center; column-gap: 8px; }
.graph-cue { width: 40px; height: 40px; padding: 0; border: 1px solid var(--colour-rule); background: var(--colour-paper); color: var(--colour-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; justify-self: center; }
.graph-cue[hidden] { display: flex; visibility: hidden; }
.graph-cue--left { order: -1; }
.graph-cue svg { display: block; }
.graph-cue path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: square; }
.graph-cue:hover { color: var(--colour-redline); border-color: var(--colour-redline); }
.graph-cue:focus-visible { outline: 2px solid var(--colour-redline); outline-offset: 2px; }

/* Module page head: title and description, with the module's spot
   illustration beside them where one has shipped. */
.module-head { display: flex; align-items: flex-start; gap: var(--space-3); }
.module-head > div { flex: 1 1 auto; min-width: 0; }
/* #377 spacing pass: scoped to this one page (a new class on admin-users.
   ejs's <main>, not a change to .module-head itself, which 14 other views
   share) - the gap into the table below relied entirely on the closing
   <p class="prose">'s own 1rem bottom margin, cramped next to an 11-column
   table. A bare `.module-head` change would have shifted every page that
   uses it, most of which are auth flows well outside this issue's scope. */
.page--admin-users .module-head { margin-bottom: var(--space-3); }
/* The spot sits on a plate: the illustration's own cream, a hairline graphite
   border, and the same lift the primary button has (crisp offset plus a soft
   diffuse shadow) so it reads as a mounted print rather than a picture pasted
   flat on the paper. Graphite, not redline: redline means "press me". */
.module-spot {
  flex: 0 0 auto; width: 160px; height: 160px; margin-top: 0.25rem;
  box-sizing: border-box; padding: 4px;
  background: #fffcf7;
  border: 1px solid var(--colour-graphite);
  box-shadow:
    2px 2px 0 var(--colour-graphite),
    6px 8px 12px rgba(28, 27, 26, 0.1);
}
@media (max-width: 640px) { .module-head { flex-direction: column-reverse; } .module-spot { width: 120px; height: 120px; } }

/* #327 follow-up: the graph header's own illustration, smaller than every
   other page's .module-spot (the shared class and its 160px are untouched
   - module.ejs, the auth pages, admin-users.ejs and no-organisation.ejs
   all keep the full size).
   The graph page has a progress bar, a legend and a nine-chip row to fit
   before the fold that no other .module-spot page carries, so this is the
   one place the illustration itself is the cheapest thing to shrink. */
.module-spot--graph-header { width: 88px; height: 88px; }
@media (max-width: 640px) { .module-spot--graph-header { width: 72px; height: 72px; } }

/* "You have completed N of M" (#215): a plain mono status line, same
   register as the graph/list progress labels, not a second visual system. */
.module-progress { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-muted); margin: 0.25rem 0 1.5rem; }
.module-progress a { color: inherit; }

/* Auth pages sit in a 420px column, so their spot is a smaller version of
   the module one: sits level with the heading, not beside a whole intro. */
.auth-head { align-items: center; margin-bottom: var(--space-1); }
.auth-head h1 { margin: 0; flex: 1 1 auto; }
.auth-head .module-spot { width: 96px; height: 96px; margin-top: 0; }
@media (max-width: 640px) { .auth-head { flex-direction: row; } .auth-head .module-spot { width: 80px; height: 80px; } }

/* BugPilot widget theme. The widget injects its own stylesheet into <head>
   at init, after this file, with its defaults declared on :root, so an
   override here on :root would lose on source order. Declaring on body
   wins instead: the trigger and dialog are body's children and take the
   nearest ancestor's value. Same tokens as the rest of the app, so the
   tab and form read as part of the field manual, not a bolted-on widget:
   graphite ink, paper and white surfaces, hairline graphite border, square
   corners, no drop shadow (the app's only shadows are the deliberate
   "mounted print" offsets on cards, which a floating tab should not copy).
   z-index: the app's highest own layer is the hero tooltip at 10 and there
   is no modal or dialog layer to stay under, so 100 clears page content
   with headroom for anything short of a future modal, which should then
   sit above it. The widget's own default is 2147483647, which would win
   over everything forever. */
body {
  --bp-primary: var(--colour-graphite);
  --bp-primary-hover: var(--colour-redline);
  --bp-surface: var(--colour-paper);
  --bp-surface-raised: var(--colour-white);
  --bp-border: var(--colour-graphite);
  --bp-border-width: 1px;
  --bp-text: var(--colour-graphite);
  --bp-text-muted: var(--colour-muted);
  --bp-required: var(--colour-redline);
  --bp-error: var(--colour-redline);
  --bp-radius: 0;
  --bp-radius-sm: 0;
  --bp-trigger-radius: 0;
  --bp-shadow: none;
  --bp-trigger-shadow: none;
  --bp-font: var(--font-body);
  --bp-z-index: 100;
}

/* My reports (issue #258). A report is a card in the same hairline
   register as the admin table: rule between entries, mono for the number
   and the ladder, no shadow. The ladder is five stops drawn as a row of
   labels with a rule between them; reached stops are ink, the current one
   is redline, the rest muted. */
.report-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.report { padding: var(--space-2) 0; border-top: 1px solid var(--colour-rule); }
.report:last-child { border-bottom: 1px solid var(--colour-rule); }
.report__head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.report__title { font-size: 1.125rem; margin: 0; }
.report__number { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--colour-muted); }
.ladder { list-style: none; display: flex; flex-wrap: wrap; gap: 0; margin: 0.75rem 0; padding: 0; }
.ladder__step {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--colour-muted);
  padding: 0.25rem 0.75rem 0.25rem 0;
  margin-right: 0.75rem;
  border-bottom: 2px solid var(--colour-rule);
}
.ladder__step--done { color: var(--colour-graphite); border-bottom-color: var(--colour-graphite); }
.ladder__step--current { color: var(--colour-redline); border-bottom-color: var(--colour-redline); }
.report__reply { margin: 0.75rem 0 0; padding: 0.75rem 1rem; border-left: 2px solid var(--colour-graphite); background: var(--colour-white); max-width: 70ch; white-space: pre-line; }
.report__reply p { margin: 0; }
.report__reply-label { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--colour-muted); margin: 0 0 0.375rem; }
.report__pending { font-family: var(--font-mono); font-size: 0.75rem; color: var(--colour-muted); margin: 0.5rem 0 0; }
