/* ===========================================================================
   Cortex University — CLASSIC REGISTER, full-portal skin ("antiquarian").
   Loaded on every page from base.html, LAST, and every rule in it is scoped to
   `body.reg-classic`. On the default (modern) register not one selector here
   matches, so the modern portal renders byte-identical with or without this
   file. Switch registers with the nav's style toggle (lynx_ui cookie) — that
   toggle IS the kill switch this change ships behind (CLAUDE.md §0.7).

   WHY A SKIN AND NOT A SECOND SET OF PAGES. The portal already has a UI-register
   axis (i18n.resolve_ui / SUPPORTED_UIS / ui_asset / body.reg-<ui>), orthogonal
   to light-dark, and portal.css is written almost entirely against the shared
   --bg/--fg/--border/--accent tokens. So the whole institution can change
   register by repointing about a dozen custom properties on <body> — every
   page, every menu, every table follows, and there is exactly one copy of the
   content and the markup. Duplicating 200-odd templates under /old would have
   produced the same picture and a second portal to maintain.

   THE DESIGN. A university prospectus printed in 1890: laid cream paper, Oxford
   blue ink, an oxblood second ink, gold leaf on the arms only, Cinzel (Roman
   inscriptional capitals) for anything engraved and EB Garamond for anything
   read. Square corners, hairline rules, no gradient used as decoration, no
   glow, and no shadow a sheet of paper could not cast.

   ART. Nothing new is drawn. Heraldry resolves through ui_asset(), which
   prefers /static/ui/classic/* and falls back to a sibling register only when
   the classic plate does not exist — so the classic heralds, badges, seals and
   frames already shipped are what the register wears, and no modern mark ever
   appears on it.

   ORDER OF BUSINESS BELOW
     §1  tokens          — the dozen properties that re-skin most of the portal
     §2  the sheet       — paper ground, grain, type
     §3  chrome          — top nav, brandline, footer, boot splash
     §4  primitives      — headings, links, buttons, inputs, badges, tables
     §5  containers      — cards → ruled entries, panels, heroes
     §6  the doors       — the login/vault HUD, redrawn as a sealed letter
     §7  set pieces      — landing, catalogue, gamification, the manifesto
   =========================================================================== */

/* ═══ §0 · The reading face ═══════════════════════════════════════════════
   Cinzel (the engraving face) is already declared globally in
   tokens-classic.css. EB Garamond is declared here because it is new with this
   skin and nothing outside the classic register uses it — a browser on the
   modern register parses these four rules and downloads none of the files,
   since no element ever asks for the family. Self-hosted OFL, latin subset;
   see /static/fonts/README.txt and /static/fonts/LICENSES/ebgaramond-OFL.txt. */
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/classic/ebgaramond-400.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/classic/ebgaramond-400i.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/classic/ebgaramond-500.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/classic/ebgaramond-600.woff2") format("woff2");
}

/* ═══ §1 · Tokens ═════════════════════════════════════════════════════════
   Custom properties inherit, so redefining them ON <body> beats the
   :root[data-theme] block in base.html for the entire document — including
   light mode, which is why the classic register looks the same in both themes.
   That is deliberate: "dark mode" is not a thing a printed prospectus has. */
body.reg-classic {
  /* Inks */
  --o-navy:       #10243F;
  --o-navy-deep:  #0A1729;
  --o-navy-soft:  #37506F;
  --o-oxblood:    #6E1622;
  --o-oxblood-lt: #8C2531;
  --o-gold-ink:   #A9873C;
  /* THE GOLD AT TEXT WEIGHT. --o-gold-ink is the register's gold for STROKES
     and RULES — hairlines, keylines, the boot rule, the crest outline — where
     contrast is decorative and 3:1 never applies. Measured as an INK it is
     3.05:1 on --o-paper and 3.21:1 on --o-paper-hi: under AA for every one of
     the kickers, eyebrows and crest labels that were set in it (all of them
     small, none of them "large text" at any dial setting). This is the same
     gold taken down to 31% lightness — 4.84:1 on --o-paper, 5.10:1 on
     --o-paper-hi — which is still recognisably antique gold and not a brown.
     Use it wherever gold is being READ; keep --o-gold-ink for what is drawn. */
  --o-gold-text:  #856618;
  --o-gold-pale:  #CBAE72;
  --o-paper:      #F7F3E8;
  --o-paper-hi:   #FCF9F1;
  --o-rule:       #BCAE8B;
  --o-rule-faint: #D8CDB2;

  /* …repointed onto the tokens portal.css actually consumes. */
  color-scheme: light;
  --bg:            var(--o-paper);
  --bg-elev:       var(--o-paper-hi);
  --fg:            #1E1A14;
  --muted:         var(--o-navy-soft);
  --border:        var(--o-rule);
  --accent:        #7A5E18;   /* deep gold-bronze: raw gold fails AA on cream */
  --accent-strong: var(--o-oxblood);
  --danger:        var(--o-oxblood);
  --ok:            #1F4D2E;
  --warn:          #8A5A12;
  /* base.html defines --gold-text per THEME (#e0b341 dark / #856618 light).
     This register is cream in both themes (see §"The Theme toggle is not
     offered in this register"), so on the live site — served
     `data-theme="dark" data-ui="classic"` — the surface came from the lines
     above and the gold did NOT: #e0b341 landed on --o-paper-hi at 1.87:1.
     pa11y flagged it on `.about-pillar dt`; the same miss reached all
     thirteen `var(--gold-text)` rules in portal.css. The token belongs in
     this repointed list like every other one portal.css consumes. */
  --gold-text:     var(--o-gold-text);

  /* Nothing on a printed page has a radius. */
  --radius: 0px;
  --radius-lg: 0px;
  --radius-pill: 0px;

  /* ── The spacing scale, re-cut in --reg-unit (see §1a) ───────────────────
     portal.css declares its whole rhythm in PX on :root — --space-*, --nav-h
     — so raising the type alone grows the words and leaves every gutter,
     pad and gap at its modern size: the page comes out cramped, text
     crowding rules it used to clear. Browser zoom does not have this problem
     because it scales the pixel itself. So the register restates the rhythm
     in --reg-unit — "one pixel, as this register measures it" — and the page
     enlarges as one object. Each number below is portal.css's own px value
     with the unit taken off it, so the two files stay legibly in step.
     `:root` is (0,1,0); `body.reg-classic` is (0,1,1), so these win
     without !important and lose to nothing except an author override here. */
  --space-xs: calc(4  * var(--reg-unit, 1px));
  --space-sm: calc(8  * var(--reg-unit, 1px));
  --space:    calc(14 * var(--reg-unit, 1px));
  --space-md: calc(20 * var(--reg-unit, 1px));
  --space-lg: calc(28 * var(--reg-unit, 1px));
  --space-xl: calc(44 * var(--reg-unit, 1px));
  --nav-h:    calc(49 * var(--reg-unit, 1px));

  /* Paper casts a short, warm shadow — not a black one. */
  --shadow-sm: 0 1px 0 rgba(90, 72, 34, 0.10);
  --shadow-md: 0 2px 8px rgba(90, 72, 34, 0.14);
  --shadow-lg: 0 6px 20px rgba(90, 72, 34, 0.16);
  --ring: 0 0 0 2px var(--o-oxblood);

  --font-body: "EB Garamond", "Iowan Old Style", "Palatino Linotype", Palatino,
    "Book Antiqua", Georgia, "Times New Roman", serif;
  --font-display: "Cinzel", "Trajan Pro", Optima, "Palatino Linotype", Georgia, serif;
  --transition: 130ms linear;
}

/* The one thing that cannot be fixed on <body>. portal.css paints `html, body
   { background: var(--bg) }`, and <html> sits OUTSIDE the element the tokens
   above are declared on, so it keeps the dark theme's --bg — which shows
   through wherever the body box is shorter than the viewport (short pages,
   overscroll, the gap under a footer). Rather than raise the token block's
   specificity over base.html's inline `:root[data-theme]` style — which loses
   on source order, being in <head> after this file — <html> is given the paper
   colour directly. `[data-ui="classic"]` is the register attribute base.html
   stamps on <html> and portal.js keeps in sync, so this tracks the toggle. */
html[data-ui="classic"] {
  background: #F7F3E8;
  color-scheme: light;
}

/* ═══ §1a · THE DIAL ══════════════════════════════════════════════════════
   ┌──────────────────────────────────────────────────────────────────────┐
   │  --reg-dial is the ONE number that sets how large the classic         │
   │  register is set. 1 = exactly today's modern sizing. 1.32 = the       │
   │  shipped value, which puts the reading face at 20.5px and is what     │
   │  "reads like a book at 100% zoom" came out to when looked at.         │
   │  Nudge it here and nowhere else: type, gutters, padding, nav height   │
   │  and every rem in every sheet move together, exactly as browser zoom  │
   │  moves them.                                                          │
   │  Sensible range 1.15 (restrained) … 1.45 (large-print prospectus).    │
   │  Do not raise it past 1.32 without re-checking the three-up card      │
   │  columns: 1.44 was measured strangling them.                          │
   │  --reg-unit is the dial as *applied* — ONE PIXEL, as this register    │
   │  measures it — and is what the rest of the sheet reads.               │
   └──────────────────────────────────────────────────────────────────────┘

   WHY IT LIVES ON <html> AND NOT ON <body>. `rem` resolves against the ROOT
   font size, and portal.css sets it with `html, body { font-size: 15.5px }`.
   The register's old `body.reg-classic { font-size: 17px }` therefore moved
   only the text that inherits — every `rem` in portal.css, gamification.css
   and in this very sheet (`.auth-label { font-size: 0.64rem }`, the button
   plates, the kickers) kept measuring against 15.5px and stayed modern-sized.
   Setting the root instead makes one dial move all of it. `html, body` is
   (0,0,1) and `html[data-ui="classic"]` is (0,1,1), so this wins on
   specificity, not on source order.

   The dial deliberately does NOT scale the *viewport* breakpoints, because
   those are properties of the device, not of the register — a phone must
   still get the phone layout. That is the one way this differs from zoom,
   and it is the correct difference.

   WHY IT RAMPS, AND WHY IT USED TO JUMP. A phone has no room to spend on a
   prospectus's generosity, so the dial is eased back on a narrow window. That
   easing was first written as a media query — full dial above 640px, ×0.88
   at or below — which is a step function: dragging a window across 641px
   resized the whole page by 13.6% in one frame. /old's sheet had already
   argued the other way and ramped with clamp(), on the grounds that a page
   should never jump as it is resized, and it is right; this is the same shape
   applied to the live pages. None of the easing below 400px of window (a
   phone is a phone), all of the dial by 1280px (the width the "reads like a
   book" judgement was made at), a straight line in between, and the two
   endpoints are exactly the two values the step function used, so nothing
   got bigger or smaller at either end — only the middle stopped snapping.

   WHY A LENGTH AND NOT A NUMBER. The ramp has to be driven by 100vw, and CSS
   calc() cannot divide a length by a length, so there is no way to arrive at
   a plain unitless factor. --reg-unit is therefore the ramp expressed as the
   thing it is used for: one scaled pixel. `N * var(--reg-unit)` is a length
   (number × length), which is what every consumer wanted anyway. The `/ 880`
   is the ramp's run in px — 1280 − 400 — carried as a bare number so the
   division stays length ÷ number, which calc() does allow. */
html[data-ui="classic"] {
  --reg-dial: 1.32;                       /* ←── NUDGE THIS AND NOTHING ELSE */
  --reg-phone: 0.88;      /* the fraction of the dial a ≤400px window is set at */
  --reg-unit: clamp(
    calc(var(--reg-dial) * var(--reg-phone) * 1px),
    calc(var(--reg-dial) * var(--reg-phone) * 1px
         + var(--reg-dial) * (1 - var(--reg-phone)) * (100vw - 400px) / 880),
    calc(var(--reg-dial) * 1px)
  );
  font-size: calc(15.5 * var(--reg-unit, 1px));
}

/* ═══ §2 · The sheet ══════════════════════════════════════════════════════ */

/* portal.css paints two accent-coloured radial washes on <body>. On paper they
   read as a stain. Replace with the printer's vignette + a faint parchment
   grain, big and pale enough that the tile's repeat is not readable. */
body.reg-classic {
  background:
    radial-gradient(120% 85% at 50% -15%, rgba(255, 254, 249, 0.85), transparent 66%),
    radial-gradient(100% 100% at 50% 125%, rgba(120, 100, 60, 0.05), transparent 60%),
    var(--o-paper);
  background-attachment: fixed;
  /* EB Garamond has a small x-height, so even at parity with a sans it reads
     undersized. The size now comes from the root (§1a) — this only has to
     stop portal.css's `html, body { font-size: 15.5px }` from pinning <body>
     back to the absolute modern value, which `1rem` does by definition. */
  font-size: 1rem;
  line-height: 1.62;
  font-kerning: normal;
  font-variant-ligatures: common-ligatures;
  font-variant-numeric: oldstyle-nums proportional-nums;
}
body.reg-classic::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: url("/static/ui/classic/patterns/parchment.png") repeat;
  /* The tile was re-cut seamless and SQUARE on 2026-08-10 (256×256, was
     108×160) and is lighter and less amber than the plate this opacity was
     first tuned against, so the grain now reads subtler at the same 0.13. If
     the paper ever looks flat, this is the number — not the image. */
  background-size: 460px auto;
  opacity: 0.13;
  mix-blend-mode: multiply;
}

/* Tabular data and money want lining figures — old-style numerals in a column
   do not line up, which is the one place they are wrong. */
body.reg-classic table,
body.reg-classic .cat-price,
body.reg-classic .ust-tile-value,
body.reg-classic .marks-score,
body.reg-classic code,
body.reg-classic pre,
body.reg-classic .mono {
  font-variant-numeric: lining-nums tabular-nums;
}

/* ═══ §3 · Chrome ═════════════════════════════════════════════════════════ */

/* Top nav: a ruled running head, not a floating bar. */
body.reg-classic .topnav {
  background: transparent;
  backdrop-filter: none;
  border-bottom: 1px solid var(--o-navy);
  box-shadow: none;
}
body.reg-classic .topnav-brandline {
  border-bottom: 1px solid var(--o-rule-faint);
}
body.reg-classic .brand-text,
body.reg-classic .nl-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--o-navy);
}
body.reg-classic .brand-text-accent,
body.reg-classic .nl-wordmark-accent { color: var(--o-oxblood); }

/* `/newsletter` at 390 laid out a 423px document — classic only. Three things
   compound: `.nl-masthead-text` is a flex/grid item with the default
   `min-width: auto`, so it cannot shrink below its own min-content; the wordmark
   is two adjacent spans with NO whitespace between them, so "CortexUniversity"
   is a single unbreakable word and therefore that min-content; and the rule above
   sets it uppercase at 0.14em, which is what pushes the one word past the
   viewport. Modern renders the same markup at 238px and never overflows, so the
   whole repair is scoped here and modern is left untouched.
   `min-width: 0` ALONE does not fix it — measured: the item shrinks 307→224px
   and the wordmark still overflows it at 307px. A break opportunity is required.
   It is a U+200B ZERO WIDTH SPACE at the seam between the two spans, i.e. at the
   real word boundary, so the masthead stacks CORTEX / UNIVERSITY the way a
   printed one does. NOT `overflow-wrap: anywhere` on `.nl-wordmark`, which was
   the obvious reach and is the banned kind of fix: measured, it chops the
   wordmark mid-word — two fragments at 390 and three at 320 — because with one
   unbreakable word it can only break wherever the line happens to run out. */
body.reg-classic .nl-masthead-text { min-width: 0; }
body.reg-classic .nl-wordmark-accent::before { content: "\200B"; }
/* …and the seam itself. portal.css now gives the accent the header's 2px, but
   2px is a gap this register cannot see: it tracks the masthead at 0.14em,
   which computes to 3.72px BETWEEN EVERY LETTER, so a 2px word space is
   narrower than the letter spacing and the masthead read "CORTEXUNIVERSITY".
   The gap has to beat the tracking, so it is stated in em and tracks the dial. */
body.reg-classic .nl-wordmark-accent { margin-left: 0.34em; }

/* Menu items in small caps, underlined on hover — a table of contents. */
body.reg-classic .nav-link,
body.reg-classic .nav-dd-toggle,
body.reg-classic .nav-control {
  font-variant-caps: small-caps;
  letter-spacing: 0.09em;
  font-weight: 500;
  border-radius: 0;
  background: transparent;
  border-color: transparent;
  color: var(--o-navy);
}
body.reg-classic .nav-link:hover,
body.reg-classic .nav-dd-toggle:hover,
body.reg-classic .nav-control:hover {
  color: var(--o-oxblood);
  background: transparent;
  border-bottom: 1px solid var(--o-oxblood);
}
body.reg-classic .nav-link[aria-current="page"],
body.reg-classic .nav-link.is-active {
  color: var(--o-oxblood);
  border-bottom: 1px solid var(--o-gold-ink);
  background: transparent;
}
body.reg-classic .nav-dd-menu {
  background: var(--o-paper-hi);
  border: 1px solid var(--o-navy);
  border-radius: 0;
  box-shadow: var(--shadow-md);
}

/* ── The Theme toggle is not offered in this register ─────────────────────
   §1 repoints --bg/--bg-elev/--fg onto cream ON BODY, which beats base.html's
   `:root[data-theme]` block for the whole document — so the classic register
   is the same paper in `data-theme="dark"` as in `"light"`. That is the
   register's premise, stated at §1 ("dark mode is not a thing a printed
   prospectus has"), and other rules now DEPEND on it: portal.css gates the
   ivory cut of the heraldic marks on "dark theme AND a register that is not
   the always-cream one", precisely because the theme alone is the wrong test.

   The bug the 2026-08-13 QA pass found is therefore not the palette. It is
   that #theme-toggle was still offered here: clicking it flipped `data-theme`
   between dark and light and changed nothing a visitor could see — measured
   identical in both states (html #F7F3E8, card #FCF9F1, body ink #1E1A14). A
   control that does nothing is worse than an absent one, so the register that
   has no themes does not advertise a theme switch. `display: none` (not
   `disabled`, not `visibility`) because a greyed-out switch still asks to be
   understood, and because it takes the button out of the accessibility tree
   and the tab order along with the layout — the Classic/Modern toggle beside
   it stays, and is one tap from a register that DOES have both themes.

   Nothing is lost: the `lynx_theme` cookie is untouched here, so a visitor who
   set light or dark in the modern register still finds it there. The rule is
   keyed on the body class portal.js swaps client-side, so the control
   reappears the moment the register is toggled back, with no reload. */
body.reg-classic #theme-toggle { display: none; }

/* Footer: the colophon. */
body.reg-classic .site-footer {
  border-top: 1px solid var(--o-navy);
  background: rgba(16, 36, 63, 0.03);
}
body.reg-classic .site-footer-brand > strong {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}
body.reg-classic .site-footer-motto { font-style: italic; color: var(--o-oxblood); }
body.reg-classic .site-footer-links { font-variant-caps: small-caps; letter-spacing: 0.08em; }

/* Boot splash: a title page, not a HUD. The SVG crest strokes and the wordmark
   are re-inked; the art backdrop is dimmed to a paper wash. */
body.reg-classic .boot-splash { background: var(--o-paper); }
body.reg-classic .boot-splash-art { opacity: 0.10; filter: sepia(0.65) saturate(0.5); }
body.reg-classic .boot-splash-scrim {
  background: linear-gradient(180deg, rgba(247, 243, 232, 0.7), var(--o-paper));
}
body.reg-classic .boot-crest-ring,
body.reg-classic .boot-crest-shield,
body.reg-classic .boot-crest-glyph {
  stroke: var(--o-gold-ink);
  fill: none;
}
body.reg-classic .boot-wordmark {
  color: var(--o-navy);
  text-transform: uppercase;
  letter-spacing: 0.18em;
}
body.reg-classic .boot-wordmark-accent { color: var(--o-oxblood); }
/* The same repair as `.nl-wordmark-accent` in §2, for the same reason, on the
   FIRST thing a mobile visitor sees. The wordmark is two adjacent spans with no
   whitespace between them, so "CortexUniversity" is one unbreakable word; the
   rule above sets it uppercase at 0.18em on top of the register's dial, and at
   390px that one word measured 428px inside a 342px column. It could not wrap,
   and `.boot-splash-inner` was a grid item sized to max-content, so instead of
   shrinking it overflowed to a 476px box — off centre, the wordmark clipped to
   "CORTEX UNIVERS" and the motto to "VERITAS · INGENIUM · PROGRE". portal.css
   now caps the box at the viewport; this is the break opportunity that lets it
   stack CORTEX / UNIVERSITY, at full size, the way a title page would set it.
   A U+200B ZERO WIDTH SPACE at the real word boundary — never
   `overflow-wrap: anywhere`, which breaks wherever the line runs out. */
body.reg-classic .boot-wordmark-accent::before { content: "\200B"; }
body.reg-classic .boot-rule {
  background: var(--o-gold-ink);
  box-shadow: none;
}
body.reg-classic .boot-motto { color: var(--o-oxblood); font-style: italic; }

/* ═══ §4 · Primitives ═════════════════════════════════════════════════════ */

body.reg-classic h1,
body.reg-classic h2,
body.reg-classic h3,
body.reg-classic .cat-title,
body.reg-classic .guide-title,
body.reg-classic .ov-title,
body.reg-classic .page-header h1 {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--o-navy);
  letter-spacing: 0.08em;
  line-height: 1.24;
}
body.reg-classic h1,
body.reg-classic .cat-title,
body.reg-classic .guide-title,
body.reg-classic .ov-title { text-transform: uppercase; letter-spacing: 0.13em; }
body.reg-classic h2 { text-transform: uppercase; letter-spacing: 0.1em; font-size: 1.2rem; }

body.reg-classic a {
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--o-rule);
}
body.reg-classic a:hover { color: var(--o-oxblood); text-decoration-color: var(--o-oxblood-lt); }

/* Buttons: struck plates. Overrides the P21 red→gold gradient in portal.css,
   which was a colour patch on the modern button shape; this is the shape.

   ┌── A PAGE BUTTON IS NOT A CHROME CONTROL. Read this before widening the
   │   selector back to a bare `button`. ─────────────────────────────────────
   │ A PAGE BUTTON is an action in the document: Submit, Apply, Save, Enrol,
   │   the dashboard's one call to action. It is read as a phrase, it is sized
   │   by its own text, and it is the thing this plate was designed for —
   │   Cinzel caps, 0.14em of tracking, 1.4em of padding a side.
   │ A CHROME CONTROL is an affordance in the shell around the document: the
   │   nav's theme/register/locale pills, the logout link-button, the chat
   │   dock's 26px icon buttons. Those are sized in FIXED PIXELS by the
   │   component that owns them, they are usually a glyph rather than a word,
   │   and they sit in rows that are already at their width budget.
   │ The rule used to say `body.reg-classic button` and hit both. With the
   │   dial at 1.32 that dressed every 26px icon as an engraved plate —
   │   0.76rem is 15.5px and 1.4em of padding is ~22px a side — which cost
   │   the chat dock its Close button (the head is a nowrap row: the actions
   │   grew 182px → 240px and the last button was clipped past the panel
   │   edge) and pushed the nav cluster's min-content to 430px, pinning
   │   scrollWidth at 446 and scrolling every signed-in page sideways on a
   │   phone. Both from one selector, so both are fixed in one place.
   │ THE EXEMPTION, and why it is a container test and not a list of classes.
   │   Chrome is not a property of a button, it is a property of WHERE the
   │   button lives — `.topnav` and `.cchat` are the two shells that persist
   │   across every page, and everything inside them is by definition an
   │   affordance of the shell. A container test also covers the controls
   │   those shells have not grown yet, which a class list would not.
   │   `button.link` is the third case and the only class-based one: it is a
   │   button that has explicitly asked to be typography rather than a
   │   control, so a plate is exactly what it is opting out of.
   │ :where() keeps the exemption at ZERO specificity, so this selector still
   │   weighs (0,1,2) — the same as the bare `button` it replaces. That
   │   matters: several rules below (.btn-link, .auth-submit, .cchat-send)
   │   are (0,2,1) and are meant to win over it. Raising this to (0,2,2)
   │   would silently invert all of them. The three :not()s are chained
   │   rather than written as one comma-separated list because the scope
   │   checker in design/ANTIQUARIAN_REGISTER.md splits selectors on commas;
   │   a list inside :not() would read to it as three unscoped selectors.
   └──────────────────────────────────────────────────────────────────────── */
body.reg-classic button:not(:where(.topnav *)):not(:where(.cchat *)):not(:where(.link)),
body.reg-classic .btn-primary,
body.reg-classic .btn-secondary,
body.reg-classic .btn-tertiary,
body.reg-classic .landing-btn,
body.reg-classic .cat-apply-btn,
body.reg-classic .auth-submit {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.76rem;
  border-radius: 0;
  border: 1px solid var(--o-navy);
  background: transparent;
  background-image: none;
  color: var(--o-navy);
  box-shadow: none;
  padding: 0.72em 1.4em;
}
body.reg-classic button.primary,
body.reg-classic .btn-primary,
body.reg-classic .landing-btn-primary {
  background: var(--o-navy);
  background-image: none;
  color: var(--o-paper-hi);
  border-color: var(--o-navy);
  box-shadow: inset 0 0 0 1px var(--o-paper-hi), inset 0 0 0 2px var(--o-navy);
}
/* Same exemption on the hover state, for the same reason: a chrome control
   that fills solid navy on hover is a plate again, just later. */
body.reg-classic button:not(:where(.topnav *)):not(:where(.cchat *)):not(:where(.link)):hover,
body.reg-classic .btn-secondary:hover,
body.reg-classic .btn-tertiary:hover {
  background: var(--o-navy);
  color: var(--o-paper-hi);
  border-color: var(--o-navy);
  filter: none;
}
body.reg-classic button.primary:hover,
body.reg-classic .btn-primary:hover,
body.reg-classic .landing-btn-primary:hover {
  background: var(--o-oxblood);
  border-color: var(--o-oxblood);
  filter: none;
  box-shadow: inset 0 0 0 1px var(--o-paper-hi), inset 0 0 0 2px var(--o-oxblood);
}
body.reg-classic .btn-link,
body.reg-classic .primary-link { font-family: var(--font-body); text-transform: none; letter-spacing: 0; }

/* Fields: ruled lines to be written on. */
body.reg-classic input[type="text"],
body.reg-classic input[type="email"],
body.reg-classic input[type="password"],
body.reg-classic input[type="search"],
body.reg-classic input[type="tel"],
body.reg-classic input[type="url"],
body.reg-classic input[type="number"],
body.reg-classic input[type="date"],
body.reg-classic select,
body.reg-classic textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--o-navy-soft);
  border-radius: 0;
  box-shadow: none;
}
/* The login/verify HUDs are a FIXED dark scheme, register-independent
   (see .auth-screen / .verify-screen). The classic field restyle above must
   NOT reach them: it repainted the typed text with the classic register's dark
   ink (--fg = #1E1A14) on the HUD's dark-navy input shell, so the username you
   typed was invisible (BL visual-fix #2). Their own light-on-dark rules in
   portal.css (color: var(--auth-fg)) are the right ones in every register, so
   they are deliberately excluded from the list above. */
body.reg-classic textarea { border: 1px solid var(--o-navy-soft); }
body.reg-classic input:focus,
body.reg-classic select:focus,
body.reg-classic textarea:focus {
  outline: none;
  border-bottom-color: var(--o-oxblood);
  box-shadow: 0 1px 0 0 var(--o-oxblood);
}
body.reg-classic label { font-variant-caps: small-caps; letter-spacing: 0.07em; }
/* …but ONLY on the label's own text. `font-variant-caps` INHERITS, and every
   form on this portal (register, set-password, reset) wraps its control in its
   <label>, so the small-caps was landing on what the visitor TYPED: "borja"
   rendered as ʙᴏʀᴊᴀ. It reads as SHOUTED UPPERCASE and had the owner doubting
   his own keystrokes (2026-08-11, verified in the browser). Engraved labels are
   the classic register's whole voice and stay; user input is data, not chrome,
   and is shown back exactly as entered.
   Set on the controls THEMSELVES rather than on `label > *`: a directly-matched
   declaration beats an inherited one outright, so this holds however deeply a
   future form nests its fields. */
body.reg-classic input,
body.reg-classic select,
body.reg-classic textarea,
body.reg-classic .auth-input,
body.reg-classic .verify-input { font-variant-caps: normal; }
body.reg-classic input[type="checkbox"],
body.reg-classic input[type="radio"] { accent-color: var(--o-navy); }

/* Badges / chips / pills: engraved labels, not lozenges. */
body.reg-classic .badge,
body.reg-classic .band-pill,
body.reg-classic .phase-chip,
body.reg-classic .nl-chip,
body.reg-classic .apps-chip,
body.reg-classic .ust-badge,
body.reg-classic .spdx-chip,
body.reg-classic .teaches-tag,
body.reg-classic .program-badge,
body.reg-classic .prereq-badge,
body.reg-classic .cat-bootcamp-tag,
body.reg-classic .verify-badge,
body.reg-classic .na-badge,
/* The go-deep status chip ("IN PROGRESS" / "STATUS NOT STATED"). It keeps its
   own colour — the amber warning is the point of it — and takes only the
   register's shape: square, small-caps display face, hairline rule. */
body.reg-classic .gd-chip,
body.reg-classic .notif-type {
  border-radius: 0;
  background: transparent;
  border: 1px solid currentColor;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.62rem;
  padding: 0.28em 0.6em;
  box-shadow: none;
}

/* Tables: ruled ledgers. Head in small caps under a heavy rule, hairlines
   between rows, no zebra fill — the ruling is the structure. */
body.reg-classic table,
body.reg-classic .ust-tile,
body.reg-classic .table-scroll { border-radius: 0; }
body.reg-classic th {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.66rem;
  color: var(--o-navy);
  background: transparent;
  border-bottom: 2px solid var(--o-navy);
}
body.reg-classic td { border-bottom: 1px solid var(--o-rule-faint); }
body.reg-classic tr:hover td { background: rgba(169, 135, 60, 0.07); }
body.reg-classic .ust-zebra tbody tr:nth-child(even) td,
body.reg-classic .ust-zebra-x { background: transparent; }

/* ═══ §5 · Containers ═════════════════════════════════════════════════════ */

/* Every card family becomes a leaf of better paper inside a printed double
   frame: a 1px navy border plus a 1px gold keyline, done with an inset shadow
   so no layout moves. This is the single rule that carries most of the portal. */
body.reg-classic .card,
body.reg-classic .cat-card,
body.reg-classic .ov-card,
body.reg-classic .lib-card,
body.reg-classic .nl-card,
body.reg-classic .na-card,
body.reg-classic .founder-card,
body.reg-classic .landing-card,
body.reg-classic .about-panel,
body.reg-classic .cat-bootcamp,
/* The two catalogue explainer panels (L0 "Recommended prior education",
   L1 "Boost your preparation"). They are card-shaped containers sitting
   directly above the card grids they explain, and they were never listed here:
   portal.css gives them `background: var(--bg-elev)` + `1px solid var(--border)`,
   which in this register lands on the right paper but a bare rule — no navy
   border, no gold keyline, no leaf shadow — so they read as unframed next to
   every card around them. Same paper, same frame, one selector list. */
body.reg-classic .cat-prior-ed,
body.reg-classic .cat-level1-boost,
/* The go-deep shelf's card (`templates/go_deep/_card.html`), rendered by both
   /go-deep and the /library section. Same story as the two panels above: its
   own sheet gives it `--bg-elev` + `1px solid var(--border)`, which lands on
   the right paper here but a bare rule, so it read as an unframed leaf beside
   every `.lib-card` next to it. Listed = framed. */
body.reg-classic .gd-card,
/* The remaining card-like classes carrying the `--bg-elev` + `1px solid
   --border` signature, swept in one pass rather than one-at-a-time: the landing
   page's operator/legal leaf (styled in `landing.html`'s own <style> block,
   which is why grepping this repo's .css files never surfaced it — it sat
   unframed beside four correctly framed `.landing-card`s), and the two
   university-status dashboard cards plus the admin analytics chart card, which
   are staff surfaces with exactly the same defect. `test_visual_qa_polish.py`
   now derives that signature from the sheets and asserts every card-like class
   it finds appears in this list, so the next one cannot be forgotten. */
body.reg-classic .landing-operator-card,
body.reg-classic .ust-chart-card,
body.reg-classic .ust-backlog-card,
body.reg-classic .an-chart-card,
body.reg-classic .note,
body.reg-classic .ust-tile,
body.reg-classic .phase-timeline-card,
body.reg-classic .problem-report-box,
body.reg-classic .catd-head,
body.reg-classic .catd-herald,
body.reg-classic .syl-pending,
body.reg-classic .empty-state {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  background: var(--o-paper-hi);
  background-image: none;
  box-shadow:
    inset 0 0 0 1px var(--o-paper-hi),
    inset 0 0 0 2px var(--o-gold-pale),
    var(--shadow-sm);
}
/* `.about-panel` carries no padding of its own (`portal.css` sets only
   `margin-top`), which is harmless in the modern register where the panel has no
   border. In this register it becomes a framed "printed leaf" above, so its
   edge-most children land their own 1px border directly on top of the frame's
   navy rule and gold keyline — three parallel lines down each side, which reads
   as a rendering fault. `.about-pillars` is a full-bleed grid with no side
   padding, so the inset has to come from the panel. `--space-lg` matches
   `.card`'s padding, so the framed panel reads like every other leaf. */
body.reg-classic .about-panel {
  padding: var(--space-lg);
}

/* `.about-mark-card` left the list above on purpose. The crest is photographic
   art with baked-in dark modelling and a gold rim-light, hard-cut to alpha —
   on parchment it reads as a pasted-in cut-out, not a printed engraving. The
   register keeps its frame (square corners, navy rule, gold keyline) but the
   plate stays dark: portal.css's radial ground is what the art was lit for. */
body.reg-classic .about-mark-card {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  box-shadow:
    inset 0 0 0 2px var(--o-gold-pale),
    var(--shadow-sm);
}

body.reg-classic .card:hover,
body.reg-classic .cat-card:hover,
body.reg-classic .landing-card:hover {
  transform: none;
  border-color: var(--o-oxblood);
}

/* The programme detail page (`/catalogue/{slug}/{tier}`, 60 public URLs) had no
   line in this file at all, so clicking a squared, navy-framed `.cat-card` landed
   the visitor on a 14px-rounded panel — out of the printed leaf and back into the
   modern register. `.catd-head` and `.catd-herald` join the frame list above; the
   fact rows are a ruled ledger instead, because seven double-keylined boxes
   stacked at a 0.55rem gap is a stack of cards, not a page of a prospectus, and
   §4 already rules exactly this shape (a `<dl>` of label/value pairs) with a
   heavy head rule and hairlines between the rows. */
body.reg-classic .catd-facts {
  gap: 0;
  margin-top: 1.4rem;
  border-top: 2px solid var(--o-navy);
}
body.reg-classic .catd-fact {
  border: 0;
  border-bottom: 1px solid var(--o-rule-faint);
  border-radius: 0;
  background: transparent;
  padding: 0.6rem 0.2rem;
}
body.reg-classic .catd-fact:last-child { border-bottom: 0; }
/* The herald plate is a framed plate INSIDE the framed head, so its own inner
   keyline would be a third line 3px from the second. One navy hairline. */
body.reg-classic .catd-herald {
  box-shadow: none;
  background: var(--o-paper);
}
body.reg-classic .catd-herald img { border-radius: 0; }

/* Doctoral rank, in this register's channels. portal.css tints
   `.catd-head.is-doctoral` and `.cat-card.is-doctoral` with
   `color-mix(… var(--m-violet, #7b2fff) 40%, var(--border))`, and `--m-violet` is
   declared only under `body.reg-modern` (`tokens-modern.css`) — so classic fell
   through to the neon literal and diluted it into a mauve rgb(162,123,185)
   hairline, the one saturated hue on the page. On the cards the container rule
   above already masked it; on the detail head nothing did.
   Squaring the frame would drop the distinction silently, so it moves onto the
   channels §9 defines for the same threshold on the path map: the tinted plate
   and a heavier rule. The rank is also stated in words directly above the title
   ("Level 3 · Ph.D", `.cat-card-level`), so colour is never the only channel.
   §9 is the source of truth for `--o-plate`; the literal here is only a safety
   net, because an undefined custom property is invalid at computed-value time
   and would take the whole `box-shadow` declaration with it — leaving the one
   doctoral card in the grid with no keyline at all rather than merely untinted. */
body.reg-classic .cat-card.is-doctoral,
body.reg-classic .catd-head.is-doctoral {
  border-width: 2px;
  border-color: var(--o-navy);
  background: var(--o-plate, #F0E7CF);
  box-shadow:
    inset 0 0 0 1px var(--o-plate, #F0E7CF),
    inset 0 0 0 2px var(--o-gold-pale),
    var(--shadow-sm);
}
/* Restated because the rule above and `.cat-card:hover` carry equal specificity
   and this one is later in the file — without it a doctoral card would be the
   one card in the grid with no hover feedback. */
body.reg-classic .cat-card.is-doctoral:hover { border-color: var(--o-oxblood); }

/* Flash + toasts: an official notice, ruled in the second ink. */
body.reg-classic .flash,
body.reg-classic .toast {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  border-left-width: 3px;
  background: var(--o-paper-hi);
  box-shadow: var(--shadow-sm);
  font-style: italic;
}
body.reg-classic .flash-error,
body.reg-classic .toast-error { border-left-color: var(--o-oxblood); }
body.reg-classic .flash-success,
body.reg-classic .toast-success { border-left-color: #1F4D2E; }
body.reg-classic .flash-info,
body.reg-classic .toast-info { border-left-color: var(--o-gold-ink); }

/* Heroes: portal.css clips a gradient into the headline text. On paper a
   headline is one ink. */
body.reg-classic .hero h1,
body.reg-classic .landing-hero h1,
body.reg-classic .state-hero h1,
body.reg-classic .splash-hero h1 {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: var(--o-navy);
}
body.reg-classic .landing-hero::before,
body.reg-classic .hero::before { background: none; }
body.reg-classic .landing-crest-wrap::after { background: none; filter: none; }

/* The hero PLATE itself (`.splash-hero`, the banner image at the top of /about
   and the "programme in preparation" page). portal.css hard-codes
   `border-radius: 0.9rem` and a 30px black drop shadow on it; neither is ever
   reset here, so the single largest element on the page rendered with an
   18.4px radius and a soft modern float while `--radius: 0` squared everything
   around it. A photograph in a prospectus is a plate struck into the page:
   squared, ruled in navy with the gold keyline outside it (an INSET keyline is
   painted under replaced content and would be invisible behind the image), and
   dropped on the register's own warm paper shadow rather than a black one. */
body.reg-classic .splash-hero {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  box-shadow: 0 0 0 1px var(--o-gold-pale), var(--shadow-md);
}

/* ═══ §6 · The doors ══════════════════════════════════════════════════════
   /login, /register and the recovery flows render inside a science-fiction
   vault HUD (.auth-* / .vault-*) — the single loudest modern surface in the
   portal, and the one a visitor meets first. Redrawn here as a sealed letter:
   the animated vault is stood down to a quiet engraved crest, and the form
   becomes ruled fields on a framed leaf. Markup untouched. */
body.reg-classic .auth-screen {
  background: transparent;
}
body.reg-classic .auth-hud {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  /* The modern HUD has no padding — its own children carry the insets, and the
     "frame" is a glow, which needs no margin. Once this register draws a real
     printed rule around the leaf, type running to within a hair of it reads as
     a trimming error; on a 390px column, at the enlarged scale, the
     confidential footer line touched both rules. A ruled panel gets a margin. */
  padding: var(--space-md) var(--space);
  background: var(--o-paper-hi);
  background-image: none;
  box-shadow:
    inset 0 0 0 1px var(--o-paper-hi),
    inset 0 0 0 2px var(--o-gold-pale),
    var(--shadow-md);
  backdrop-filter: none;
}
body.reg-classic .auth-tagline,
body.reg-classic .auth-readout {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.68rem;
  color: var(--o-navy-soft);
}
body.reg-classic .auth-label {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.64rem;
  color: var(--o-navy-soft);
}
body.reg-classic .auth-input-shell {
  border: 0;
  border-bottom: 1px solid var(--o-navy-soft);
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
body.reg-classic .auth-hex,
body.reg-classic .auth-icon { color: var(--o-gold-ink); }
/* The hexagon "scanner" glyphs are pure sci-fi furniture — struck out rather
   than restyled, because there is no antiquarian version of them. */
body.reg-classic .auth-hex { display: none; }
body.reg-classic .auth-submit {
  border-radius: 0;
  background: var(--o-navy);
  color: var(--o-paper-hi);
  border: 1px solid var(--o-navy);
  box-shadow: inset 0 0 0 1px var(--o-paper-hi), inset 0 0 0 2px var(--o-navy);
}
body.reg-classic .auth-submit:hover { background: var(--o-oxblood); border-color: var(--o-oxblood); }
body.reg-classic .auth-links { font-variant-caps: small-caps; letter-spacing: 0.08em; }
body.reg-classic .auth-mode { font-variant-caps: small-caps; letter-spacing: 0.06em; }

/* `--auth-muted` is #7f93ad — a cool grey-blue picked to sit quietly on the
   vault's near-black ground. This register puts the same HUD on paper and never
   repointed it, so it measured 2.99:1 for the only two links on the login page
   ("Forgot your password?", "Create an account") and for every field label
   above them. One token carries both (portal.css uses it in exactly two rules:
   `.auth-label` and `.auth-links a`), so one line fixes both at 7.86:1. */
body.reg-classic .auth-screen { --auth-muted: var(--o-navy-soft); }
/* …and, having been given a legible ink, they must also still look like links:
   portal.css strips the underline and leaves a transparent bottom rule for the
   hover to fill in, which on the modern ground is carried by the blue. On
   paper, in small caps, that leaves two links indistinguishable from the
   tagline under them. Ruled, the way every other link in this register is. */
body.reg-classic .auth-links a { border-bottom-color: var(--o-rule); }
body.reg-classic .auth-links a:hover,
body.reg-classic .auth-links a:focus-visible {
  color: var(--o-oxblood);
  border-bottom-color: var(--o-oxblood);
}

/* The vault. It is a hundred-element inline SVG of a robotic bank-vault door —
   steel gradients, blue core, spinning wheel, scan sweep, hex rain — and there
   is no antiquarian version of any of that. Re-inking it part by part would
   have produced a gold-plated spaceship, so the register stands the whole
   machine down and puts the university's arms in the aperture instead: the
   thing a nineteenth-century institution actually put on its front door.

   The SVG is hidden rather than deleted, so the markup, the ARIA and the
   state machine (portal.js drives data-auth-state on .auth-screen) are all
   untouched — switch back to modern and the vault is there, mid-animation. */
body.reg-classic .vault-svg { display: none; }
body.reg-classic .auth-vault {
  background-image: url("/static/brand/coat-of-arms-traditional.webp");
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  perspective: none;
  /* The arms are taller than wide (531×574); the vault box is square, so
     shrink it a little and let the plate set the height. */
  width: clamp(200px, 46vw, 300px);
}
/* Belt and braces: any vault part that somehow still paints (a future markup
   change, a cached stylesheet) is silenced rather than left glowing. */
body.reg-classic .vault-rim-glow,
body.reg-classic .vault-scan,
body.reg-classic .vault-tv-snow,
body.reg-classic .vault-tv-roll,
body.reg-classic .vault-tv-scan,
body.reg-classic .vault-tv-chroma,
body.reg-classic .vault-flash,
body.reg-classic .vault-hex,
body.reg-classic .blade,
body.reg-classic .bolt,
body.reg-classic .deadbolt,
/* ``.vault-emblem`` is a separate raster (/static/ui/auth-emblem.png — the
   modern chrome-and-cyan "C") layered over the SVG, so hiding .vault-svg
   leaves it floating on top of the arms. It is the modern mark; on this
   register it goes. */
body.reg-classic .vault-emblem,
body.reg-classic .vault-seal { display: none; }

/* ── §6a · The wax seal ───────────────────────────────────────────────────
   The modern door answers a login attempt with a bank vault: the wheel spins,
   a scan sweep runs, hex rain falls, eight blades iris open. Standing that
   down (above) left the classic door with no answer at all — you pressed the
   button and the page sat there. This is the register's answer, and it is the
   thing the institution the page is imitating actually did at the moment it
   admitted you: it pressed a seal.

   NO NEW MARKUP AND NO NEW JAVASCRIPT. portal.js already writes the state
   machine onto .auth-screen[data-auth-state] (idle → verifying → granted |
   denied → hold | locked | error) and already guarantees a ≥3s dwell on
   VERIFYING so the choreography is always seen whatever the server latency.
   The seal is a ::after on .auth-vault — a box that is `position: relative`,
   `aria-hidden`, and whose only remaining content on this register is the
   arms painted as a background — so the wax lands ON the arms with nothing
   added to the DOM. The plate is /static/ui/classic/seals/wax-red.png, re-cut
   at 165×220 on 2026-08-10 (it was 73×94). The ratio below tracks the FILE:
   with `contain`, a stale ratio letterboxes the wax inside its own box and
   the press looks off-centre for no visible reason.

   THE RESTING POSITION IS THE BASE RULE, and every state animation ends on
   it. That is what keeps the requirement that no state can leave a seal stuck
   in mid-air: mid-air only exists inside a keyframe, never in a computed
   style. Two states (idle, hold) simply never turn it on; two (denied, error)
   lift it back off and end at opacity 0.

   Only transform and opacity are animated — no width, no top, no filter
   keyframes — so every state is a compositor job and nothing reflows. The
   drop-shadow is static: wax is a physical object about a millimetre thick,
   and that is the one shadow this register's rules do allow a page to cast. */
body.reg-classic .auth-vault::after {
  content: "";
  position: absolute;
  z-index: 5;
  /* Off the arms' own axis, low and to the right, over the tail of the motto
     ribbon. Dead-centre was tried and read as a *charge* — one more element of
     the heraldry — instead of as a separate object laid on top of it; and it
     buried the shield. Off-register, slightly askew and overlapping the
     ribbon is what a seal actually looks like on a document, because a clerk
     pressed it by hand and did not line it up with the engraving. */
  left: 62%;
  top: 74%;
  width: 26%;                     /* ≈ the plate's native 73px on a 300px box */
  aspect-ratio: 165 / 220;
  margin: 0;
  background: url("/static/ui/classic/seals/wax-red.png") center / contain no-repeat;
  filter: drop-shadow(0 1px 1px rgba(90, 72, 34, 0.38));
  pointer-events: none;
  /* THE RESTING POSITION — pressed, opaque, and five degrees off square. */
  transform: translate(-50%, -50%) scale(1) rotate(-5deg);
  transform-origin: 50% 50%;
  opacity: 0;                     /* …but not shown until a state asks for it */
}

/* VERIFYING (≥3s, enforced in portal.js). The seal descends from off the top
   of the leaf, over-presses by a hair, rocks once as the wax spreads, and
   settles. ~1.05s of movement, then it sits perfectly still for the remaining
   two seconds — which is the point: the stillness after the press is what
   reads as "being considered", and it is the only kind of waiting a printed
   page knows how to show. `forwards` holds the resting position. */
body.reg-classic .auth-screen[data-auth-state="verifying"] .auth-vault::after {
  animation: reg-seal-press 1050ms cubic-bezier(0.34, 0.9, 0.24, 1) both;
}

/* GRANTED. The seal is already down, so nothing descends again — the wax is
   simply driven home once more and left. One short 420ms press, ending on the
   identical resting transform, so the verifying→granted handover has no jump.
   The rest of the ~2.9s before the redirect is stillness. */
body.reg-classic .auth-screen[data-auth-state="granted"] .auth-vault::after {
  opacity: 1;
  animation: reg-seal-confirm 420ms cubic-bezier(0.3, 0.7, 0.2, 1) both;
}

/* DENIED, and ERROR. The impression is refused: the seal is lifted straight
   back off the page and taken away, leaving the leaf blank. It ends at
   opacity 0 — so DENIED's follow-on HOLD state (a 5s countdown before the
   reload) inherits an empty page and has nothing to leave hanging. ERROR is
   the network/500 fallback, after which portal.js re-enables the form: the
   same lift-off returns the door to exactly its idle appearance, ready for a
   second attempt. */
body.reg-classic .auth-screen[data-auth-state="denied"] .auth-vault::after,
body.reg-classic .auth-screen[data-auth-state="error"] .auth-vault::after {
  animation: reg-seal-lift 520ms cubic-bezier(0.4, 0, 0.7, 0.4) both;
}

/* LOCKED — terminal; no retry, no redirect. Here the seal is not a welcome,
   it is a closure: struck a second time, harder, and left deliberately askew,
   the way a clerk cancels a file rather than signs it. It runs against
   --auth-locked-dur (portal.js sets it from the real length of the lockdown
   audio; the var()'s own default carries the timing if the sound never
   loads) so the register stays in step with the modern door's clock. */
body.reg-classic .auth-screen[data-auth-state="locked"] .auth-vault::after {
  opacity: 1;
  animation: reg-seal-cancel calc(var(--auth-locked-dur, 14.76s) * 0.0610)
    cubic-bezier(0.5, 0, 0.75, 1) both;
}
/* portal.css closes a lockdown with a red→black radial vignette over the whole
   screen (`.auth-screen[data-auth-state="locked"]::after`) — soot on cream.
   Re-inked as a flat oxblood wash that simply deepens: the leaf is stained
   shut. Flat, because this register does not use a gradient as decoration. */
body.reg-classic .auth-screen[data-auth-state="locked"]::after {
  background: rgba(110, 22, 34, 0.10);
  animation: reg-seal-stain calc(var(--auth-locked-dur, 14.76s) * 0.4) ease-out both;
}

/* The press. Descends from off the top of the leaf held at an angle, meets
   the paper at 45%, over-presses (scale < 1 — the wax spreading), rocks once
   as the matrix is lifted clear, and settles on the resting transform. */
@keyframes reg-seal-press {
  0%   { opacity: 0; transform: translate(-50%, -380%) scale(1.55) rotate(-14deg); }
  45%  { opacity: 1; transform: translate(-50%, -72%)  scale(1.14) rotate(-9deg); }
  62%  { opacity: 1; transform: translate(-50%, -50%)  scale(0.94) rotate(-3.5deg); }
  78%  { opacity: 1; transform: translate(-50%, -53%)  scale(1.04) rotate(-5.8deg); }
  100% { opacity: 1; transform: translate(-50%, -50%)  scale(1)    rotate(-5deg); }
}
@keyframes reg-seal-confirm {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(1)     rotate(-5deg); }
  38%  { opacity: 1; transform: translate(-50%, -50%) scale(0.955) rotate(-5deg); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1)     rotate(-5deg); }
}
@keyframes reg-seal-lift {
  0%   { opacity: 1; transform: translate(-50%, -50%)  scale(1)    rotate(-5deg); }
  30%  { opacity: 1; transform: translate(-50%, -64%)  scale(1.05) rotate(-1deg); }
  100% { opacity: 0; transform: translate(-50%, -300%) scale(1.35) rotate(8deg); }
}
@keyframes reg-seal-cancel {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(1)    rotate(-5deg); }
  22%  { opacity: 1; transform: translate(-50%, -80%) scale(1.10) rotate(-1deg); }
  52%  { opacity: 1; transform: translate(-50%, -50%) scale(0.92) rotate(-15deg); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1)    rotate(-14deg); }
}
@keyframes reg-seal-stain {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* The always-on CRT scanline shimmer portal.css lays over the whole auth
   screen is both a science-fiction artefact and a permanently moving surface —
   two things this register does not have. Struck out, so the only thing that
   ever moves on the classic door is the seal. */
body.reg-classic .auth-screen::before { display: none; }

/* The state readout is neon on the modern door — a coloured glow per state,
   plus a pulse on VERIFYING. Re-set in ink: no shadow, no pulse, and the
   register's two colours doing the work (navy = the institution speaking,
   oxblood = a refusal). portal.css's rules are (0,3,0), so these have to lead
   with .auth-screen too in order to reach them. */
body.reg-classic .auth-screen[data-auth-state] .auth-readout {
  text-shadow: none;
  animation: none;
  letter-spacing: 0.2em;
  padding-left: 0.2em;
  font-weight: 700;
}
body.reg-classic .auth-screen[data-auth-state="verifying"] .auth-readout { color: var(--o-navy-soft); }
body.reg-classic .auth-screen[data-auth-state="granted"] .auth-readout { color: var(--o-navy); }
body.reg-classic .auth-screen[data-auth-state="denied"] .auth-readout,
body.reg-classic .auth-screen[data-auth-state="hold"] .auth-readout { color: var(--o-oxblood); }
body.reg-classic .auth-screen[data-auth-state="error"] .auth-readout { color: var(--warn); }
body.reg-classic .auth-screen[data-auth-state="locked"] .auth-readout {
  color: var(--o-oxblood);
  font-weight: 700;
  letter-spacing: 0.24em;
  padding-left: 0.24em;
  text-shadow: none;
}

/* Reduced motion: the seal still tells the story — it is either on the page or
   it is not — but it stops travelling. Every state collapses to a plain 260ms
   cross-fade at the resting position, so the end frames are pixel-identical to
   the full-motion ones and only the journey is dropped.

   This rule used to carry an `!important` on every animation, because
   portal.css's global reduce-motion kill was `animation: none !important` on
   `*` — which did not leave the seal still, it left it never drawn at all (its
   base state is `opacity: 0`), so a visitor who asked for less motion got a
   blank door and no answer. That was portal.css's bug, not this register's,
   and it has been fixed at the source: the global rule now COLLAPSES every
   animation to 0.01ms instead of cancelling it, and an element may hand back a
   real duration for an animation that changes only opacity by declaring
   `--motion-fade-duration`. A cross-fade moves nothing — nothing translates,
   rotates or scales — so it is what prefers-reduced-motion asks for, not an
   override of it. The `!important`s are gone; the declaration below is the
   whole of the contract.

   The `opacity` alongside each animation stays. It is the floor: if a future
   rule wins the animation fight anyway, the seal is still in the right state —
   shown for verifying/granted/locked, gone for denied/error — just without the
   fade. The animation is the nicety; the opacity is the contract. */
@media (prefers-reduced-motion: reduce) {
  body.reg-classic .auth-screen[data-auth-state="verifying"] .auth-vault::after,
  body.reg-classic .auth-screen[data-auth-state="granted"] .auth-vault::after,
  body.reg-classic .auth-screen[data-auth-state="locked"] .auth-vault::after {
    opacity: 1;
    --motion-fade-duration: 260ms;
    animation: reg-seal-fade-in 260ms linear both;
  }
  body.reg-classic .auth-screen[data-auth-state="denied"] .auth-vault::after,
  body.reg-classic .auth-screen[data-auth-state="error"] .auth-vault::after {
    opacity: 0;
    --motion-fade-duration: 260ms;
    animation: reg-seal-fade-out 260ms linear both;
  }
  /* Same for the lockdown stain, which would otherwise appear as a hard flash
     of colour over the whole leaf — the one thing louder than the fade. */
  body.reg-classic .auth-screen[data-auth-state="locked"]::after {
    --motion-fade-duration: 400ms;
    animation: reg-seal-stain 400ms linear both;
  }
}
@keyframes reg-seal-fade-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(1) rotate(-5deg); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(-5deg); }
}
@keyframes reg-seal-fade-out {
  from { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(-5deg); }
  to   { opacity: 0; transform: translate(-50%, -50%) scale(1) rotate(-5deg); }
}

/* The submit control is an octagon cut with clip-path. Square it: a printed
   button is a rectangle struck into the page. */
body.reg-classic .auth-submit {
  clip-path: none;
  width: auto;
  min-width: 132px;
  height: auto;
}

/* ═══ §7 · Set pieces ═════════════════════════════════════════════════════ */

/* Landing: the eyebrow rules and trust row already read as engraving once the
   gold is re-inked; the CTA row and cards are covered above. */
body.reg-classic .landing-eyebrow,
body.reg-classic .landing-kicker,
body.reg-classic .cat-label,
body.reg-classic .syl-kicker,
body.reg-classic .ust-eyebrow,
body.reg-classic .founder-eyebrow {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--o-gold-text);
}
body.reg-classic .landing-card-index,
body.reg-classic .guide-crest-level { color: var(--o-gold-text); font-family: var(--font-display); }

/* /about: the two gold inks and the mark-card caption.
   portal.css sets `.about-motto` / `.about-section-title` to #b8860b and lifts
   them to #e0b341 under `[data-theme="dark"]` — which base.html hard-codes on
   <html> for every page. On this register's cream that landed at 1.77:1 and
   1.87:1. The dark lift is now scoped away from the classic register in
   portal.css (it is a statement about the ground, not about the attribute);
   these are the paper inks it falls back to. */
body.reg-classic .about-motto,
body.reg-classic .about-section-title { color: var(--o-gold-text); }
/* No figcaption override here: the dark plate is back under the crest (see the
   dedicated `.about-mark-card` rule after §5), so portal.css's fixed #cdd3da
   caption ink is correct again — `--o-navy-soft` on that plate would be the
   new contrast failure. */

/* Catalogue: level heads become chapter rules. */
body.reg-classic .cat-level-head {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  border-bottom: 2px solid var(--o-navy);
  padding-bottom: 0.35rem;
}
body.reg-classic .cat-price-free { font-variant-caps: small-caps; color: var(--o-oxblood); }
body.reg-classic .cat-price-off { color: var(--o-oxblood); }

/* Gamification (gamification.css): the grade gems are already cut in both
   registers — only the modern neon housing needs removing. */
body.reg-classic .grade-gems,
body.reg-classic .pratio,
body.reg-classic .mark-bar-wrap,
body.reg-classic .ust-bar-track,
body.reg-classic .ust-meter,
body.reg-classic .ov-meter {
  border-radius: 0;
  box-shadow: none;
  background: rgba(16, 36, 63, 0.06);
}
body.reg-classic .mark-bar,
body.reg-classic .ust-bar-fill,
body.reg-classic .ov-meter-bar {
  border-radius: 0;
  background-image: none;
  background-color: var(--o-gold-ink);
  box-shadow: none;
}
body.reg-classic .band-text-distinction,
body.reg-classic .band-text-excellence { color: var(--o-oxblood); }

/* The manifesto (library/index.html) is drawn as a green-phosphor CRT on the
   modern register and already carries a `[data-ui="classic"]` branch — an aged
   parchment terminal in gold and oxblood. Nothing to add here beyond making
   sure the housing is square and unlit; the branch supplies the rest. */
body.reg-classic .lib-crt,
body.reg-classic .lib-crt-screen,
body.reg-classic .lib-crt-bar {
  border-radius: 0;
  box-shadow: none;
}

/* Anything still animating a glow, a pulse or a sweep on the register: stop.
   A printed page does not move. Kept last so it wins over the component rules
   above, and narrow enough that genuine progress/state transitions survive. */
body.reg-classic .skeleton,
body.reg-classic .reveal,
body.reg-classic .grow { animation: none; }

@media print {
  body.reg-classic::after { display: none; }
  body.reg-classic { background: #fff; }
}

/* ═══ §8 · The signed-in walk ═════════════════════════════════════════════
   Everything above was verified logged OUT. This section is the long tail the
   design note predicted: what a signed-in student actually meets — the
   dashboard, the catalogue, the programme overview, the guide, marks, the
   profile form, the library and search — walked at 1280px on the classic
   register. The token repointing carried nearly all of it. What follows is the
   list of surfaces that hold their own colour instead of the tokens', so the
   register could not reach them, plus a handful that the register reached only
   halfway and left worse than it found them. */

/* THE SHEET WAS TWO SHEETS, AND THE GRAIN WAS ON THE WRONG ONE. Every signed-in
   page whose content is shorter than the viewport — the dashboard, marks,
   search, all three not-found pages — showed a hard horizontal seam where the
   footer ended: paper above, a visibly warmer and coarser paper below.

   The cause is the parchment overlay in §2. It is `z-index: -1`, so it paints
   with the negative-index children of the root — which is BEFORE the block
   backgrounds of in-flow descendants, and <body> is one of those. Its own
   opaque cream therefore covered the grain over the whole content area, and the
   grain was only ever visible in the strip below the body box where nothing
   covered it. The register was not wearing its texture; it was wearing a band
   of it at the bottom of short pages.

   So the ground moves up one level: <html> takes the paper and the printer's
   vignette (fixed to the viewport, exactly as §2 set them), and <body> stops
   painting a ground of its own. One sheet, laid grain across all of it, seam
   gone. §2's rule is left intact as the statement of what the ground IS; this
   only moves where it is painted. */
html[data-ui="classic"] {
  background:
    radial-gradient(120% 85% at 50% -15%, rgba(255, 254, 249, 0.85), transparent 66%),
    radial-gradient(100% 100% at 50% 125%, rgba(120, 100, 60, 0.05), transparent 60%),
    #F7F3E8;
  background-attachment: fixed;
}
body.reg-classic {
  background-color: transparent;
  background-image: none;
}

/* ── The university chat ────────────────────────────────────────────────────
   chat.css opens by declaring itself "deliberately committed to a dark glass
   look (independent of the page theme)". Independent of the theme it may be,
   but not of the register: it is the one component that sits on top of every
   signed-in page, and on cream paper it read as a black lozenge floating over
   the prospectus. Worse, the register had half-converted it — the generic
   button and link rules in §4 had already re-inked its title, its channel names
   and its Send control to Oxford blue, and navy on near-black is not text any
   reader can recover. It was the single worst thing on the signed-in portal.

   The good news is that chat.css tokenised its own palette for exactly this
   ("the palette is a one-line swap (incl. a possible light-panel flip)"), so
   the panel converts the same way the portal did: repoint the tokens and the
   component follows. The role colours go with them — orange, purple and a
   pillar-box red are three inks this press does not have. */
body.reg-classic .cchat {
  --chat-glass-bg: var(--o-paper-hi);
  --chat-glass-head: rgba(16, 36, 63, 0.05);
  --chat-glass-border: var(--o-rule);
  --chat-fg: #1E1A14;
  --chat-muted: var(--o-navy-soft);
  --chat-accent: var(--o-navy);
  --chat-danger: var(--o-oxblood);
  --chat-radius: 0px;
  --chat-role-student: var(--o-navy);
  --chat-role-professor: var(--o-gold-ink);
  --chat-role-admin: var(--o-navy-soft);
  --chat-role-ceo: var(--o-oxblood);
  font-family: var(--font-body);
}

/* The leftovers the tokens do not reach: chat.css raises a surface with white
   at 6–10% and recesses one with black at 16–20%, which is how you model depth
   on dark glass and how you get grey smears on paper. Each becomes what it
   meant — a rule, a wash of the first ink, or nothing at all. */
body.reg-classic .cchat-panel {
  border: 1px solid var(--o-navy);
  box-shadow: var(--shadow-lg);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body.reg-classic .cchat-drawer {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body.reg-classic .cchat-head { border-bottom: 2px solid var(--o-navy); }
body.reg-classic .cchat-title {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.68rem;
  color: var(--o-navy);
  /* …and it must be allowed to give way. `.cchat-head` is a nowrap flex row
     ending in the five window controls, so whatever the row cannot fit is
     pushed off the panel's right edge and clipped — and the thing at that
     edge is the Close button. Of the four items in the row only two can
     yield: `.cchat-ctx` already can (flex: 1), and the title could not,
     because `white-space: nowrap` makes a flex item's automatic minimum size
     its whole text. The register makes the title the widest thing in the row
     — Cinzel caps with 0.12em of tracking sets "Cortex University" at 140px
     against the modern face's 105 — so on a 296px panel it was the title
     that stole the Close button's space. `min-width: 0` re-enables the shrink
     the ellipsis was already written for: the name abbreviates, the controls
     stay reachable, and that is the correct order of sacrifice.
     The order is most visible MINIMISED, where the dock is 220px and the five
     controls are 182 of it: the register spends what is left on the controls
     and the title goes to nothing, so a collapsed dock reads as its row of
     buttons. The modern register makes the opposite trade at that width and
     keeps a label whose last three buttons — Close among them — hang off the
     panel and cannot be clicked at any viewport size. Measured, not assumed. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.reg-classic .cchat-rail,
body.reg-classic .cchat-compose,
body.reg-classic .cchat-uploadbar { background: rgba(16, 36, 63, 0.04); }
body.reg-classic .cchat-rail-label {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--o-gold-text);
}
body.reg-classic .cchat-rail-item:hover,
body.reg-classic .cchat-roster-item:hover,
body.reg-classic .cchat-usermenu button:hover { background: rgba(169, 135, 60, 0.12); }
/* The selected channel was a wash of electric blue — the one hue the register
   forbids outright, and the brightest thing on any signed-in page. */
body.reg-classic .cchat-rail-item.active {
  background: rgba(169, 135, 60, 0.18);
  color: var(--o-oxblood);
}
body.reg-classic .cchat-rail-item,
body.reg-classic .cchat-roster-item { border-radius: 0; }
/* Every input, chip and control in the panel is a white-on-glass raise. */
body.reg-classic .cchat-compose input[type="text"],
body.reg-classic .cchat-attach,
body.reg-classic .cchat-filechip,
body.reg-classic .cchat-drawer select,
body.reg-classic .cchat-nameform input,
body.reg-classic .cchat-nameform button,
body.reg-classic .cchat-adminlinks a {
  border-radius: 0;
  background: var(--o-paper);
  border-color: var(--o-navy-soft);
}
body.reg-classic .cchat-send {
  border-radius: 0;
  background: var(--o-navy);
  color: var(--o-paper-hi);
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.66rem;
}
/* The unread count and the launcher's alert dot are lozenges in a fluorescent
   red (#ff5c5c) that belongs to a notification tray, not to a seal. */
body.reg-classic .cchat-unread,
body.reg-classic .cchat-launcher-dot {
  border-radius: 0;
  background: var(--o-oxblood);
  border-color: var(--o-oxblood);
  color: var(--o-paper-hi);
}
/* Presence: #37d67a is a phosphor green. Present reads in the ok ink, absent in
   the second ink, which is the distinction the roster is actually drawing. */
body.reg-classic .cchat-pres--online { background: var(--ok); }
body.reg-classic .cchat-usermenu,
body.reg-classic .cchat-profilecard {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  box-shadow: var(--shadow-md);
}
/* The pending-name notice was blue-on-blue: a tinted panel, a blue keyline and
   #cfe0ff text. As a notice it is the same object as a .flash, so it is ruled
   like one. */
body.reg-classic .cchat-namepending {
  border-radius: 0;
  background: var(--o-paper);
  border-color: var(--o-gold-ink);
  color: var(--fg);
  font-style: italic;
}
/* Closed, the whole widget collapses to a 54px button carrying a blue steel
   gradient and a black drop shadow — a modern app's chat bubble. Struck flat. */
body.reg-classic .cchat-launcher {
  border-radius: 0;
  background: var(--o-navy);
  background-image: none;
  color: var(--o-paper-hi);
  box-shadow: var(--shadow-md);
}
body.reg-classic .cchat-launcher:hover {
  transform: none;
  background: var(--o-oxblood);
  box-shadow: var(--shadow-md);
}
body.reg-classic .cchat-drawer hr { border-top-color: var(--o-rule-faint); }

/* ── The 404 plate ──────────────────────────────────────────────────────────
   /notifications, /practice and /academic-calendar do not exist yet, so a
   signed-in student meets the not-found page routinely — and it leads with a
   1024px raster of a violet neon "404" glowing inside the modern hexagon "C",
   which is both the loudest and the only place the modern mark still appears on
   this register. There is no antiquarian plate under /static/ui/state/ to
   resolve to, and re-inking a neon render with a filter produces a muddy neon
   render. So the plate is stood down exactly as the vault is in §6 — the
   engraved heading, the tagline and the way back are the page, and a printed
   prospectus would not have illustrated a missing page at all. The real fix is
   a classic plate in ui/state/; this is the holding line until there is one. */
body.reg-classic .state-hero { display: none; }

/* ── The running head, signed in ────────────────────────────────────────────
   Logged out the nav is four links; logged in it grows a persona chip and a
   Logout control, and neither had been given the register's voice. The chip was
   set in Garamond upper-and-lower on a grey fill — a modern account pill, and
   the only filled block in a bar that is otherwise all rules. Logout is a
   `button.link`, which portal.css sizes past every rule in §3, so it stood a
   third taller than the menu it sits in and shouted the exit. */
body.reg-classic .nav-identity {
  background: transparent;
  border-bottom: 1px solid var(--o-rule);
  font-variant-caps: small-caps;
  letter-spacing: 0.08em;
  color: var(--o-navy);
}
body.reg-classic .topnav button.link {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  color: var(--o-navy);
}

/* THE CLUSTER IS ALLOWED TO TAKE A SECOND LINE. `.nav-controls` is a nowrap
   flex row pinned at `flex: none`, so its min-content is a hard floor under
   the whole document: whatever it comes to, the page cannot be narrower, and
   below that width every signed-in page scrolls sideways. Un-plating the
   controls above took the classic floor from 430px to 387 — but the modern
   register's own floor is 366, and it scrolls below 380 for exactly this
   reason. So the floor is not something the register created and it is not
   something the register can type its way out of: seven controls with real
   text in them do not fit across 320px at any size worth reading, and
   shrinking them until they do is how you get a row of unhittable 20px
   targets. A printed page has no horizontal scroll, so the row wraps
   instead — full width when there is room, two lines when there is not, and
   nothing hidden, truncated or made smaller in either case. `flex: 0 1 auto`
   is what lets the cluster be offered less than its max-content in the first
   place; without it `flex: none` hands it the full width and the wrap never
   fires. Right-aligned, because it is still the right-hand end of the
   running head. */
body.reg-classic .topnav .nav-controls {
  flex: 0 1 auto;
  min-width: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: var(--space-xs);
}
/* …but on a phone the cluster is not the right-hand END of anything: it is its
   own row. portal.css's `@media (max-width: 640px)` gives it `flex: 1 0 100%`
   and `justify-content: flex-start` so the header is two purposeful rows
   instead of four accidental ones; the rule above is (0,3,1) and would win by
   specificity alone, dragging the cluster back onto the brand line. Restated
   here at the same breakpoint so the register agrees with the layout rather
   than out-specifying it — right-aligned is still correct everywhere it has a
   line to share. */
@media (max-width: 640px) {
  body.reg-classic .topnav .nav-controls {
    flex: 1 0 100%;
    justify-content: flex-start;
  }
}

/* ── Page furniture ─────────────────────────────────────────────────────────
   The kicker above every signed-in page title ("Program", "Records") is a
   `.mono` line, so it arrived in the browser's default monospace — a typewriter
   label above a stone-cut headline. It is doing the job §7's eyebrows do, so it
   is set the way they are. Scoped to the page header: `.mono` elsewhere is an
   ID or a token, where a monospaced face is the point. */
body.reg-classic .page-header .mono {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: 0.66rem;
  color: var(--o-gold-text);
}

/* The dashboard's one call to action is an `a.primary`, not a `button`, so the
   struck-plate rule in §4 never reached it and it kept portal.css's P21
   gold→oxblood diagonal gradient with white lowercase text: the only gradient
   left on the signed-in portal, sitting inside a ruled frame. */
body.reg-classic a.primary,
body.reg-classic a.primary.btn-link {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.76rem;
  border-radius: 0;
  border: 1px solid var(--o-navy);
  background: var(--o-navy);
  background-image: none;
  color: var(--o-paper-hi);
  box-shadow: inset 0 0 0 1px var(--o-paper-hi), inset 0 0 0 2px var(--o-navy);
  padding: 0.72em 1.4em;
}
body.reg-classic a.primary:hover,
body.reg-classic a.primary.btn-link:hover {
  background: var(--o-oxblood);
  border-color: var(--o-oxblood);
  box-shadow: inset 0 0 0 1px var(--o-paper-hi), inset 0 0 0 2px var(--o-oxblood);
}

/* The profile page opens with a "complete your profile" notice drawn as a
   modern alert: a rounded pink panel keylined in a signal red that is nowhere
   in this palette. It is the same object as a .flash, and is ruled like one. */
body.reg-classic .profile-status {
  border-radius: 0;
  background: var(--o-paper-hi);
  background-image: none;
  border: 1px solid var(--o-navy);
  border-left: 3px solid var(--o-oxblood);
  color: var(--fg);
  font-style: italic;
  box-shadow: var(--shadow-sm);
}

/* A modal darkens the page behind it with the browser's default backdrop, which
   over cream reads as a dirty olive. The page behind should look like the sheet
   seen through the first ink, not like mould. */
body.reg-classic dialog::backdrop {
  background: rgba(16, 36, 63, 0.45);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The global infographic lightbox (`lightbox.css`, opened by clicking any
   `.gx-zoomable` — the path map, the guide's journey diagram, the handbook
   plates) is the one dialog shell the register never reached. Its INSIDE was
   already right: the tokens carry the panel's ground and ink, and §9 re-inks the
   path map itself, so clicking the diagram produced a correctly engraved plate
   mounted in a modern frame — 12px corners and a 24px/80px black glow, the two
   things §2 rules out by name. The declarations that were wrong are the ones
   asking for their own numbers instead of a token, which is why the token sweep
   went past them; they become the same double-keyline mount every other panel on
   the register wears (`.card` / `.catd-head`, §5). Backdrop, close button and
   zoom controls needed nothing: they resolve through `dialog::backdrop` above and
   the generic button rules of §4. */
body.reg-classic .gx-lightbox {
  border-radius: 0;
  border: 1px solid var(--o-navy);
  background: var(--o-paper-hi);
  box-shadow:
    inset 0 0 0 1px var(--o-paper-hi),
    inset 0 0 0 2px var(--o-gold-pale),
    var(--shadow-sm);
}

/* §7 put the gem rows in the same tinted housing as the progress meters, on the
   grounds that they are all "gamification". They are not the same object: a
   meter needs a track to be read against, a row of struck emblems needs the
   page. On the guide's marks table the housing showed as five grey slabs
   ruled into an otherwise clean ledger — and, because the tier plates the
   template asks for are missing under /static/emblems/ in BOTH registers, the
   slabs were all there was to see. The gems sit on the paper. */
body.reg-classic .grade-gems {
  background: transparent;
}

/* ── Grids with a minimum column wider than a phone ─────────────────────────
   `repeat(auto-fill, minmax(X, 1fr))` does not shrink below X. When X exceeds
   the container, the track sticks out and the whole page scrolls sideways —
   the classic auto-fill trap, and the fix is the classic one:
   `minmax(min(X, 100%), 1fr)`, which is a no-op wherever there is room for X
   and clamps the track to the container where there is not.

   Two different causes land here, both only visible once the nav cluster
   above stopped being the widest thing on a phone and hiding them.
   `.guide-attr-list` / `.guide-emblem-list` ask for 320px flat, so they
   overflow a 320px window on BOTH registers — portal.css's bug, not the
   register's, and it stays fixed here only because portal.css is not this
   file's to edit. `.lib-grid` asks for 17rem, which the dial turns from
   263px into 306px in a 288px column: that one the register did cause, and
   it is the one place where making a rem-based sheet bigger runs into a
   length that has to keep fitting a device. */
body.reg-classic .guide-attr-list,
body.reg-classic .guide-emblem-list {
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
}
body.reg-classic .guide-crest-showcase {
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
}
body.reg-classic .lib-grid {
  grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
}

/* ── The corners the token sweep missed ─────────────────────────────────────
   --radius is repointed to 0 in §1, which squares everything that asks for the
   token. These ask for their own number instead — 4px on an advisory, 8px on a
   licence chip, 999px on the enrolment-slot pips, 14px on a dialog — so they
   stayed rounded while their neighbours went square, and on a ruled page one
   rounded corner in a row of sharp ones is more visible than all of them being
   round. Collected in one place rather than chased into each component, because
   the reason is identical every time. */
body.reg-classic .guide-attr-item,
body.reg-classic .guide-emblem-item,
body.reg-classic .guide-crest-item,
body.reg-classic .guide-clause,
body.reg-classic .guide-attr-item img,
body.reg-classic .guide-emblem-item img,
body.reg-classic .guide-crest-item img,
body.reg-classic .cat-level-note,
body.reg-classic .cat-prior-ed,
body.reg-classic .cat-level1-boost,
body.reg-classic .cat-prior-ed-historical,
body.reg-classic .cat-status,
body.reg-classic .ov-slot,
body.reg-classic .ov-advisory,
body.reg-classic .flag-chip,
body.reg-classic .lib-ip-note,
body.reg-classic .lib-cat-tag,
body.reg-classic .lib-licence-chip,
body.reg-classic .path-dialog,
/* The last two 999px pills on the register: the syllabus page's tier switcher
   (`.syl-tier`, up to six of them in a row under the heading of every multi-rung
   programme) and the newsletter's category filters (`.nl-filter`). Both are
   keylined chips of text, which this register sets as ruled tabs, not lozenges. */
body.reg-classic .syl-tier,
body.reg-classic .nl-filter,
body.reg-classic code {
  border-radius: 0;
}

/* The enrolment meter's used pips were the last `--m-violet` fall-through on the
   register — the same root cause as the mauve `.catd-head.is-doctoral` hairline
   fixed above, and fixed the same way. portal.css paints `.ov-slot.is-used` with
   `linear-gradient(90deg, var(--m-cyan-deep, #0099cc), var(--m-violet, #7b2fff))`
   and both of those custom properties are declared only under `body.reg-modern`
   (`tokens-modern.css`), so classic took the literals and printed a cyan-to-neon-
   violet bar — two saturated hues found nowhere else on the paper — across the top
   of `/overview`. §2 forbids a gradient used as decoration in any case: a filled
   slot is filled, so it is one ink. The empty slots stay `var(--border)`
   (repointed to `--o-rule`), which keeps used/free at 8.9:1 against each other,
   and the meter's own "N of 3 in flight" text states the count in words besides. */
body.reg-classic .ov-slot.is-used {
  background: var(--o-navy);
  background-image: none;
}

/* ═══ §9 · The path map ═══════════════════════════════════════════════════
   `/path-overview` serves one hand-drawn diagram inline — 660 lines of SVG
   with every colour as a presentation attribute (`static/diagrams/
   path-overview.svg`, held in `routes/catalogue.py::_PATH_OVERVIEW_SVG`).
   Written for the modern register, it is a slate-and-neon chart: nine
   saturated level chips, a near-black off-spine column, a navy gradient
   masthead. Dropped on cream, those solid darks read as holes punched in the
   paper rather than as ink on it.

   CSS reaches all of it, because a `fill="…"` presentation attribute loses to
   any stylesheet declaration — so this section re-inks the same markup, and
   only for this register. Nothing here is a second copy of the diagram: the
   modern register keeps every original attribute, since not one selector below
   matches without `body.reg-classic`.

   WHAT REPLACES COLOUR. The modern chart distinguishes its bands purely by
   hue and darkness — teal L1, blue L2, violet L3, green/yellow/orange L4–L6,
   deep violet apex, amber honours. Flatten that to parchment and the ranks
   collapse into one look, so the distinction moves onto the channels a press
   actually had: the WEIGHT of the frame carries rank, the INK carries kind,
   and a warm tint on the plate marks the doctoral threshold.

     L0  advisory            paper,     navy-soft hairline, DASHED
     L1  on-ramp             paper-hi,  navy hairline (1)
     L2  MSc                 paper-hi,  navy rule (2)
     L3  PhD                 tinted,    navy rule (2)          ← the tint
     L4–L6 post-doctoral     tinted,    navy heavy rule (3)
     L7  apex                tinted,    OXBLOOD heavy rule (3)
     L8–L10 conferred        tinted,    GOLD rule (2.5)

   So base → doctoral → apex → honours differ on at least two channels each,
   the SVG's own six-swatch legend and the HTML legend under the figure are
   re-inked to match, and the text label inside every chip is untouched — the
   diagram never depended on colour alone and still does not.

   The second ink does one more job: whitehat defensive security stays navy and
   blackhat offensive security becomes oxblood, which is the distinction the
   blue/red pair was drawing all along.

   Contrast, measured against the plate each mark actually sits on:
   navy #10243F on paper-hi 14.8:1, on the tint 12.7:1; oxblood 11.1 / 9.5:1;
   navy-soft (secondary figures) 7.9 / 6.7:1; reversed paper on a navy roundel
   14.1:1. Gold is `var(--accent)` (#7A5E18, the bronze §1 already uses because
   raw gold leaf fails on cream) not `--o-gold-ink`, so even the honours frame
   clears 3:1 as a graphical object — #A9873C would have come in at 2.7:1. */

body.reg-classic {
  /* The tinted plate: one warm step below the paper, for doctoral and above.
     Declared on the body rather than on `.pm-svg` so the HTML legend under the
     figure — which is a sibling of the diagram, not a descendant — can state
     the same three grounds without repeating the hex. Third ground in the
     register; §1's two are the sheet and the leaf a panel prints on. */
  --o-plate: #F0E7CF;
}

/* Square corners and no drop shadows: §2's rules, applied to SVG geometry.
   `rx`/`ry` and `filter` are CSS properties on SVG elements, so both the
   `rx="8"` attributes and the two `filter="url(#cardShadow)"` references are
   overridable from here without touching the file. */
body.reg-classic .pm-svg rect { rx: 0; ry: 0; }
body.reg-classic .pm-svg [filter] { filter: none; }

/* ── The sheet and the masthead ──────────────────────────────────────────
   The diagram's own backdrop becomes the page's paper. The masthead was a
   navy gradient band; it becomes type on paper closed by a single rule.
   That rule is the band's own stroke, dashed so that only the bottom edge
   inks: a rect's stroke path starts at its top-left corner and runs
   clockwise, so `0 1495 1400 95` = nothing, skip top+right (1400+95), ink
   the bottom (1400), skip the left (95). Cheaper than adding an element. */
body.reg-classic .pm-svg rect[fill="#f8fafc"] { fill: var(--o-paper); }
body.reg-classic .pm-svg rect[fill="url(#headerGrad)"] {
  fill: var(--o-paper);
  stroke: var(--o-navy);
  stroke-width: 3;
  stroke-dasharray: 0 1495 1400 95;
}

/* ── The spine: level chips, ranked by frame ─────────────────────────────
   Each chip is the one rect in the file carrying that fill AND a stroke; the
   same fills appear again, strokeless, as the 14px legend swatches, which are
   re-inked identically further down. */
body.reg-classic .pm-svg rect[fill="#e0f2fe"][stroke],
body.reg-classic .pm-svg rect[fill="#e0f2fe"]:not([stroke]) {
  fill: var(--o-paper);
  stroke: var(--o-navy-soft);
  stroke-width: 1;
  stroke-dasharray: 5 3;
}
body.reg-classic .pm-svg rect[fill="#0e7490"][stroke],
body.reg-classic .pm-svg rect[fill="#0e7490"]:not([stroke]) {
  fill: var(--o-paper-hi);
  stroke: var(--o-navy);
  stroke-width: 1;
}
body.reg-classic .pm-svg rect[fill="#1e40af"][stroke],
body.reg-classic .pm-svg rect[fill="#1e40af"]:not([stroke]) {
  fill: var(--o-paper-hi);
  stroke: var(--o-navy);
  stroke-width: 2;
}
body.reg-classic .pm-svg rect[fill="#6d28d9"][stroke],
body.reg-classic .pm-svg rect[fill="#6d28d9"]:not([stroke]) {
  fill: var(--o-plate);
  stroke: var(--o-navy);
  stroke-width: 2;
}
body.reg-classic .pm-svg rect[fill="#15803d"][stroke],
body.reg-classic .pm-svg rect[fill="#15803d"]:not([stroke]),
body.reg-classic .pm-svg rect[fill="#ca8a04"][stroke],
body.reg-classic .pm-svg rect[fill="#c2410c"][stroke] {
  fill: var(--o-plate);
  stroke: var(--o-navy);
  stroke-width: 3;
}
body.reg-classic .pm-svg rect[fill="#5b21b6"][stroke] {
  fill: var(--o-plate);
  stroke: var(--o-oxblood);
  stroke-width: 3;
}
body.reg-classic .pm-svg rect[fill="#b45309"][stroke],
body.reg-classic .pm-svg rect[fill="#b45309"]:not([stroke]) {
  fill: var(--o-plate);
  stroke: var(--accent);
  stroke-width: 2.5;
}

/* ── The wide bands behind each level's cards ────────────────────────────
   Ten near-white cool tints. On cream they showed as pale blue-grey patches,
   so they all take the paper and are separated by a hairline instead — the
   ground stays one sheet and the chips carry the rank. `#eff6ff` and
   `#f5f3ff` are each used twice at different strokes, hence the pairing.

   The rule is navy-soft, not `--o-rule`: the tan rule is the register's own
   hairline and it looked right, but measured against the paper it comes in at
   1.98:1, and a band edge a thousand pixels long that fades out on a dim
   screen is exactly the kind of "it looks period" this cannot settle for.
   Navy-soft is 7.5:1 and is the same ink the cards inside use — no ambiguity,
   because one of these rectangles is 1030px wide and the other is 95px. */
body.reg-classic .pm-svg rect[fill="#f0f9ff"],
body.reg-classic .pm-svg rect[fill="#ecfeff"],
body.reg-classic .pm-svg rect[fill="#eff6ff"][stroke="#bfdbfe"],
body.reg-classic .pm-svg rect[fill="#f5f3ff"][stroke="#ddd6fe"],
body.reg-classic .pm-svg rect[fill="#f0fdf4"],
body.reg-classic .pm-svg rect[fill="#fefce8"],
body.reg-classic .pm-svg rect[fill="#fff7ed"],
body.reg-classic .pm-svg rect[fill="#faf5ff"],
body.reg-classic .pm-svg rect[fill="#f1f5f9"] {
  fill: var(--o-paper);
  stroke: var(--o-navy-soft);
  stroke-width: 1;
}
/* The purple super-track banner is a note, not a band: navy hairline. */
body.reg-classic .pm-svg rect[fill="#f5f3ff"][stroke="#c4b5fd"] {
  fill: var(--o-paper);
  stroke: var(--o-navy);
  stroke-width: 1.5;
}
/* L8–L10: the conferred-honour rows, and the ×10 / repeats-yearly marks that
   belong to them. Gold frame on the tinted plate — the metal, used once. */
body.reg-classic .pm-svg rect[fill="#fffbeb"],
body.reg-classic .pm-svg rect[fill="#fef3c7"],
body.reg-classic .pm-svg circle[fill="#fef3c7"] {
  fill: var(--o-plate);
  stroke: var(--accent);
  stroke-width: 1.5;
}

/* ── Programme cards ────────────────────────────────────────────────────
   The white cards become the printed leaf a panel sits on (§1's --o-paper-hi)
   inside a hairline. The two security disciplines keep their identity in ink
   rather than in hue: navy for whitehat, oxblood for blackhat. */
body.reg-classic .pm-svg rect[fill="#fff"] {
  fill: var(--o-paper-hi);
  stroke: var(--o-navy-soft);
  stroke-width: 0.9;
}
body.reg-classic .pm-svg rect[fill="#eff6ff"][stroke="#2563eb"] {
  fill: var(--o-paper-hi);
  stroke: var(--o-navy);
  stroke-width: 1.6;
}
body.reg-classic .pm-svg rect[fill="#fef2f2"][stroke="#ef4444"] {
  fill: var(--o-paper-hi);
  stroke: var(--o-oxblood);
  stroke-width: 1.6;
}

/* ── The off-spine column ───────────────────────────────────────────────
   A 175×1515 near-black panel holding four slate cards, plus the colophon
   badge in the legend strip — the largest of the punched holes. All become
   paper inside a frame. The two cards the diagram already drew dashed keep
   their `stroke-dasharray` attribute (nothing below sets it), because dashed
   means "off the ladder" and that reading survives the re-inking; Discipulus
   Maximus keeps its gold, which now means the same thing as L8–L10's. */
body.reg-classic .pm-svg rect[fill="#0f172a"] {
  fill: var(--o-paper);
  stroke: var(--o-navy);
  stroke-width: 1;
}
body.reg-classic .pm-svg rect[fill="#1e293b"]:not([stroke]),
body.reg-classic .pm-svg rect[fill="#1e293b"][stroke="#475569"] {
  fill: var(--o-paper-hi);
  stroke: var(--o-navy-soft);
  stroke-width: 1;
}
body.reg-classic .pm-svg rect[fill="#1e293b"][stroke="#f59e0b"] {
  fill: var(--o-plate);
  stroke: var(--accent);
  stroke-width: 1.2;
}
body.reg-classic .pm-svg line[stroke="#334155"] { stroke: var(--o-navy-soft); }

/* ── Numbered roundels ──────────────────────────────────────────────────
   These stay solid, because a small filled disc with a reversed numeral is an
   ornament a press could strike, not a hole: it is the only place the register
   wants ink covering the sheet. Their hue was redundant with the band they sit
   in, except for blackhat's red, which becomes the second ink. */
body.reg-classic .pm-svg circle[fill="#0e7490"],
body.reg-classic .pm-svg circle[fill="#1e40af"],
body.reg-classic .pm-svg circle[fill="#6d28d9"] { fill: var(--o-navy); }
body.reg-classic .pm-svg circle[fill="#dc2626"] { fill: var(--o-oxblood); }

/* ── Type ───────────────────────────────────────────────────────────────
   A catch-all first, then the exceptions. Forty-odd distinct text fills are in
   the file and more than half of them are pale tints chosen to sit on the
   darks that are now paper; enumerating them would leave whichever one was
   missed as white-on-cream. So everything inks navy, and only the marks with
   a reason to differ are named. */
body.reg-classic .pm-svg text { fill: var(--o-navy); }
/* Secondary figures — unit counts, notes, the muted legend lines. */
body.reg-classic .pm-svg text[fill="#64748b"],
body.reg-classic .pm-svg text[fill="#94a3b8"],
body.reg-classic .pm-svg text[fill="#334155"],
body.reg-classic .pm-svg text[fill="#a16207"],
body.reg-classic .pm-svg text[fill="#0284c7"],
body.reg-classic .pm-svg text[fill="#0ea5e9"] { fill: var(--o-navy-soft); }
/* The second ink: offensive security, and conferred-honour emphasis. */
body.reg-classic .pm-svg text[fill="#991b1b"],
body.reg-classic .pm-svg text[fill="#b91c1c"],
body.reg-classic .pm-svg text[fill="#ef4444"],
body.reg-classic .pm-svg text[fill="#f43f5e"],
body.reg-classic .pm-svg text[fill="#f472b6"],
body.reg-classic .pm-svg text[fill="#92400e"],
body.reg-classic .pm-svg text[fill="#b45309"],
body.reg-classic .pm-svg text[fill="#fef3c7"],
body.reg-classic .pm-svg text[fill="#fde68a"],
body.reg-classic .pm-svg text[fill="#fbbf24"],
body.reg-classic .pm-svg text[fill="#f59e0b"] { fill: var(--o-oxblood); }
/* Reversed out of the roundels — the numeral immediately follows its disc. */
body.reg-classic .pm-svg circle[fill="#0e7490"] + text,
body.reg-classic .pm-svg circle[fill="#1e40af"] + text,
body.reg-classic .pm-svg circle[fill="#6d28d9"] + text,
body.reg-classic .pm-svg circle[fill="#dc2626"] + text { fill: var(--o-paper); }
/* Defined but unreferenced in the file — no element carries `marker-end`.
   Inked anyway so that wiring one up later cannot reintroduce a modern blue. */
body.reg-classic .pm-svg marker path { fill: var(--o-navy); }

/* ── The five loose ornaments ───────────────────────────────────────────
   Colour emoji are the one mark in the diagram that no `fill` can re-ink: the
   font paints them, and two medals, a bank, a brain and a stopwatch floating in
   full colour over cream are the loudest thing left on the sheet once the darks
   are gone. These five are the only glyphs in the file that carry no words —
   pure decoration flanking the masthead and idling in the L1 chip — so the
   register stands them down. Every emoji that is part of a label (🎓 Bachelor's
   Degree, 🏆 TOP LAUREATE, 📡 SIGNALS, Δ, 👑, ⌨) stays, because hiding those
   would delete content, which §"anti-goals" forbids: the register changes how
   the university looks, never what it says. Keyed on font-size because that is
   what actually separates them — 28 and 20 are unique to the ornaments, and 22
   is shared only with the masthead line, which carries `font-weight`. */
body.reg-classic .pm-svg text[font-size="28"],
body.reg-classic .pm-svg text[font-size="20"],
body.reg-classic .pm-svg text[font-size="22"]:not([font-weight]) { display: none; }

/* ── The HTML legend under the figure ───────────────────────────────────
   Three swatches that must state the same three treatments as the diagram:
   base is the plain leaf, doctoral is the tinted plate, apex is the tinted
   plate under the second ink. */
body.reg-classic .pm-swatch { border-radius: 0; }
/* The level swatches keep the map's own fixed palette on classic paper —
   the SVG artwork does not re-skin, so a re-tinted key would lie about it. */
