/* XLab Verification — the palette and chrome the standalone verification
   subdomain pages run on. Two themes, day and night, over one set of variable
   names, so a rule that reads --primary or --border follows the switch
   without being touched. Both carry XLab's maroon.

   Trap: maroon at #800000 is a fine SURFACE on dark (white on it reads at
   12:1) but not fine as TEXT on dark (2.3:1). That is why --brand-ink exists
   separately from --primary: filled things use --primary, written things use
   --brand-ink, and the two differ per theme.

   Module hues run Chinese Red - Satsuma - Lunar Yellow - Khaki - Cobalt: one
   family with the brand in it rather than a general-purpose data palette
   sitting next to it. Burgundy anchors that palette at OKLCH hue 23 and
   --primary is at 29, which is why these belong together and the Okabe-Ito
   set did not.
   Each theme keeps the hue and solves lightness for its own ground, so the
   day values are the same five colours darkened to carry as text on paper,
   not different colours. Every one clears 4.5:1 where it is set (day 4.6-8.5,
   night 5.2-6.9); the old day palette cleared it on
   one of five.
   They stay decorative: warm neighbours converge for a protanope, so
   everywhere a module colour appears the module number appears as text beside
   it, and nothing is encoded by hue alone. */

:root,
:root[data-theme="light"] {
  --background: #fbfaf9;
  --foreground: #1a1614;
  --card: #ffffff;
  --primary: #800000;
  --primary-foreground: #ffffff;
  --brand-ink: #800000;
  --muted: #f2efed;
  --muted-foreground: #5f5854;
  --accent: #f2efed;
  --border: #e4dedb;
  /* The learner's own marker (::highlight(vt-hl)): Kindle's pink marker,
     per the owner — which is also the blush from the brand's palette board
     (red · blue · wine · blush), so it stays in the family. Day ink clears
     ~11:1 on it. Each theme re-solves the pair. */
  --hl: #fac8d2;
  --hl-ink: var(--foreground);
  --ring: #800000;

  --mod-0: #970a12; /* Chinese Red */
  --mod-1: #d1601f; /* Satsuma */
  --mod-2: #fecd6d; /* Lunar Yellow */
  --mod-3: #6d7636; /* Khaki */
  --mod-4: #022e4c; /* Prussian */
  /* Text on that fill. Fixed, because it depends on the fill and not on the
     page ground — so the ink follows the fill's lightness and lands dark on
     the two light hues. Satsuma is the near miss to watch: white on it is
     3.9:1, under the 4.5 its 13px chips need, and the same near-black Lunar
     Yellow already uses clears it at 4.6. Khaki passes on white at 4.9 with
     nothing to spare, so re-solving any of these five fills means re-checking
     its ink in the same edit. */
  --mod-0-ink: #ffffff;
  --mod-1-ink: #1a1614;
  --mod-2-ink: #1a1614;
  --mod-3-ink: #ffffff;
  --mod-4-ink: #ffffff;
  /* The same accent where it has to be type on the page rather than a fill.
     Forcing five hues to 4.5:1 as text compresses them into one lightness
     band and Satsuma stops being tellable from Lunar Yellow — so the fill
     carries the identity and this carries the reading. */
  --mod-0-text: #9a000c;
  --mod-1-text: #bf4f00;
  --mod-2-text: #946b00;
  --mod-3-text: #555e07;
  --mod-4-text: #3d75b1;

  /* Graded feedback. Wong's bluish-green and vermillion, never the red/green
     pair — and never on their own: every use is accompanied by the word
     (Correct / Not quite) and a ✓ or ✕.*/
  --ok: #007c5a;
  --no: #b34700;

  /* Real errors only — a form that will not submit, a widget that
     failed. Undefined here, it fell through to the app's red-600 and
     was the one un-themed colour on the page. It is --no rather than
     the maroon so an error still reads as an error and not as brand. */
  --destructive: #b34700;

  /* Okabe-Ito data-encoding accents for the Verification widgets. They live
     here, not only in globals.css, for the same reason --destructive does:
     this file re-points the palette on the Verification routes, and a token it
     does not define keeps its app default on every theme. Left un-themed these
     five sat at one fixed value over a white, a near-black and a black ground.

     Each value clears 4.5:1 against the worst surface of its own theme —
     the darkest one where the ink is dark, the lightest where it is light.
     Where the set carries a light counterpart it is used (blue -> sky blue);
     where it does not, the same hue is darkened rather than swapped, so the
     five stay separable for a colour-blind reader. Hue never carries the
     meaning alone in any case — the widgets pair it with a label. */
  /* The widgets' semantic accents. Same five colours as the module palette,
     assigned so the meaning each one carried survives the move: conceal stays
     blue, overstate stays amber, comply stays the green end, defect stays red.
     These are ink and 10%-tints, never plaques, so they take the -text
     variants. Hue never carries the meaning alone — every widget pairs it with
     its label. */
  --comply: #555e07; /* Khaki */
  --defect: #9a000c; /* Chinese Red */
  --hide: #3d75b1; /* Cobalt */
  --exaggerate: #946b00; /* Lunar Yellow */
  --free-ride: #bf4f00; /* Satsuma */

  /* Type scale. Ten steps, and every rule on these pages picks one — the sizes
     used to run to 22 distinct values between 10 and 27px, eight of them at
     half-pixels that render differently per zoom and DPI. Steps are integers
     for the same reason.

     --fs-title is every page's h1; --fs-hero is the landing's alone. They were
     five near-identical clamps before, which is four chances to drift. */
  --text-scale: 1;
  --fs-2xs: 0.75rem;
  --fs-xs: 0.8125rem;
  --fs-sm: 0.875rem;
  --fs-md: 1rem;
  --fs-lg: 1.125rem;
  --fs-xl: 1.25rem;
  --fs-2xl: 1.5rem;
  --fs-3xl: 1.875rem;
  --fs-4xl: 2.375rem;
  --fs-title: clamp(1.75rem, 3.4vw, 2.25rem);
  /* The landing headline is a full policy question, not a one- or two-word
     campaign title. Keep it distinct from an ordinary page h1 without asking
     three long lines to carry display-poster proportions. */
  --fs-hero: clamp(2rem, 3.8vw, 2.75rem);

  --radius: 14px;
  --radius-sm: 8px;
  --radius-xs: 4px;
  --hairline: 1px;
  --shadow-soft: 0 1px 2px rgba(10, 25, 47, 0.06);
  --shadow-soft-lg: 0 12px 40px rgba(10, 25, 47, 0.13);

  /* Two families: Space Grotesk for headings and display numerals, Inter for
     the app's body and interface text. The static lift does not expose Next's
     local-font variable, so it falls back to the platform sans stack.

     --label is the micro-label face: the eyebrows, counters, key-value rows
     and status chips. It resolves to the sans, and the name says what it
     selects rather than what it used to be — it was called --mono and named
     JetBrains Mono and IBM Plex Mono, neither of which this site ships, so
     every micro-label fell through to whatever ui-monospace the OS supplies
     and the pages set themselves in a different third face on macOS, Windows
     and Linux. The labels still read as labels: they keep their uppercase and
     letterspacing, which is what was doing the work, not the face.

     It stays a token rather than being deleted because it is a real slot: the
     landing's .sky-panel re-points it at the Gotham stack, so the labels
     inside that panel follow the brand face where a machine has it licensed.
     Point it at a shipped face here and every one of those labels follows.

     Trap: the mono face was also holding counters and fractions in column.
     Losing it would let digits jitter as they change, so tabular figures are
     switched on globally below rather than per counter. */
  --display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --sans: var(--font-sans, -apple-system), BlinkMacSystemFont, "Segoe UI",
    system-ui, Helvetica, Arial, sans-serif;
  --label: var(--sans);
}

:root[data-theme="dark"] {
  --background: #14100f;
  --foreground: #f4f0ee;
  --card: #1c1817;
  --primary: #9d1b1b;
  --primary-foreground: #ffffff;
  --brand-ink: #eda3a3;
  --muted: #262120;
  --muted-foreground: #b0a7a3;
  --accent: #2e2827;
  --border: #332c2a;
  --ring: #eda3a3;
  /* The day marker's blush, re-solved for the dark ground: the board's wine
     — same rose family, lightness dropped until the night ink clears ≈8:1
     on it. */
  --hl: #6b3346;
  --hl-ink: var(--foreground);

  /* Fills and their ink are the day values: a plaque's legibility is set by
     the plaque, not by the page behind it. Only the type variant re-solves. */
  --mod-0-text: #e4584f;
  --mod-1-text: #e15f00;
  --mod-2-text: #c5952b;
  --mod-3-text: #828d41;
  --mod-4-text: #518ac7;

  --ok: #3ecfa4;
  --no: #ff8f4d;
  --destructive: #ff8f4d;

  /* Lightened against the dark ground; the assignment is the light block's. */
  --comply: #828d41;
  --defect: #e4584f;
  --hide: #518ac7;
  --exaggerate: #c5952b;
  --free-ride: #e15f00;

  --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-soft-lg: 0 12px 40px rgba(0, 0, 0, 0.5);
}

/* Aa is the explicit reader preference. It comes after the palette. The
   endpoint is not the default: WCAG's 200% is the size the page must survive, while the
   learner chooses the size that is comfortable to read. */
:root[data-text-scale="100"] [data-reading-surface] {
  --text-scale: 1;
  --fs-2xs: 0.75rem;
  --fs-xs: 0.8125rem;
  --fs-sm: 0.875rem;
  --fs-md: 1rem;
  --fs-lg: 1.125rem;
  --fs-xl: 1.25rem;
  --fs-2xl: 1.5rem;
  --fs-3xl: 1.875rem;
  --fs-4xl: 2.375rem;
  --fs-title: clamp(1.75rem, 3.4vw, 2.25rem);
  --fs-hero: clamp(2rem, 3.8vw, 2.75rem);
}
:root[data-text-scale="125"] [data-reading-surface] {
  --text-scale: 1.25;
  --fs-2xs: 0.9375rem;
  --fs-xs: 1.015625rem;
  --fs-sm: 1.09375rem;
  --fs-md: 1.25rem;
  --fs-lg: 1.40625rem;
  --fs-xl: 1.5625rem;
  --fs-2xl: 1.875rem;
  --fs-3xl: 2.34375rem;
  --fs-4xl: 2.96875rem;
  --fs-title: clamp(2rem, 4.25vw, 2.8125rem);
  --fs-hero: clamp(2.5rem, 4.75vw, 3.4375rem);
}
:root[data-text-scale="150"] [data-reading-surface] {
  --text-scale: 1.5;
  --fs-2xs: 1.125rem;
  --fs-xs: 1.21875rem;
  --fs-sm: 1.3125rem;
  --fs-md: 1.5rem;
  --fs-lg: 1.6875rem;
  --fs-xl: 1.875rem;
  --fs-2xl: 2.25rem;
  --fs-3xl: 2.8125rem;
  --fs-4xl: 3.5625rem;
  --fs-title: clamp(2.25rem, 5.1vw, 3.375rem);
  --fs-hero: clamp(3rem, 5.7vw, 4.125rem);
}
:root[data-text-scale="175"] [data-reading-surface] {
  --text-scale: 1.75;
  --fs-2xs: 1.3125rem;
  --fs-xs: 1.421875rem;
  --fs-sm: 1.53125rem;
  --fs-md: 1.75rem;
  --fs-lg: 1.96875rem;
  --fs-xl: 2.1875rem;
  --fs-2xl: 2.625rem;
  --fs-3xl: 3.28125rem;
  --fs-4xl: 4.15625rem;
  --fs-title: clamp(2.5rem, 5.95vw, 3.9375rem);
  --fs-hero: clamp(3.5rem, 6.65vw, 4.8125rem);
}
:root[data-text-scale="200"] [data-reading-surface] {
  --text-scale: 2;
  --fs-2xs: 1.5rem;
  --fs-xs: 1.625rem;
  --fs-sm: 1.75rem;
  --fs-md: 2rem;
  --fs-lg: 2.25rem;
  --fs-xl: 2.5rem;
  --fs-2xl: 3rem;
  --fs-3xl: 3.75rem;
  --fs-4xl: 4.75rem;
  --fs-title: clamp(2.75rem, 6.8vw, 4.5rem);
  --fs-hero: clamp(3rem, 8.8vw, 6.5rem);
}

/* A long word at 72px is wider than a 320px screen, and a heading that cannot
   break is a horizontal scrollbar. Breaking is the lesser harm. */
:root.reader-large {
  overflow-wrap: break-word;
}

/* The learner's marker, painted by highlight.js through the Custom Highlight
   API — a pseudo, not an element, so this is the one place its colours can
   be said. Both values re-solve per theme above; a browser without the API
   never registers the name and the rule is inert. */
::highlight(vt-hl) {
  background-color: var(--hl);
  color: var(--hl-ink);
}

/* 1.4.10, the rest of it: doubling the type does not move a layout, but it
   does overrun the two things that were told never to give way — a line held
   to one line, and a row told not to wrap. Both are fine at the default size
   and both are a sideways scrollbar at twice it, so this mode releases them.
   A data table is the one thing left scrolling in its own box, which the
   criterion allows and the house rules already require. */
:root.reader-large
  :is(
    .brand,
    .nav a,
    .chip,
    .counter,
    .frac,
    .open-cue,
    .nb-count,
    .lock,
    .status,
    .track-head .name,
    .track-head .n
  ) {
  white-space: normal;
}

:root.reader-large
  :is(
    .brand,
    .header-right,
    .guide-tools,
    .rt-row,
    .sort,
    .modes,
    .progress-row,
    .gmod > .head,
    .control-row,
    /* The bank's per-track band: a label, a growing rule, a count. The rule
       gives way, the two labels do not, and at twice the type their combined
       min-content is wider than a 320px screen. */
    .track-head,
    /* A capstone card's top line — difficulty beside status. */
    .card-top
  ) {
  flex-wrap: wrap;
}

/* A grid item's automatic minimum size is its min-content, not zero, so a
   card whose longest word is wider than its column widens the column instead
   of wrapping — and the bank's grid is what then pushed the page sideways.
   Only the low-vision mode needs this: at the ordinary type size no card's
   min-content reaches a 320px column. */
:root.reader-large .bank-grid > * {
  min-width: 0;
}

/* A flex item's floor is its own content, and a form control's content is
   wider than it looks: an input reports the `size` attribute's twenty
   characters, a select its longest option. At twice the type size that floor
   is wider than the screen, and wrapping does not help — the item refuses to
   shrink, so the row wraps around something already too wide. Zero is the
   floor they need, and it has to reach the wrapper as well as the control:
   a label holding a select is the item that will not give, and `max-width:
   100%` on the select then resolves against the label's own overflow. */
:root.reader-large :is(input, select, textarea) {
  min-width: 0;
  max-width: 100%;
}
:root.reader-large
  :is(.control-row, .rt-row, .guide-tools, .sort)
  > * {
  min-width: 0;
}
/* The header pair is deliberately not in that list: it is sized by
   --hdr-action-*, and a floor of zero there would let the two come apart
   again. It reflows by wrapping instead. */

/* Label above value rather than beside it: an 88px label column beside a
   value is a two-column table, and at this size the value column has nothing
   left to hold a word. */
:root.reader-large .brief dl > div {
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
}

/* 2.5.5: 44x44 CSS px minimum for a pointer target. A minimum must not replace
   a component's layout model: forcing every button to inline-flex collapsed
   complex card-buttons into one horizontal line at enlarged text sizes. */
:root.reader-large button,
:root.reader-large [role="button"],
:root.reader-large .btn,
:root.reader-large .chip,
:root.reader-large .nav a,
:root.reader-large .site-footer a,
:root.reader-large .part-jump {
  min-height: 44px;
  min-width: 44px;
}

/* Memo-desk rows keep their authored grid explicitly; the generic target rule
   above now changes size only, never display. */
:root.reader-large .slot-btn {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  text-align: left;
}

/* The constellation is a scatter of labels positioned by trigonometry, not by
   layout; at this size it cannot be read, and the track page carries the same
   list as text. The "Open the skill map" link below it is outside this element
   on purpose — it is the way to the same graph and must survive.

   Trap: this selects the wrapper in landing.html, and a rename there silently
   turns the rule off rather than breaking anything visible. It had already
   happened once: the rule named `.sky-wrap`, which no longer exists, so the
   scatter was rendering here with 16px-tall targets. */
:root.reader-large .constellation {
  display: none;
}

/* One column, everywhere — now a choice rather than a repair. These used to be
   here because zoom left the media queries believing in a width the layout no
   longer had; with the layout reading the real viewport they fire correctly on
   their own. They stay because at 200% type a three-column grid holds a
   handful of words per line, and tracking from the end of one short line to
   the start of the next is the reading problem this mode exists to remove. */
:root.reader-large .objectives,
:root.reader-large .modules,
:root.reader-large .roster,
:root.reader-large .lh-row,
:root.reader-large .facts,
:root.reader-large .map-body,
:root.reader-large .desk,
:root.reader-large .cap-grid,
:root.reader-large .stat-row,
:root.reader-large .player .shell {
  grid-template-columns: minmax(0, 1fr) !important;
  /* One column only holds if the flow fills by row. A grid that fills by
     column (.objectives does, so its numbers read down the left) answers a
     single declared column by inventing implicit ones beside it, and the
     narrowest of those is the cramped line this mode exists to remove. */
  grid-auto-flow: row !important;
}

/* The header is a fixed-height single row by default; at this size its own
   contents no longer fit across, so it wraps like it does on a phone. */
:root.reader-large .site-header .bar {
  display: flex;
  height: auto;
  align-items: stretch;
  flex-direction: column;
  flex-wrap: wrap;
  gap: 10px 16px;
  padding: 8px 16px;
}

/* The course destinations are compact controls, not prose. Letting each link
   shrink and wrap at 200% produced six labels painted on top of one another.
   They keep their line and travel in one explicitly scrollable rail; the page
   itself never scrolls sideways. */
:root.reader-large .site-header .nav {
  width: 100%;
  flex: none;
  flex-wrap: nowrap;
  overflow-x: auto;
}
:root.reader-large .site-header .nav a {
  flex: none;
  white-space: nowrap;
}

:root.reader-large .site-header .brand {
  flex-wrap: nowrap;
  gap: 6px;
  font-size: var(--fs-md);
  white-space: nowrap;
}
:root.reader-large .site-header .brand-mark {
  height: 20px;
}

:root.reader-large .site-header .header-right {
  width: 100%;
}
/* A sticky side column has to stop being one when the column is gone. The rule
   above forces .desk to a single column, but the memo desk's two rails are
   made sticky inside `@media (min-width: 1080px)` — a query that still matches
   at this width — so they kept a viewport-tall max-height in a stacked layout
   and the writing surface was drawn straight through them.
   `.slot-rail`/`.check-rail` are named here because this rule said `.rail` and
   matched neither: the same silent miss the .constellation note above records,
   so both desk rails are spelled out rather than assumed. */
:root.reader-large .rail,
:root.reader-large .slot-rail,
:root.reader-large .check-rail {
  position: static;
  width: auto;
  max-width: none;
  height: auto;
  max-height: none;
  overflow: visible;
}

h1,
h2,
h3,
h4 {
  font-family: var(--display);
  font-weight: 500;
  letter-spacing: -0.015em;
  line-height: 1.15;
}

/* Chrome is not text — nothing pressable takes a selection. A blue smear over
   a theme glyph or a nav tab is always a mis-drag, and it reads as a broken
   page. Trap: `all: unset` resets this, so any rule using it must re-declare
   user-select: none directly after. */
button,
[role="button"],
[role="radio"],
[role="tab"],
.chip,
.nav a,
.brand,
.btn,
.stat dt,
.legend,
.mod-chip,
.memo-link,
.skip,
.site-footer a {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

:where(button, a, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

.mono {
  font-family: var(--label);
}

.wrap {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
}

/* ---------- header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  border-bottom: var(--hairline) solid var(--border);
  /* Opaque chrome avoids a full-width backdrop-blur repaint while scrolling
     and compresses cleanly during screen sharing. */
  background: var(--background);
}

/* App widgets portal their scrims to <body>, outside the course article. The
   shared dialog/sheet primitives add a blur utility there; keep Verification's
   scrims flat too so opening a source card cannot reintroduce a viewport-wide
   backdrop pass. This stylesheet is disabled when client navigation leaves
   the track, so the platform primitives keep their normal treatment elsewhere. */
[data-slot="dialog-overlay"],
[data-slot="sheet-overlay"] {
  backdrop-filter: none;
}
/* One column grid for the chrome AND the landing page, declared once.
   The bar was 1436px with 80px gutters (BlueDot's desktop geometry) while the
   page content sat in a separate 1168px wrap with 24px gutters — so the brand
   and the h1 under it started on different verticals at every width, and on a
   wide monitor the content huddled left of the chrome. Both now read these
   two values, so they cannot disagree again. landing.css consumes them for
   the hero/band wraps; interior course pages keep the narrower reading wrap
   on purpose — they are documents, not compositions. */
:root {
  /* 1600 with a fluid gutter, not BlueDot's 1436 with a fixed 80: that pair
     stacked a centering margin on top of the gutter as soon as the window
     passed the cap, and a 1500-1600px monitor — the common case — got a
     120-160px void right of the avatar. Up to 1600 the gutter is the whole
     inset; only genuinely wide screens center. */
  --chrome-maxw: 1600px;
  --chrome-gutter: clamp(24px, 4vw, 64px);
}

.site-header .bar {
  /* Match BlueDot's desktop header geometry exactly: its 1505px viewport
     resolves a 1436px outer container with 80px internal gutters, putting
     content at x=114.5…1390.5 inside a 76px bar. The flexible first column
     keeps the nav/action composition on the right instead of gluing the first
     destination to the logotype. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  column-gap: 48px;
  height: 76px;
  max-width: var(--chrome-maxw);
  margin: 0 auto;
  padding: 0 var(--chrome-gutter);
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--display);
  font-size: var(--fs-lg);
  /* 700 is the top of the embedded Space Grotesk's weight range — asking
     for more gets synthetic bold, which smears the letterforms. */
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.015em;
  text-decoration: none;
  white-space: nowrap;
}
/* The @ is a connector, not a brand mark — the logotype beside it carries
   the brand. It used to take --brand-ink, which on the night ground is a
   lightened dusty pink (maroon reads at 2.3:1 as text there), so the one
   pink thing on the page was a preposition. */
.brand i {
  color: var(--muted-foreground);
  font-style: normal;
  line-height: 1;
}

/* The XLab logotype beside the word.

   Trap: the file is 3300x1050 but the ink only fills the middle 55% of that
   box (y 236-814) and is inset horizontally too, so `height` buys about half
   of what it says. At 24px the visible ink is roughly 13px high: the same cap
   height as the 18px word beside it. Re-crop the PNG if you want the number
   itself to describe the ink rather than its transparent canvas.

   Two files, not one filtered file. The day artwork is maroon-and-black and
   disappears on the night ground; the white artwork keeps the maroon cross
   and turns the letters white. Both ship, and CSS shows one — a filter that
   re-lit the day file pushed the cross off-brand towards pink. */
.brand-mark {
  height: 24px;
  width: auto;
  display: block;
}

/* Which artwork, per ground. These must stay AFTER .brand-mark: it sets
   display:block at the same specificity, so ordering is what decides. */
.mark-night {
  display: none;
}
:root[data-theme="dark"] .mark-day {
  display: none;
}
:root[data-theme="dark"] .mark-night {
  display: block;
}

/* At full desktop width the nav is its content width, between BlueDot's two
   48px composition gaps. At narrower widths the media rule below turns its
   grid track flexible, and min-width:0 lets the row scroll instead of forcing
   the header wider than the viewport. */
/* No scrollbars anywhere — the author's call, at the root so it holds for
   the page and for every scrolling box inside it. Scrolling still works;
   only the painted bar is gone. Lives here as well as in the app's
   globals.css because this stylesheet is what a lifted copy of the course
   would carry.

   Trap: neither declaration is a fallback for the other — Firefox reads
   scrollbar-width, WebKit and Chromium only the pseudo-element, and that
   one must be display:none rather than width:0. */
:root,
* {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
*::-webkit-scrollbar {
  display: none;
}

.nav {
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 14px;
  font-size: var(--fs-md);
  overflow-x: auto;
}
.nav a {
  white-space: nowrap;
}

/* Scrollbars are hidden site-wide (the rule above this block), so a nav wider
   than its box offers the reader nothing at all. On a 390px phone this row is
   478px of links in a 358px box, which means "Team" and "About" are not just
   off screen, they are indistinguishable from links that do not exist. The
   fade is the only signal left once the scrollbar is gone.

   It lifts on focus-within: a keyboard or screen-reader walk must never dim
   the link it has just landed on. */
@media (max-width: 720px), (pointer: coarse) {
  .nav {
    -webkit-mask-image: linear-gradient(
      to right,
      #000 calc(100% - 28px),
      transparent
    );
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
  .nav:focus-within {
    -webkit-mask-image: none;
    mask-image: none;
  }
}
.nav a {
  position: relative;
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  color: var(--muted-foreground);
  text-decoration: none;
  transition: color 0.15s ease, background 0.15s ease;
}
.nav a:hover {
  color: var(--foreground);
  background: var(--muted);
}
.nav a[aria-current="page"] {
  color: var(--foreground);
}
.nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-inline: 14px;
  bottom: -1px;
  height: 2px;
  border-radius: var(--radius-xs);
  background: var(--primary);
}

/* The switch, notebook and account control form one header action group. The
   two labelled controls share a width floor; all three share a height token.

   The width is set by the longer of the two labels the button can carry —
   "Account" once a session is detected, not "Sign in" — because a width fitted
   to the shorter one clips the other the moment sync.js swaps it. The height
   is a variable because the large text size lifts every tap target to 44px;
   if only the button read that rule the pair would come apart at that size
   alone. */
.header-right {
  --hdr-action-w: 100px;
  --hdr-action-h: 44px;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* At 200% type the label and the two 44px segments both outgrow the default
   size, so the pair is given a bigger one rather than a fixed one it would
   spill out of. */
:root.reader-large .header-right {
  --hdr-action-w: 168px;
  --hdr-action-h: 54px;
}

/* A floor, not a fixed size: at the default size neither one's content reaches
   it, so both settle on exactly the same number — which is the point. Setting
   `width` instead would clip the label the moment the type grew. The theme
   switch shares only the height: with two segments its content is narrower
   than the floor, and a width floor would leave dead ground inside its border. */
.header-right > .btn.small,
.header-right > #verification-notebook-launcher {
  min-width: var(--hdr-action-w);
  min-height: var(--hdr-action-h);
}
.header-right .theme-switch {
  min-height: var(--hdr-action-h);
}

/* Two-way theme switch, drawn as radios rather than a cycling button: a
   cycling control gives no way to see which is active. */
.theme-switch {
  display: inline-flex;
  align-items: center;
  /* Content-sized: two squares, one 2px gap, the padding, the border. */
  gap: 2px;
  padding: 2px;
  border: var(--hairline) solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card);
}
/* Each segment is a square, the height of the switch's inside — so the
   selected one reads as a square swatch of the theme it names.

   Trap: `height: 100%` does not work here. The switch is sized by its
   content, so a percentage height resolves against nothing and the button
   collapses onto its icon — 30 x 13.5, which the corner radius then rounds
   into a squat pill. The side is stated instead, off the same token that
   sets the header's action height, so the two segments stay square when the
   large text size makes everything bigger — and minus the padding and the two
   hairlines, so the control still stands exactly as tall as the button beside
   it. */
.theme-switch button {
  --seg: calc(var(--hdr-action-h, 44px) - 4px - 2 * var(--hairline));
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--seg);
  height: var(--seg);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.theme-switch button:hover {
  color: var(--foreground);
  background: var(--accent);
}
.theme-switch button[aria-checked="true"] {
  background: var(--primary);
  color: var(--primary-foreground);
}
.theme-switch .theme-trigger {
  display: none;
}
.theme-switch .theme-trigger,
.theme-switch .theme-trigger:hover {
  background: var(--primary);
  color: var(--primary-foreground);
}

.theme-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 70;
  display: flex;
  flex-direction: column;
  min-width: 208px;
  padding: 4px;
  border: var(--hairline) solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: 0 12px 28px -12px rgb(0 0 0 / 0.35);
}
.theme-menu[hidden] {
  display: none;
}
.theme-switch .theme-menu button {
  width: 100%;
  height: auto;
  min-height: var(--hdr-action-h);
  justify-content: flex-start;
  gap: 10px;
  padding: 8px 10px;
  color: var(--foreground);
  font: inherit;
  font-size: var(--fs-md);
  text-align: left;
}
.theme-switch .theme-menu button:hover {
  background: var(--accent);
  color: var(--foreground);
}
.theme-switch .theme-menu button[aria-checked="true"] {
  background: var(--primary);
  color: var(--primary-foreground);
}
.theme-menu button svg {
  flex: none;
}
.theme-switch svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Equal boxes draw unequal icons: the sun's rays reach 20 of the 24 viewBox
   units where the moon reaches 18. Sizing the sun down
   matches the ink the eye compares, not the box the CSS sets — and the sun is
   the one that has to survive the filled ground of the selected state. */
.theme-switch button[data-theme="light"] svg {
  width: 13.5px;
  height: 13.5px;
}
/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 50px;
  padding: 0 24px;
  border: var(--hairline) solid transparent;
  border-radius: var(--radius-sm);
  background: var(--primary);
  color: var(--primary-foreground);
  font: inherit;
  font-size: var(--fs-xl);
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.btn:hover {
  background: color-mix(in srgb, var(--primary) 88%, var(--foreground));
}
.btn.outline {
  background: transparent;
  color: var(--foreground);
  border-color: var(--border);
}
.btn.outline:hover {
  background: var(--muted);
}
.btn.small {
  height: 44px;
  min-height: 44px;
  padding: 0 14px;
  font-size: var(--fs-lg);
}

.skip {
  position: fixed;
  top: 10px;
  left: 10px;
  width: max-content;
  z-index: 60;
  padding: 8px 14px;
  border: var(--hairline) solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--background);
  transform: translateY(-200%);
}
.skip:focus {
  transform: none;
}

/* ---------- footer ---------- */

.site-footer {
  border-top: var(--hairline) solid var(--border);
  margin-top: 72px;
  padding: 22px 0 40px;
  color: var(--muted-foreground);
  font-family: var(--label);
  font-size: var(--fs-xs);
}
.site-footer .wrap {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.foot-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
}
/* A footer row is navigation, not a sentence, so it does not get the inline
   exception WCAG's target-size rule grants links inside prose — at 19px tall
   these were under the 24px floor on a phone. The padding takes them past it;
   the negative margin keeps the row's spacing where it was. */
.foot-links > * {
  color: var(--muted-foreground);
  text-decoration: none;
  display: inline-block;
  padding-block: 4px;
  margin-block: -4px;
}
.foot-links a:hover {
  color: var(--foreground);
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* A destination that does not exist yet. It must not look pressable — no
   pointer, no hover, no underline — or it reads as a broken link. */
.foot-links .pending {
  color: var(--muted-foreground);
  opacity: 0.5;
  cursor: default;
}

/* One rule closes the page; a second one inside the footer only boxes the
   copyright off from the links it belongs with. Spacing separates them. */
.foot-end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 20px;
  padding-top: 4px;
}
.foot-end > span:last-of-type {
  margin-right: auto;
}

/* Between phone and full desktop widths, keep one compact row and surrender
   only the flexible brand-to-nav spacer. The link row can then scroll inside
   its own track without disturbing the actions at the right edge. */
@media (min-width: 721px) and (max-width: 1435px) {
  .site-header .bar {
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 24px;
  }
}

/* The nav wraps to its own scrolling row rather than collapsing into a menu:
   hiding the destinations behind a button on the one viewport where the
   sidebar is already gone leaves no way through.
   The desktop grid is restored to flex here so the third row can span the
   whole bar without a second layout wrapper. */
@media (max-width: 720px) {
  :root {
    --chrome-gutter: 16px;
  }
  .site-header .bar {
    display: flex;
    height: auto;
    flex-wrap: wrap;
    gap: 10px 12px;
    padding: 8px 16px;
  }
  .nav {
    order: 3;
    flex: none;
    width: 100%;
    overflow-x: auto;
    padding-bottom: 2px;
  }
  .brand,
  .nav a {
    min-height: 44px;
  }
  .nav a {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
    padding: 8px;
  }
  .header-right {
    /* The 44px tap floor, not above it: 50px here made the phone header the
       largest rendition of the chrome anywhere ("sizes", translated). The large
       text size keeps its own 54px — that floor is the type size's, not the
       viewport's. */
    --hdr-action-h: 44px;
    gap: 8px;
    order: 2;
    /* No width: it shares the first row with the brand, pinned right by the
       auto margin. Forcing a full-width row of its own left the brand alone
       on line one and the controls floating under it. On a screen too narrow
       for both, flex-wrap still breaks it to its own right-aligned row. */
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-left: auto;
  }
  .theme-switch .theme-trigger {
    display: inline-flex;
    --seg: var(--hdr-action-h);
  }
  .theme-switch button[role="radio"] {
    display: none;
  }
  .header-right .theme-switch {
    position: relative;
    padding: 0;
    border: 0;
    background: transparent;
  }
  .header-right .theme-switch,
  .header-right > .btn.small,
  .header-right > #verification-notebook-launcher {
    min-width: var(--hdr-action-h);
  }
  .header-right > .btn.small {
    padding: 0 12px;
    font-size: var(--fs-md);
  }
  .brand-word {
    display: none;
  }
  .brand-mark {
    height: 22px;
  }

  .foot-links > * {
    display: inline-flex;
    min-height: 44px;
    align-items: center;
    padding-block: 8px;
    margin-block: -8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .site-header {
    background: var(--background);
  }
}
