/* Lynx Cortex — portal stylesheet.
   Themes (dark/light) are declared via CSS custom properties on :root in
   base.html. This file is layout + components only.
   Mobile-first; all layout collapses gracefully on narrow viewports. */

:root {
  --radius: 8px;
  --radius-lg: 14px;
  --radius-pill: 999px;
  --space-xs: 4px;
  --space-sm: 8px;
  --space: 14px;
  --space-md: 20px;
  --space-lg: 28px;
  --space-xl: 44px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.18);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.22), 0 1px 2px rgba(0, 0, 0, 0.18);
  --shadow-lg: 0 14px 36px rgba(0, 0, 0, 0.28), 0 2px 4px rgba(0, 0, 0, 0.22);
  --ring: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
  --font-body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    sans-serif;
  --font-mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo,
    Consolas, monospace;
  --transition: 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
  /* Height of a ONE-ROW sticky top nav. Kept for reference/anchoring offsets;
     note the desktop nav is now a fixed TWO rows (brandline + menu row), so
     nothing may assume this is the nav's real rendered height (the old sticky
     table headers did, and slid under the nav — they are static now). */
  --nav-h: 49px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 15.5px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  background:
    radial-gradient(1100px 600px at 12% -10%,
      color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%),
    radial-gradient(900px 500px at 100% 0%,
      color-mix(in srgb, var(--accent-strong) 10%, transparent), transparent 55%),
    var(--bg);
  background-attachment: fixed;
}

code, pre, kbd, samp { font-family: var(--font-mono); }
pre {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space);
  overflow-x: auto;
  font-size: 0.9em;
}
code:not(pre code) {
  background: color-mix(in srgb, var(--bg-elev) 80%, transparent);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 0.88em;
}
/* Invite / set-password links: long signed tokens must wrap cleanly inside
   their box and never overflow, clip, or push past the card. A full-width
   block that breaks the unbroken token anywhere reads as an intentional frame. */
code.invite-link {
  display: block;
  max-width: 100%;
  margin: 4px 0;
  padding: 10px 12px;
  line-height: 1.6;
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-all;
}
a[data-invite-url] {
  display: block;
  max-width: 100%;
  text-decoration: none;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-sm);
}
h1 { font-size: 1.85rem; font-weight: 700; }
h2 { font-size: 1.35rem; font-weight: 650; }
h3 { font-size: 1.1rem;  font-weight: 600; }

.container {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 var(--space);
}

main.container {
  padding-top: var(--space-lg);
  padding-bottom: var(--space-xl);
}

footer.container {
  border-top: 1px solid var(--border);
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  padding-bottom: var(--space-md);
  color: var(--muted);
  font-size: 0.85em;
}

a {
  color: var(--accent);
  text-decoration: none;
  transition: color var(--transition);
}
a:hover { color: var(--accent-strong); text-decoration: underline; }
a:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 3px; }

.muted { color: var(--muted); }
.small { font-size: 0.85em; }
.danger { color: var(--danger); }
.ok { color: var(--ok); }
.mono { font-family: var(--font-mono); }

/* ── Top nav ────────────────────────────────────────────────────────── */

.topnav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg-elev) 88%, transparent);
  backdrop-filter: saturate(140%) blur(10px);
  -webkit-backdrop-filter: saturate(140%) blur(10px);
  border-bottom: 1px solid var(--border);
}
/* The header gets a wider canvas than page prose: with the menu sharing the
   brand row (centered) the full admin link set needs the real viewport width
   to sit on ONE line (the content container stays 1080px). */
.topnav .container { max-width: 1600px; }

/* ONE header row, three zones on a single flex line:
   [ brand · natural width ] [ menu · flex:1, centered ] [ controls · natural ].
   Wraps only as a last-resort safety valve on very narrow screens (the menu
   is display:none on mobile, so the row is just brand + controls there). */
.topnav-brandline {
  display: flex;
  align-items: center;
  gap: var(--space);
  padding: 7px 0;
  flex-wrap: wrap;
}
.topnav .brand { flex: none; }
.topnav .nav-controls { flex: none; }

/* Center zone — the menu. flex:1 + justify-content:center keeps the links
   optically centered between brand and controls; min-width:0 lets the zone
   shrink (rather than wrap the row) so brand/controls always hold their line;
   the .nav-primary inside pans within THIS zone when the link set is wider
   than the space (the page body never h-scrolls). */
.nav-links {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
}
.nav-primary,
.nav-controls {
  display: flex;
  align-items: center;
  gap: 2px;
}
/* Single-row guarantee for the admin link set: never wrap. If a viewport is
   narrower than the full set (1024–1150px, before the hamburger takes over),
   the bar pans INSIDE itself — the page never scrolls horizontally and no
   link ever drops to an orphan second line. */
.nav-primary {
  flex-wrap: nowrap;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.nav-primary::-webkit-scrollbar { display: none; }

/* Nav dropdown groups (Study/Programs/Community/Account/Admin ▾): the flat
   link list collapses into a few role-aware dropdowns so the top-level row
   stays short — nothing ever scrolls out of the overflow zone. The admin
   elements carry both the shared .nav-dd* classes and their legacy
   .nav-admin-* names; the shared classes style them all. */
.nav-dd {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
}
.nav-dd-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  border: none;
  background: none;
  font: inherit;
}
.nav-caret {
  transition: transform var(--transition);
  opacity: 0.8;
}
.nav-dd.open .nav-caret { transform: rotate(180deg); }
/* The menu: an absolutely-positioned card that opens under the toggle. Hidden
   until .open (toggled by portal.js); native fallback re-shows it on mobile. */
.nav-dd-menu {
  display: none;
  /* fixed (not absolute) so it escapes the .nav-primary overflow-x clip;
     portal.js positions it under the toggle on open. */
  position: fixed;
  top: 0;
  right: 0;
  min-width: 200px;
  flex-direction: column;
  gap: 1px;
  padding: 6px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px -8px rgba(0, 0, 0, 0.45);
  z-index: 60;
}
.nav-dd.open .nav-dd-menu { display: flex; }
.nav-dd-menu a {
  display: block;
  width: 100%;
  padding: 8px 10px;
  font-size: 0.86em;
}
.nav-dd-menu a::after { display: none; }
.nav-controls { gap: var(--space-sm); }

/* "Not applicable" card for non-student staff surfaces (marks/progress/etc.). */
.na-card { text-align: center; }
.na-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-sm);
  border-radius: 50%;
  border: 2px solid var(--border);
  color: var(--muted);
  font-size: 1.6rem;
  line-height: 1;
}

/* Profile: contact fieldset, verified badges, programs list. */
.profile-contact {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space);
  margin: var(--space-sm) 0;
}
.profile-contact legend {
  padding: 0 6px;
  font-weight: 600;
  color: var(--muted);
}
.verify-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 8px;
  border-radius: 20px;
  font-size: 0.72em;
  font-weight: 600;
  vertical-align: middle;
}
.verify-badge.is-verified {
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 15%, transparent);
}
.verify-badge.is-unverified {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
}
.verify-link { margin-left: 6px; font-size: 0.78em; }
.program-list { list-style: none; padding: 0; margin: 0; }
.program-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.program-list li:last-child { border-bottom: none; }
.program-badge {
  padding: 2px 10px;
  border-radius: 20px;
  font-size: 0.74em;
  font-weight: 600;
  white-space: nowrap;
}
.program-badge.is-enrolled {
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 15%, transparent);
}
.program-badge.is-pending {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, transparent);
}

/* Professor console roster — a clean, well-aligned learner table (item C). */
.prof-roster {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.prof-roster th,
.prof-roster td {
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: middle;
}
.prof-roster thead th {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
}
.prof-roster tbody tr:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.prof-roster .col-num { text-align: right; white-space: nowrap; }
.prof-roster .col-actions { text-align: right; white-space: nowrap; }
.prof-roster-name { font-weight: 600; }
.prof-roster tbody tr:last-child td { border-bottom: none; }

/* Owner & Admins account table — roomier columns + clean alignment (item E). */
.accounts-table { width: 100%; border-collapse: collapse; }
.accounts-table th,
.accounts-table td {
  padding: 0.6rem 1.1rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: middle;
}
.accounts-table thead th {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
}
.accounts-table tbody tr:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.accounts-table .inline-form { display: inline-block; margin-left: 0.4rem; }

/* Accounts ACTIONS column — a tidy vertical stack of equal-width controls that
   stays contained in its cell instead of a ragged overflow that collided with
   the floating chat bubble (Borja 2026-07-20). */
.accounts-actions-cell { vertical-align: top; }
.acct-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.4rem;
  min-width: 11rem;
  max-width: 15rem;
}
.acct-actions > a,
.acct-actions > form,
.acct-actions > .badge { margin: 0; }
/* Reset the inline-form default (inline-block + left margin) inside the stack. */
.acct-actions .inline-form { display: block; margin-left: 0; }
.acct-actions .acct-inspect { align-self: flex-start; padding: 2px 0; }
.acct-actions > .badge { align-self: flex-start; }
/* Equal-width action buttons, centered labels. */
.acct-actions form > button,
.acct-actions .acct-setpw > summary {
  width: 100%;
  box-sizing: border-box;
  text-align: center;
}
/* Set / reset password disclosure: summary looks like a button; the inline
   form stacks its input + submit fully inside the column. */
.acct-setpw { width: 100%; }
.acct-setpw > summary {
  cursor: pointer;
  list-style-position: inside;
  padding: 4px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  font-size: 0.85em;
  font-weight: 600;
  user-select: none;
}
.acct-setpw[open] > summary { margin-bottom: 0.4rem; }
.acct-setpw .inline-form {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0;
}
.acct-setpw .inline-form input[type="password"] { width: 100%; box-sizing: border-box; }

.admin-subhead { margin-top: var(--space); }

.topnav .brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--fg);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}
.topnav .brand:hover { text-decoration: none; color: var(--fg); }
.brand-mark {
  width: 22px;
  height: 22px;
  color: var(--accent);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 45%, transparent));
}
.brand-text-accent {
  color: var(--accent);
  margin-left: 2px;
}

/* Brand crest (nav only — the 24-grid marks elsewhere keep the base
   .brand-mark sizing). The artwork itself, and its per-ground cut, live in
   §"Owner-supplied heraldic marks" at the end of this file; what stays here is
   the slot. The sheen animation that swept the old drawn shield went with it —
   a raster mark has no element to sweep, and faking one over owner artwork
   would be decoration applied to someone else's drawing. */
.brand-crest {
  width: 27px;
  height: 27px;
  filter: drop-shadow(0 0 6px color-mix(in srgb, #d8b24a 50%, transparent));
}

/* Primary destination links: quiet by default, with an animated underline that
   grows on hover and locks in on the active section. Compact metrics so the
   FULL admin set (18 links) holds one row at common desktop widths. */
.nav-link {
  position: relative;
  color: var(--muted);
  padding: 6px 8px;
  border-radius: var(--radius);
  font-size: 0.8em;
  font-weight: 500;
  white-space: nowrap;
  flex: none;
  transition: color var(--transition), background var(--transition);
}
.nav-link::after {
  content: "";
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: center;
  opacity: 0;
  transition: transform var(--transition), opacity var(--transition);
}
.nav-link:hover {
  color: var(--fg);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  text-decoration: none;
}
.nav-link:hover::after { transform: scaleX(0.65); opacity: 0.5; }
.nav-link.is-active { color: var(--fg); }
.nav-link.is-active::after { transform: scaleX(1); opacity: 1; }

/* Identity chip — reads as a label, not a link. */
.nav-identity {
  color: var(--muted);
  font-size: 0.86em;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--fg) 5%, transparent);
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Theme button + locale link share one compact control pill. */
.nav-control {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.76em;
  letter-spacing: 0.03em;
  line-height: 1;
  transition: color var(--transition), border-color var(--transition),
    background var(--transition);
}
.nav-control:hover {
  color: var(--fg);
  border-color: color-mix(in srgb, var(--fg) 30%, var(--border));
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  text-decoration: none;
}
.nav-control-icon { flex: none; }
a.nav-control { color: var(--muted); }
a.nav-control:hover { color: var(--fg); }

/* Notification bell: a compact icon link with an unread count badge. */
.nav-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  color: var(--muted);
  transition: color var(--transition), background var(--transition);
}
.nav-bell:hover,
.nav-bell.is-active { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); text-decoration: none; }
.nav-bell-icon { flex: none; }
.notif-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--danger);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  line-height: 16px;
  text-align: center;
  font-weight: 600;
}

/* Accessible-only text (visually hidden, read by assistive tech). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Visually-hidden until focused (WCAG 2.4.1 skip link). On keyboard focus the
   element becomes visible and interactive. */
.sr-only-focusable:focus,
.sr-only-focusable:focus-within {
  position: fixed;
  top: 0.5rem;
  left: 0.5rem;
  width: auto;
  height: auto;
  padding: 0.6rem 1rem;
  margin: 0;
  overflow: visible;
  clip: auto;
  z-index: 1000;
}
.skip-link:focus {
  background: var(--bg-elev, #161b22);
  color: var(--fg, #e6e6e6);
  border: 2px solid var(--accent, #58a6ff);
  border-radius: 6px;
  text-decoration: none;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}

/* Notification feed (/me/notifications). */
.notif-feed { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.notif-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
}
.notif-item.is-unread { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.notif-item.is-unread::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--accent);
}
.notif-item.is-read::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: 50%;
  background: transparent;
}
.notif-body { flex: 1 1 auto; min-width: 0; }
.notif-title { margin: 0; font-size: 0.95rem; font-weight: 600; }
.notif-text { margin: 2px 0 0; color: var(--muted); font-size: 0.85rem; }
.notif-type {
  display: inline-block;
  margin-top: 4px;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.03em;
}
.notif-item form { flex: none; }

/* Logout lives inside a POST form so the request is CSRF-protected; style
   the form as a layout no-op and the button to read like the other nav
   links rather than a hard-bordered button. */
.nav-logout {
  display: inline-flex;
  margin: 0;
  padding: 0;
}
.nav-logout button.link {
  background: transparent;
  border: 0;
  color: var(--muted);
  padding: 6px 10px;
  font: inherit;
  font-size: 0.93em;
  cursor: pointer;
  border-radius: var(--radius);
}
.nav-logout button.link:hover {
  color: var(--fg);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
}

/* ── Cards ──────────────────────────────────────────────────────────── */

.card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  margin-bottom: var(--space-md);
  box-shadow: var(--shadow-sm);
}
.card.narrow {
  max-width: 440px;
  margin-left: auto;
  margin-right: auto;
}
.card.elevated { box-shadow: var(--shadow-md); }
.card h1 { margin-top: 0; }
.card h1 + p.muted { margin-top: -4px; margin-bottom: var(--space); }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space);
}

/* ── Login hero ─────────────────────────────────────────────────────── */

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-xl) var(--space) var(--space-md);
}
.hero .brand-mark {
  width: 52px;
  height: 52px;
  filter: drop-shadow(0 0 18px color-mix(in srgb, var(--accent) 55%, transparent));
}
.hero h1 {
  font-size: 2.2rem;
  margin: 0;
  text-align: center;
  background: linear-gradient(135deg, var(--fg), color-mix(in srgb, var(--accent) 80%, var(--fg)));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hero .tagline {
  color: var(--muted);
  margin: 0;
  font-size: 1rem;
  text-align: center;
  max-width: 36ch;
}

/* ── Forms ──────────────────────────────────────────────────────────── */

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space);
}
.row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space);
  flex-wrap: wrap;
}

label > span {
  display: block;
  color: var(--muted);
  font-size: 0.82em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 6px;
}

input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="url"],
input[type="search"],
input[type="tel"],
select,
textarea {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: var(--radius);
  padding: 10px 12px;
  font: inherit;
  transition: border-color var(--transition), box-shadow var(--transition),
    background var(--transition);
}
input:hover, select:hover, textarea:hover {
  border-color: color-mix(in srgb, var(--fg) 25%, var(--border));
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}
textarea { font-family: var(--font-mono); min-height: 5em; resize: vertical; }

/* Native file pickers: theme the field + its "Browse" chip (the raw control
   rendered as a white UA widget inside dark cards). color-scheme in base.html
   keeps the OS dialog on-theme; this styles the in-page part. */
input[type="file"] {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: var(--radius);
  padding: 7px 12px;
  font: inherit;
  cursor: pointer;
}
input[type="file"]::file-selector-button {
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: var(--radius);
  padding: 4px 12px;
  margin-right: 10px;
  font: inherit;
  font-size: 0.9em;
  cursor: pointer;
}

button {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: var(--radius);
  padding: 9px 16px;
  cursor: pointer;
  font: inherit;
  font-weight: 500;
  transition: background var(--transition), border-color var(--transition),
    transform var(--transition), box-shadow var(--transition);
}
button:hover {
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  border-color: color-mix(in srgb, var(--fg) 30%, var(--border));
}
button:active { transform: translateY(1px); }
button:focus-visible { outline: none; box-shadow: var(--ring); }

button.primary {
  background: linear-gradient(135deg, var(--accent), var(--accent-strong));
  border-color: var(--accent-strong);
  color: #fff;
  font-weight: 600;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 30%, transparent);
}
button.primary:hover {
  filter: brightness(1.08);
  box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Anchor-as-button: several admin surfaces use a plain <a> for an action
   that must render identically to button.primary (a <button> element gets
   the box for free from the bare `button` rule above; an <a> does not).
   .btn-clear is the admin student-search "Clear" link (polish A1); .primary
   generalises the same look so any <a> can match a sibling <button.primary>
   (e.g. bookkeeping year-end downloads, polish D6). */
a.btn-clear,
a.primary {
  display: inline-block;
  background: linear-gradient(135deg, var(--accent), var(--accent-strong));
  border: 1px solid var(--accent-strong);
  color: #fff;
  border-radius: var(--radius);
  padding: 9px 16px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 30%, transparent);
  transition: background var(--transition), border-color var(--transition),
    transform var(--transition), box-shadow var(--transition), filter var(--transition);
}
a.btn-clear:hover,
a.primary:hover {
  filter: brightness(1.08);
  box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 45%, transparent);
  text-decoration: none;
}
a.btn-clear:active,
a.primary:active { transform: translateY(1px); }
a.btn-clear:focus-visible,
a.primary:focus-visible { outline: none; box-shadow: var(--ring); }

/* ── Filter/action toolbars (G1: one control-sizing contract) ───────────
   A recurring defect across admin surfaces: filter inputs/selects and their
   Filter/Clear (or export) buttons drifted to different heights, paddings
   and — for plain `<a class="muted">` "Clear" links — no box at all, so
   they read as different widgets. Root cause was two competing box models:
   inputs/selects/button all inherit body line-height (1.6) + 10px/9px
   padding, while `a.btn-clear`/`a.primary` hard-code line-height:1.2 — same
   padding, visibly shorter box. `.filter-bar` fixes every child to one
   explicit height instead of relying on font metrics, so the mismatch can't
   recur. Apply this ONE class to a toolbar `<form>` (or a plain button row
   like the finance export actions) rather than hand-tuning per page.
   Used by: admin overview student search, admin/logs (with .is-compact),
   admin/audit-log, finance year-end (fiscal-year picker + export actions),
   labs/teacher filter. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin: 10px 0 4px;
}
.filter-bar label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
}
.filter-bar label > span { margin-bottom: 0; }
.filter-bar form { margin: 0; }
.filter-bar input,
.filter-bar select,
.filter-bar button,
.filter-bar a.btn-clear,
.filter-bar a.primary {
  height: 42px;
  box-sizing: border-box;
  margin: 0;
}
.filter-bar input,
.filter-bar select {
  padding: 0 12px;
}
.filter-bar button,
.filter-bar a.btn-clear,
.filter-bar a.primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 16px;
  line-height: 1.2;
  white-space: nowrap;
}

/* Compact modifier: admin/logs packs six fields + Filter/Clear above a world
   map and needs the whole row on one line — same alignment contract, a
   smaller footprint (was a page-local `.lg-toolbar` block; centralised here
   so the sizing rule stays singular). */
.filter-bar.is-compact { gap: 7px 9px; }
.filter-bar.is-compact label { gap: 2px; }
.filter-bar.is-compact label > span { font-size: 0.82em; }
.filter-bar.is-compact input,
.filter-bar.is-compact select {
  height: 34px;
  padding: 0 7px;
  font-size: 0.82em;
}
.filter-bar.is-compact input[type="text"] { width: 8rem; }
.filter-bar.is-compact input[type="date"] { width: 8.2rem; }
.filter-bar.is-compact select { max-width: 8.5rem; }
.filter-bar.is-compact button,
.filter-bar.is-compact a.btn-clear,
.filter-bar.is-compact a.primary {
  height: 34px;
  padding: 0 12px;
  font-size: 0.82em;
}

button.danger,
/* `.btn-danger` had NO rule anywhere while three templates used it, including
   the snapshot console's "Restore (destructive)" — the single most dangerous
   control in the app — and every OFF state on the ownership console's kill
   switches. The armed state rendered as a browser-default grey button while the
   safe state was styled, which reads as the exact opposite of the truth. */
.btn-danger {
  border-color: var(--danger);
  color: var(--danger);
}
button.danger:hover,
.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* Accept (spec §8): a solid green button, same shape/size as the reject button. */
button.btn-accept {
  background: var(--ok);
  border-color: var(--ok);
  color: #fff;
  font-weight: 600;
}
button.btn-accept:hover {
  filter: brightness(1.08);
  background: var(--ok);
  border-color: var(--ok);
}

/* Inline account enable/disable toggle (spec §9): the label shows the current
   state; green = enabled, red = disabled. Clicking flips it in place. */
button.acct-toggle { padding: 4px 12px; font-size: 0.85em; font-weight: 600; }
button.acct-toggle-on {
  border-color: var(--ok);
  color: var(--ok);
}
button.acct-toggle-on:hover {
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border-color: var(--ok);
}
/* The off-state hook the template has always applied and no rule ever matched.
   It only looked different because `danger` was applied alongside it. */
button.acct-toggle-off {
  border-color: var(--danger);
  color: var(--danger);
}
button.acct-toggle-off:hover {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-color: var(--danger);
}

/* Applications review — aligned Accept / Reject / Reason controls (spec §8). */
.apps-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  /* nowrap: Reason must sit on the SAME row next to Reject, never dropping to a
     second line (Borja 2026-07-21). The table is in a `.table-scroll` wrapper,
     so a narrow viewport scrolls horizontally rather than wrapping the row. */
  flex-wrap: nowrap;
}
.apps-actions form { margin: 0; flex: 0 0 auto; }
/* Compact, tidy action buttons — smaller than the page default (9px 16px) so
   the four controls read as one neat row (Borja 2026-07-21). */
.apps-actions button {
  margin: 0;
  padding: 5px 11px;
  font-size: 0.82rem;
  font-weight: 600;
  white-space: nowrap;
}
/* Equal-width, aligned action controls (Accept / +Request / Reject / Reason)
   so they read as one tidy row instead of a ragged stack (Borja 2026-07-20). */
.apps-actions > .btn,
.apps-actions > form > .btn { min-width: 4.75rem; text-align: center; }
.apps-actions > form > .btn { width: 100%; }

/* Applications table — a touch smaller and tighter than the page default, with
   clean bottom rules and aligned cells (Borja 2026-07-21). */
.apps-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.apps-table th {
  text-align: left;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.apps-table td {
  padding: 7px 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.apps-table tbody tr:last-child td { border-bottom: none; }
.apps-table tbody tr:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }

/* Request-more-information (spec §8b): a yellow button in the same family,
   positioned between Accept (green) and Reject (red). */
button.btn-hold {
  background: var(--warn, #e0a400);
  border-color: var(--warn, #e0a400);
  color: #1a1400;
  font-weight: 600;
}
button.btn-hold:hover {
  filter: brightness(1.08);
  background: var(--warn, #e0a400);
  border-color: var(--warn, #e0a400);
}
/* On-hold status chip on an awaiting-info application row. */
.apps-chip {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
  vertical-align: middle;
}
.apps-chip-hold {
  background: color-mix(in srgb, var(--warn, #e0a400) 22%, transparent);
  color: var(--warn, #e0a400);
  border: 1px solid var(--warn, #e0a400);
}
/* Light-theme contrast lift (a11y 1.4.3): the amber #e0a400 chip text is only
   ~2.2:1 on the pale-amber ground in the light theme — darken just the text
   (the dark theme keeps the readable #e0a400). */
:root[data-theme="light"] .apps-chip-hold { color: #7a5c00; }

/* Decision-note dialog. */
.apps-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card, var(--bg));
  color: var(--fg);
  padding: 0;
  width: min(560px, calc(100vw - 32px));
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.apps-dialog::backdrop { background: rgba(0, 0, 0, 0.5); }
.apps-dialog-form { padding: 18px 20px; display: flex; flex-direction: column; gap: 12px; }
.apps-dialog-form h3 { margin: 0; }
.apps-dialog-form textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  font: inherit;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
}
.apps-email-opt { display: flex; align-items: center; gap: 8px; font-size: 0.92em; }
.apps-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin: 0;
  padding: 0;
}
.apps-dialog-actions button { margin: 0; }

/* Shared enquiry dialog + inline form (CATALOGUE_LIFECYCLE_ENQUIRY §4/§6). */
.enquiry-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card, var(--bg));
  color: var(--fg);
  padding: 0;
  width: min(560px, calc(100vw - 32px));
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.enquiry-dialog::backdrop { background: rgba(0, 0, 0, 0.5); }
.enquiry-form { display: flex; flex-direction: column; gap: 12px; }
.enquiry-dialog .enquiry-form { padding: 18px 20px; }
.enquiry-form h2 { margin: 0 0 4px; font-size: 1.15rem; }
.enquiry-field { display: flex; flex-direction: column; gap: 4px; font-size: 0.92em; }
.enquiry-field > span { font-weight: 600; }
.enquiry-field input,
.enquiry-field select,
.enquiry-field textarea {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
}
.enquiry-field textarea { resize: vertical; }
.enquiry-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin: 4px 0 0;
  padding: 0;
}
.enquiry-actions button { margin: 0; }
/* Read-only identity references on the profile (User ID + Token). Copyable. */
.id-ref { margin: 10px 0; }
.id-ref-label { display: block; font-weight: 600; font-size: 0.9em; margin-bottom: 4px; }
.id-ref-row { display: flex; gap: 8px; align-items: stretch; }
.id-ref-input {
  flex: 1 1 auto;
  min-width: 0;
  box-sizing: border-box;
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  color: var(--fg);
}
.id-ref-copy { flex: 0 0 auto; white-space: nowrap; }

.enquiry-form-inline { max-width: 42rem; }
/* About "Contact us": the inline enquiry form must span the FULL content width
   of the About column so its email/subject/message boxes line up with the other
   content boxes on the page (e.g. the License rectangle) — Borja 2026-07-19. */
.about-contactus .enquiry-form-inline { max-width: none; }
/* Red rejection dialog (NDA toggle attempted by a non-owner admin). */
.nda-reject-dialog { border-color: var(--danger, #d9534f); }
.nda-reject-dialog h2 { color: var(--danger, #d9534f); }
.nda-reject-dialog::backdrop { background: rgba(120, 0, 0, 0.35); }

/* Quiet inline action: a submit button that reads as a link, not a chunky
   button (e.g. partner "Set tier" / "Record"). The bordered nav-logout variant
   keeps its own muted styling via the more-specific `.nav-logout button.link`. */
button.link {
  background: transparent;
  border: 0;
  color: var(--accent);
  padding: 6px 8px;
  font-weight: 500;
  box-shadow: none;
}
button.link:hover {
  background: transparent;
  border: 0;
  color: var(--accent-strong);
  text-decoration: underline;
}

/* Inline tier/outcome selects sit beside their submit button in a single-line
   form row; keep them content-width so the button never wraps to a new line
   (overrides the global full-width select rule). */
.tier-select,
.outcome-select {
  width: auto;
  min-width: 7rem;
}

/* ── Flash ──────────────────────────────────────────────────────────── */

.flash {
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: var(--space);
  background: var(--bg-elev);
  font-size: 0.95em;
}
.flash-info    { border-left-color: var(--accent); }
.flash-success { border-left-color: var(--ok);     color: var(--ok); }
.flash-error   { border-left-color: var(--danger); color: var(--danger); }
/* Used by ownership/console.html's staff-MFA panel since it shipped; there was
   no rule for it, so the one caution state on that console rendered neutral. */
.flash-warning { border-left-color: var(--warn, #b8860b); color: var(--warn, #b8860b); }

/* ── Tables ─────────────────────────────────────────────────────────── */

/* Wide data tables are wrapped in `.table-scroll` so they scroll horizontally
   inside their card on narrow viewports instead of overflowing the page. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.phase-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}
.phase-table th,
.phase-table td {
  border-bottom: 1px solid var(--border);
  padding: 12px 8px;
  text-align: left;
  vertical-align: top;
}
.phase-table th {
  color: var(--muted);
  font-weight: 600;
  font-size: 0.78em;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom-width: 1px;
  background: transparent;
  /* NOT sticky. Two independent failure modes killed stickiness here:
     1. inside a `.table-scroll` wrapper (every wide table) the wrapper is
        itself a scroll container, so `position: sticky; top: var(--nav-h)`
        pinned the header against the WRAPPER's scrollport — displaced
        --nav-h px down at rest, printing over the first data row;
     2. outside a wrapper, `top: var(--nav-h)` assumed a one-row (49px) nav,
        but the logged-in nav wraps to 2–3 rows on most desktop widths, so a
        scrolled header slid UNDER the translucent nav and ghosted through
        its blur. Static headers are correct in both cases. */
  position: static;
}
/* Belt-and-braces: inside a `.table-scroll` wrapper the header must never be
   sticky (see failure mode 1 above). Kept as an explicit override so a future
   re-introduction of sticky headers cannot regress the wrapped tables. */
.table-scroll .phase-table th {
  position: static;
}
.phase-table tbody tr {
  transition: background var(--transition);
}
.phase-table tbody tr:hover {
  background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.phase-table tbody tr:hover a { color: var(--accent-strong); }

/* Dense-row modifier (G2 2026-08-03): shrinks a `.phase-table`'s row height
   and text a touch, and its `.btn-link` CTAs to match, without forking a new
   table component — same "apply a modifier class to the shared component"
   idiom as `.filter-bar.is-compact`. Used by the professor console's "My
   programs" table (teacher/dashboard's `.prof-roster` already covers the
   learner roster; this covers the professor's own program list). */
.phase-table.is-dense th,
.phase-table.is-dense td { padding: 0.55rem 0.7rem; font-size: 0.88rem; }
.phase-table.is-dense th { font-size: 0.72rem; }
.phase-table.is-dense .btn-link { padding: 4px 10px; font-size: 0.85em; }

/* Width-canvas tiers (G2): `.table-wide-canvas` (1400px, partners idiom) is
   the "as wide as it gets" tier; `.table-wide-canvas-sm` is the "widen
   slightly" tier for tables that don't need the full registry canvas. */
main.container:has(.table-wide-canvas-sm) { max-width: 1220px; }

.phase-table td:first-child {
  font-family: var(--font-mono);
  font-size: 0.85em;
  width: 3em;
}

/* Real-name / @login split (G2): a reusable pair for any roster table that
   needs its "Student" column broken into two — the real name stays plain
   body text (own column, sized to its own widest value by normal table
   auto-layout) and the @login sits in its own narrow, muted, mono column
   next to it. First used by the lab-tracker teacher matrix. */
.roster-login {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--muted);
  white-space: nowrap;
}

/* Lab-tracker teacher matrix per-phase completion chips (G2: the table was
   entirely unstyled — a bare `<table class="matrix">` with no rule in this
   sheet at all). Reuses the `.badge` pill proportions at a slightly smaller
   size instead of inventing new chrome. */
.phase-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 8px;
  margin: 2px 4px 2px 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg) 60%, transparent);
  font-size: 0.78em;
  color: var(--muted);
  white-space: nowrap;
}
.phase-chip .blocked {
  color: var(--danger);
  font-style: normal;
  font-weight: 700;
}

/* ── Badges ─────────────────────────────────────────────────────────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 0.75em;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 1px solid var(--border);
  color: var(--muted);
  background: color-mix(in srgb, var(--bg) 60%, transparent);
}
.badge-in_progress {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.badge-done,
.badge-active,
.badge-live {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 55%, var(--border));
  background: color-mix(in srgb, var(--ok) 12%, transparent);
}
/* Four more the class-rule guard found on its first run, all used in templates
   with no selector anywhere: the invite-sent tick, the dashboard's locked-phase
   chip, the chat moderation reject button and the accepted-terms banner. */
.badge-success {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 55%, var(--border));
  background: color-mix(in srgb, var(--ok) 12%, transparent);
}
.badge-locked {
  color: var(--muted);
  border-color: var(--border);
  background: color-mix(in srgb, var(--muted) 10%, transparent);
  opacity: 0.85;
}
/* Fast-track challenge outcomes (BL-034). The pass badge is a literal class in
   dashboard.html, which is how the class-rule guard caught it; index.html builds
   the same names from ``attempt.outcome``, so both outcomes get a rule here.
   Passing a challenge CREDITS a phase without completing it, so this reads as a
   distinct state rather than borrowing .badge-done's "you finished this". */
.badge-fast-track-passed {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.badge-fast-track-failed {
  color: var(--muted);
  border-color: var(--border);
  background: color-mix(in srgb, var(--muted) 10%, transparent);
}
/* A secondary action beside a primary one — visible, but not competing. Used on
   the two chat moderation surfaces for "Reject". */
.btn-ghost {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
}
.btn-ghost:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}
/* The accepted-terms banner hosts a band pill; it needs the row, not a look. */
.band-pill-host {
  display: flex;
  align-items: center;
  gap: var(--space);
  flex-wrap: wrap;
}

/* The prerequisite chips on /content-review used this with no rule behind it,
   so "not started" was indistinguishable from the neutral base badge. */
.badge-not_started {
  color: var(--muted);
  border-color: var(--border);
  background: color-mix(in srgb, var(--muted) 8%, transparent);
}
.badge-pending {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.badge-archived {
  color: var(--muted);
  border-color: var(--border);
  background: color-mix(in srgb, var(--muted) 10%, transparent);
}
/* Blocked / suspended account or enrollment — a warm danger flag so the CEO/
   admin roster surfaces a blocked account at a glance. */
.badge-suspended {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, var(--border));
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* Partner tier hierarchy: gold / silver / bronze. Used by the leaderboard and
   newsletter to rank companies at a glance. */
.badge-t1 {
  color: #5a4708;
  border-color: color-mix(in srgb, #d8b24a 60%, var(--border));
  background: color-mix(in srgb, #d8b24a 26%, transparent);
}
.badge-t2 {
  color: var(--fg);
  border-color: color-mix(in srgb, #b8bcc4 55%, var(--border));
  background: color-mix(in srgb, #b8bcc4 22%, transparent);
}
.badge-t3 {
  color: #6e4a23;
  border-color: color-mix(in srgb, #c08a4a 55%, var(--border));
  background: color-mix(in srgb, #c08a4a 20%, transparent);
}
:root[data-theme="dark"] .badge-t1 { color: #f0d68a; }
:root[data-theme="dark"] .badge-t3 { color: #e0b07a; }

/* ── Notes (review) ─────────────────────────────────────────────────── */

.note-list { list-style: none; padding: 0; }
.note {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space);
  margin-bottom: var(--space);
  background: var(--bg);
  transition: border-color var(--transition);
}
.note:hover { border-color: color-mix(in srgb, var(--fg) 25%, var(--border)); }
.note-body code, .note-body pre {
  background: var(--bg-elev);
  padding: 2px 6px;
  border-radius: 4px;
  /* A note is learner-pasted text, so it carries whatever they pasted: a fenced
     block or a long URL pushed the whole phase page sideways. `.theory-body pre`
     has had this guard (and a comment about it) since the theory renderer
     landed; the notes panel beside it never got one. */
  overflow-x: auto;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* ── Quiz / review ──────────────────────────────────────────────────── */

.quiz-item, .review-card {
  border-top: 1px solid var(--border);
  padding: var(--space) 0;
}
.quiz-item:first-child, .review-card:first-child { border-top: 0; padding-top: 0; }

/* Choice items render their options (migration 113). The fieldset carries no
   border of its own — the .quiz-item rule above already separates items, and a
   second frame reads as a nested card. */
/* The chapter-language note (phase.html). Reads as a quiet aside, not a
   warning: an untranslated chapter is a gap in the corpus, not the learner's
   problem, and the tone should match. */
.chapter-lang-note {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 1px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill, 999px);
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.quiz-choices {
  border: 0;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--space) / 2);
}
.quiz-choice {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--space) / 2);
  padding: calc(var(--space) / 2);
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  cursor: pointer;
}
/* The whole row is the hit target, not just the 13px control — a choice list is
   the one place a learner clicks under time pressure. */
.quiz-choice input { margin-top: 0.2em; flex: 0 0 auto; }
.quiz-choice span { flex: 1 1 auto; }
.quiz-choice:hover { border-color: var(--accent, currentColor); }
.quiz-choice:focus-within { box-shadow: var(--ring); border-color: var(--accent, currentColor); }

/* ── Utilities ──────────────────────────────────────────────────────── */

.text-center { text-align: center; }
.divider {
  height: 1px;
  background: var(--border);
  margin: var(--space) 0;
}
.kbd-hint {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.75em;
  padding: 1px 5px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 4px;
  color: var(--muted);
}

/* ── Page header ────────────────────────────────────────────────────── */

.page-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space);
  flex-wrap: wrap;
  margin: var(--space-sm) 0 var(--space-md);
}
.page-header h1 { margin: 0 0 4px; }
.page-header p { margin: 0; }
/* Course-dashboard title herald: ONE small institutional mark on a serious
   surface (catalogue register) — full-size heraldry lives on profile pages. */
.dash-title-herald { display: block; align-self: center; object-fit: contain; }
.page-header-actions {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}
.btn-link {
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  font-size: 0.9em;
  transition: background var(--transition), border-color var(--transition);
}
.btn-link:hover {
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  border-color: color-mix(in srgb, var(--fg) 30%, var(--border));
  text-decoration: none;
}
.btn-link.muted { color: var(--muted); }

.phase-link {
  font-weight: 600;
  color: var(--fg);
}
.phase-link:hover { color: var(--accent); text-decoration: none; }

.bare-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.bare-list li {
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg) 70%, transparent);
}

/* ── Toasts ─────────────────────────────────────────────────────────── */

.toasts {
  position: fixed;
  top: 70px;
  right: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 200;
  pointer-events: none;
  max-width: min(360px, calc(100vw - 36px));
}
.toast {
  pointer-events: auto;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--bg-elev);
  color: var(--fg);
  font-size: 0.92em;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateX(20px);
  transition: opacity var(--transition), transform var(--transition);
  cursor: pointer;
}
.toast-in  { opacity: 1; transform: translateX(0); }
.toast-out { opacity: 0; transform: translateX(20px); }
.toast-info    { border-left-color: var(--accent); }
.toast-success { border-left-color: var(--ok);     color: var(--ok); }
.toast-error   { border-left-color: var(--danger); color: var(--danger); }

/* ── Empty state ────────────────────────────────────────────────────── */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-xl) var(--space);
  gap: var(--space-sm);
}
.empty-state .brand-mark {
  width: 44px;
  height: 44px;
  opacity: 0.55;
}
.empty-state h2 { margin: 0; }
.empty-state p  { margin: 0; max-width: 40ch; }

/* ── Skeleton (htmx loading) ────────────────────────────────────────── */

.skeleton {
  display: block;
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--bg-elev) 60%, transparent) 0%,
    color-mix(in srgb, var(--fg)       10%, var(--bg-elev)) 50%,
    color-mix(in srgb, var(--bg-elev) 60%, transparent) 100%);
  background-size: 200% 100%;
  animation: skel-pulse 1.4s ease-in-out infinite;
  margin-bottom: 8px;
}
.skeleton.skeleton-lg { height: 28px; }
.skeleton.skeleton-sm { height: 10px; width: 60%; }
@keyframes skel-pulse {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: block; }

/* ── Mobile nav ─────────────────────────────────────────────────────── */

.nav-toggle {
  display: none;
  background: transparent !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--radius);
  padding: 6px !important;
  color: var(--fg) !important;
  cursor: pointer;
}
/* Collapse the MENU row into the hamburger below 1024px. The brandline stays
   put (brand left, session cluster right, then the burger); the links live in
   a stacked dropdown anchored under the brandline. The session cluster is NOT
   inside the dropdown — theme/locale/logout remain one tap away. */
@media (max-width: 1024px) {
  .nav-toggle { display: inline-flex; align-items: center; }
  .topnav .container { position: relative; }
  /* The centered menu leaves the flow; brand stays left, so push the session
     cluster (and the burger after it) to the right edge — the flex:1 menu that
     did this on desktop is now display:none. */
  .topnav .nav-controls { margin-left: auto; }
  .nav-links {
    display: none;
    flex: none;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    position: absolute;
    top: calc(100% + 4px);
    right: var(--space);
    left: var(--space);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space);
    box-shadow: var(--shadow-lg);
  }
  .nav-links.open { display: flex; }
  .nav-primary {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    flex-wrap: nowrap;
    overflow: visible;
  }
  /* Every nav dropdown group restacks as a full-width section set off by a
     top hairline — the toggle is hidden and every grouped link shows inline
     (native fallback, no JS needed to reveal them in the hamburger). */
  .nav-dd {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    padding-top: var(--space-sm);
    border-top: 1px solid var(--border);
  }
  /* The first group needs no separator above it. */
  .nav-primary > .nav-dd:first-child {
    padding-top: 0;
    border-top: none;
  }
  .nav-dd-toggle { display: none; }
  .nav-dd-menu {
    display: flex;
    position: static;
    min-width: 0;
    padding: 0;
    gap: 2px;
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .nav-dd-menu a { font-size: 0.92em; padding: 9px 11px; }
  .nav-link { padding: 9px 11px; font-size: 0.92em; }
  .nav-link::after { display: none; }
  .nav-link.is-active {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
  }
}

@media (max-width: 640px) {
  .hero h1 { font-size: 1.7rem; }
  .page-header { flex-direction: column; align-items: flex-start; }
  .phase-table th, .phase-table td { padding: 10px 6px; font-size: 0.92em; }
  /* ── The brandline is TWO deliberate rows on a phone, not four accidental
     ones ───────────────────────────────────────────────────────────────────
     "Stays one line as long as possible" was the old intent, and at 390px it
     never held: the cluster is a wrap-enabled flex row inside a wrap-enabled
     flex row, so it broke wherever it ran out and the header came to four
     stacked rows and ~190px of chrome before a single word of the page —
     brand / a row of bare glyphs / a right-aligned Logout on its own / the
     hamburger alone on the fourth. Nothing chose those breaks.

     So they are chosen here. Row 1 is the running head: the brand at the left,
     the hamburger at the right end of the SAME line (`order` moves it ahead of
     the cluster without touching nav.html, where it is the last child).
     Row 2 is the session cluster, given the full width by a 100% flex-basis
     so it can never be dragged up into row 1's leftovers. Two rows, both
     purposeful, and the tap targets keep their size.

     The labels come back with the row. `.nav-control-text { display: none }`
     was the price of trying to hold one line, and it turned Theme and Classic
     into a bare moon and a bare column with no text at any size — the register
     toggle in particular is unguessable as a glyph. Given a row of their own
     they fit, so they are read again. (Both buttons do carry an `aria-label`,
     so a screen reader was never the audience that lost here — a sighted
     visitor was.) */
  .topnav-brandline { row-gap: var(--space-xs); }
  .topnav-brandline .brand { flex: 1 1 auto; }
  .nav-toggle { order: 1; }
  .topnav .nav-controls {
    order: 2;
    flex: 1 0 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
    row-gap: var(--space-xs);
    margin-left: 0;
  }
  .nav-identity { max-width: 14ch; }
}

/* ── Reduce-motion ────────────────────────────────────────────────────────
   The site-wide default for `prefers-reduced-motion: reduce`. What it is
   protecting against is everything in this file that moves on its own and is
   not opted out by name: the vault door's spinning rivets/ticks/wheel, the
   CRT shimmer and the retro-TV snow, the DENIED shudder + red klaxon flash,
   the LOCKED slam, the brandline sheen, the page-enter rise, the boot-splash
   crest draw, the .reveal stagger, the skeleton shimmer, the ONGOING pulse
   and the spinner. Vestibular triggers and one photosensitivity trigger. All
   of that has to stop, and a blanket rule is the only way to be sure a future
   animation is covered by default rather than by remembering to.

   BUT: it used to say `animation: none !important`, and that is not "stop
   moving", it is "never run". Any element whose RESTING appearance is the end
   frame of its own animation — the correct way to author one, because then no
   element can ever be left stranded mid-flight — was then never drawn at all.
   The wax-seal login of the classic register (base `opacity: 0`, every state's
   keyframes ending at the resting position) rendered as a completely blank
   door for anyone with the OS setting on: no seal, no feedback, no answer.
   Same class of bug for any `stroke-dashoffset` draw-in or fill-forwards
   reveal that is not individually rescued further down this file.

   So the kill is now a COLLAPSE, not a cancel. Every animation still runs —
   once, instantly, with no delay — so it lands on its final frame and the
   resting state is drawn; nothing is left in motion, because 0.01ms of it is
   below the threshold of perception. This is the WCAG 2.3.3 / MDN-recommended
   form of the reset: the guideline is about movement (parallax, slides,
   scaling, spinning, flashing), not about withholding information from the
   people who asked for less movement. Infinite loops are clamped to a single
   iteration, so ambient motion genuinely stops rather than racing.

   Two escape hatches, in order of preference:

   1. `--motion-fade-duration` — for an animation or transition that changes
      ONLY opacity (or colour): a cross-fade carries no motion at all, so it
      may keep a real duration. Declare it on the element and this rule hands
      that duration back, with no `!important` needed at the call site. It is
      registered non-inheriting, so it never leaks to descendants, and 0.01ms
      remains the default for everything that does not ask.
      Do NOT reach for it for anything that translates, scales or rotates.
   2. `animation: none` on a specific selector — this rule only overrides
      duration/delay/iteration-count, never `animation-name`, so any component
      that genuinely wants its animation cancelled (and states its resting
      appearance in plain CSS) keeps working exactly as before. */

@property --motion-fade-duration {
  syntax: "<time>";
  inherits: false;
  initial-value: 0.01ms;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    /* The var() fallback repeats the initial value so a browser without
       @property support still collapses instead of cancelling. */
    animation-duration: var(--motion-fade-duration, 0.01ms) !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--motion-fade-duration, 0.01ms) !important;
    transition-delay: 0s !important;
  }
}

/* ── Reference mode (prereq badges, teaches tags, mode toggle) ──────── */

.meta-strip { padding: 12px 16px; }
.meta-strip p { margin: 4px 0; }

.prereq-badge {
  text-decoration: none;
  text-transform: none;
  cursor: pointer;
}
.prereq-badge:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.teaches-tag {
  font-size: 0.78em;
  padding: 1px 6px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg) 60%, transparent);
}

.mode-toggle .mode-active {
  color: var(--accent);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 4px;
}

.next-up { border-left: 3px solid var(--accent); }

/* ── Academic: marks, bands, profile (Cortex University) ──────────────── */

.row.gap { display: flex; gap: 0.6rem; align-items: flex-end; }
.row.gap.wrap { flex-wrap: wrap; }
.grow { flex: 1 1 auto; }

/* Profile form fields (country · enrollment year · date of birth). The generic
   `.row.gap` bottom-aligns items so a [label][input] pair lines its input up
   with its neighbours — but the enrollment-year field carries an extra help
   line under its input, which shoved that field up and left the three inputs
   at three different heights (reading as an overlap/misalignment). Top-align
   here and give each field an equal, shrinkable column so labels + inputs line
   up cleanly and wrap to full width on narrow screens. */
.row.gap.profile-fields { flex-wrap: wrap; align-items: flex-start; }
.row.gap.profile-fields > label { flex: 1 1 9rem; min-width: 0; margin: 0; }

/* Phone pair (Borja 2026-07-20): dialling-region select first (left) at a fixed
   readable width, then the phone number field growing to fill the rest — sized
   as one aligned pair (the number field is no longer tiny). */
.profile-phone-pair .phone-region-field { flex: 0 1 15rem; min-width: 12rem; }
.profile-phone-pair .phone-region-field select { width: 100%; box-sizing: border-box; }
.profile-phone-pair > label.grow { flex: 1 1 12rem; }
.profile-phone-pair > label.grow input { width: 100%; box-sizing: border-box; }
/* Bottom-align the region <select> and phone <input> so the two boxes line up
   even though the phone label carries extra chrome (verify badge + link) that
   makes its column taller (task #115). Both controls share the same base
   height/padding, so a shared bottom edge reads as one aligned pair. */
.row.gap.profile-phone-pair { align-items: flex-end; }
.profile-phone-pair .phone-region-field select,
.profile-phone-pair > label.grow input { min-height: 42px; }

.primary-link { font-weight: 700; }

/* Classification band palette (fail→excellence). */
.band-fail { --band: #c0392b; }
.band-pass { --band: #8a6300; }
.band-merit { --band: #2e86c1; }
.band-distinction { --band: #6c5ce7; }
.band-excellence { --band: #1e8e5a; }
.band-none { --band: var(--muted, #888); }

.band-pill {
  display: inline-block; padding: 0.1rem 0.55rem; border-radius: 0.7rem;
  font-size: 0.72rem; font-weight: 700; color: #fff; background: var(--band);
}
.band-text-fail { color: #c0392b; }
.band-text-pass { color: #8a6300; }
/* Merit 3.72:1 and Excellence 3.89:1 on the light ground. Both PASS where they
   land on `.marks-score-num` (2.6rem — large text, 3:1), and both FAIL where
   `teacher/dashboard.html` applies them to a body-size <strong>. Darkened to
   5.17:1 and 5.72:1; the hue is unchanged, so the band ladder still reads
   blue → violet → green. Distinction (4.56:1) and the two ends already pass. */
.band-text-merit { color: #0f6ea8; }
/* 4.38:1 on classic paper — it cleared AA on --bg and failed on the register
   the site actually defaults to, which is exactly the gap the guard added with
   this change exists to close. */
.band-text-distinction { color: #5b4bd6; }
.band-text-excellence { color: #15704a; }
.band-warn { color: #8a6300; }

.marks-overview { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; }
.marks-score { display: flex; align-items: baseline; gap: 0.2rem;
  padding: 0.4rem 1rem; border-radius: 0.6rem; border-left: 4px solid var(--band); }
.marks-score-num { font-size: 2.6rem; font-weight: 800; line-height: 1; color: var(--band); }
.marks-score-den { font-size: 1rem; opacity: 0.6; }
.marks-overview-meta { display: flex; flex-direction: column; gap: 0.3rem; }
.marks-strengths { margin: 0.8rem 0; }

.marks-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
.marks-table th, .marks-table td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border, rgba(128,128,128,0.2)); }
.marks-table tr.is-unassessed { opacity: 0.55; }
.mono { font-family: var(--mono, monospace); }

.mark-bar-wrap { position: relative; display: inline-flex; align-items: center; gap: 0.4rem;
  width: 7rem; height: 0.95rem; background: rgba(128,128,128,0.18); border-radius: 0.3rem; overflow: hidden; }
.mark-bar-wrap.wide { width: 100%; max-width: 24rem; display: block; margin: 0.3rem 0; }
.mark-bar { position: absolute; left: 0; top: 0; bottom: 0; background: var(--band); border-radius: 0.3rem; }
.mark-bar-num { position: relative; margin-left: auto; padding-right: 0.4rem; font-size: 0.7rem; font-weight: 700; }

.phase-result-row { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.phase-result .marks-score-num { font-size: 1.8rem; }

.profile-photo-preview { display: flex; align-items: center; gap: 0.6rem; }
.profile-photo-preview img { width: 84px; height: 84px; object-fit: cover; border-radius: 0.5rem; border: 1px solid var(--border, rgba(128,128,128,0.3)); }

/* ── Terms & code of conduct page ────────────────────────────────────── */
.terms-body { max-height: 30rem; overflow-y: auto; }
/* Public /terms (no acceptance form) reads as a normal page, not a scrollbox. */
.terms-body-full { max-height: none; overflow-y: visible; }
.terms-body h1 { font-size: 1.3rem; }
.terms-body h2 { font-size: 1.05rem; margin-top: 1.2rem; }
.accepted-banner { border-left: 3px solid #1e8e5a; color: #1e8e5a; font-weight: 600; }
label.check { display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.86rem; cursor: pointer; }
label.check input { margin-top: 0.2rem; }

/* ── Credential verification page ────────────────────────────────────── */
.verify-input { font-family: var(--mono, monospace); font-size: 0.8rem; width: 100%; resize: vertical; }
.verify-result { border: 2px solid var(--muted, #888); border-left-width: 5px; }
.verify-result.is-valid { border-color: #1e8e5a; background: rgba(30, 142, 90, 0.07); }
.verify-result.is-invalid { border-color: #c0392b; background: rgba(192, 57, 43, 0.07); }
.verify-result.is-required { border-color: var(--muted, #888); background: rgba(140, 140, 140, 0.06); }
.verify-result.is-valid h2 { color: #1e8e5a; }
.verify-result.is-invalid h2 { color: #c0392b; }
.verify-result.is-required h2 { color: var(--muted, #888); }
.profile-status { border: 2px solid; border-radius: 8px; padding: 10px 14px; margin: 0 0 1.1rem; font-size: 0.92rem; display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.profile-status .ps-icon { font-weight: 700; }
.profile-status .ps-missing { color: var(--fg); font-weight: 600; }
.profile-status-complete { border-color: #1e8e5a; background: rgba(30, 142, 90, 0.09); color: #1e8e5a; }
.profile-status-partial { border-color: #c89a2b; background: rgba(200, 154, 43, 0.12); color: var(--gold-text); }
.profile-status-empty { border-color: #c0392b; background: rgba(192, 57, 43, 0.09); color: #c0392b; }

/* Dark-theme contrast lift: the amber/gold status + warn text (#a9791a / #b8860b)
   are too dark to read on the dark surface (#161b22). Lighten them to a gold that
   meets contrast, and deepen the valid/invalid verify tints so they stay visible. */
:root[data-theme="dark"] .band-warn,
:root[data-theme="dark"] .profile-status-partial { color: #e0b341; }
:root[data-theme="dark"] .verify-result.is-valid { background: rgba(30, 142, 90, 0.14); }
:root[data-theme="dark"] .verify-result.is-invalid { background: rgba(192, 57, 43, 0.14); }
/* The same lift for the classification-band text colors (Wave-0 audit S3):
   the light-theme band inks are too dark on the dark surface — every band
   gets a lightened dark-theme ink (pass gets the audit-endorsed #e0b341). */
:root[data-theme="dark"] .band-text-pass { color: #e0b341; }
:root[data-theme="dark"] .band-text-fail { color: #f2705f; }
:root[data-theme="dark"] .band-text-merit { color: #58a6ff; }
:root[data-theme="dark"] .band-text-distinction { color: #a99df1; }
:root[data-theme="dark"] .band-text-excellence { color: #34c584; }

/* Shared status-hue palette — single source of truth for the status pills,
   the status-distribution SVG segments (university-status dashboard + admin
   analytics), and the build-control badges. The Wave-0 audit found this
   palette hand-copied in three places; templates now reference these vars. */
:root {
  --status-ok: #1a7f37;
  --status-wip: #bc4c00;
  --status-designed: #8250df;
  --status-info: #0969da;
  --status-gated: #57606a;
  --status-warn: #9a6700;
}
:root[data-theme="dark"] {
  --status-ok: #3fb950;
  --status-wip: #e8905c;
  --status-designed: #bc8cff;
  --status-info: #79b8ff;
  --status-gated: #b1bac4;
  --status-warn: #e3b341;
}

/* Page transition: a smooth, classy fade-rise on each navigation. A whisper of
   blur + scale that settles, rather than a hard slide — reads as a considered
   reveal, not a jump. CSS-only; reduced-motion disables it (global rule). */
@keyframes lynx-page-in {
  0%   { opacity: 0; transform: translateY(10px) scale(0.994); filter: blur(2px); }
  60%  { filter: blur(0); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}
main.container {
  animation: lynx-page-in 360ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  main.container { animation: none; }
}

/* Dashboard: phases grouped under area headers (Fundamentals, Systems, …). */
.phase-group-head {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 1.6px;
  color: var(--accent);
  font-weight: 700;
  margin: 0 0 0.7rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border);
}
.verify-fields { display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 1rem; margin: 0.6rem 0 0; font-size: 0.86rem; }
.verify-fields dt { font-weight: 700; opacity: 0.7; }
.verify-fields dd { margin: 0; }

/* ── University logo emblems on portal pages ─────────────────────────── */
.page-header-titled { display: flex; align-items: center; gap: 1rem; }
.cx-page-logo {
  height: 76px; width: auto; border-radius: 0.55rem;
  box-shadow: 0 2px 10px rgba(0,0,0,0.25);
}
@media (max-width: 40rem) { .cx-page-logo { height: 56px; } }

/* ── Grade stars & gems (honor system — static/emblems/grades/, brief B5) ──
   Bronze★=MSc · Silver★=PhD · Gold★=PostDoc · Ruby◆=Special · Diamond◆=Top;
   1/2/3 marks = Merit / Distinction / Highest Distinction. The shapes are
   inlined by grade_gems() (_partials/grade_gems.html) and take their tier
   color from currentColor; both themes tune the inks via the --gem-* vars. */
:root {
  --gem-bronze: #a5642a;
  --gem-silver: #7e8590;
  --gem-gold: #b8860b;
  --gem-ruby: #b3122e;
  --gem-diamond: #6d94b8;
}
:root[data-theme="dark"] {
  --gem-bronze: #d08a4e;
  --gem-silver: #c3cad2;
  --gem-gold: #e0b341;
  --gem-ruby: #ef3a5d;
  --gem-diamond: #b7d9f2;
}
.grade-gems {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  vertical-align: middle;
  line-height: 0;
}
.grade-gems svg { width: 14px; height: 14px; display: block; }
.grade-gems.gg-sm svg { width: 11px; height: 11px; }
.grade-gems.gg-lg svg { width: 20px; height: 20px; }
.gg-bronze  { color: var(--gem-bronze); }
.gg-silver  { color: var(--gem-silver); }
.gg-gold    { color: var(--gem-gold); }
.gg-ruby    { color: var(--gem-ruby); }
.gg-diamond { color: var(--gem-diamond); }

/* ── Branded site footer ─────────────────────────────────────────────── */
.site-footer {
  margin-top: 2.5rem;
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
}
.site-footer-inner {
  display: flex; flex-wrap: wrap; gap: 0.6rem 1.5rem;
  align-items: center; justify-content: space-between;
  padding-top: 1rem; padding-bottom: 1rem;
}
.site-footer-brand { font-size: 0.84rem; }
.site-footer-motto {
  display: block; font-size: 0.7rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); margin-top: 0.15rem;
}
.site-footer-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; font-size: 0.8rem; }
.site-footer-links a, .site-footer-links span { white-space: nowrap; }
.site-footer-links a { color: var(--muted); text-decoration: none; }
.site-footer-links a:hover { color: var(--accent); }

/* ── Prev/next phase navigation (smooth walk through the curriculum) ──── */
.phase-nav {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; margin: 1.4rem 0 0.5rem;
}
.phase-nav-link {
  display: flex; flex-direction: column; gap: 0.2rem;
  padding: 0.7rem 0.9rem; border: 1px solid var(--border); border-radius: 0.6rem;
  background: var(--bg-elev); text-decoration: none; color: inherit;
  transition: transform 120ms ease, border-color 120ms ease;
}
.phase-nav-link:hover { transform: translateY(-2px); border-color: var(--accent); }
.phase-nav-next { text-align: right; align-items: flex-end; }
.phase-nav-dir { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; color: var(--accent); }
.phase-nav-title { font-size: 0.82rem; color: var(--muted); }
@media (max-width: 40rem) { .phase-nav { grid-template-columns: 1fr; } }

/* ── Boot splash ─────────────────────────────────────────────────────────
   An elegant once-per-session reveal: the Cortex crest draws itself in gold
   over a navy gradient, the wordmark + motto rise in, then the whole overlay
   fades to reveal the app. Hidden by default — portal.js opts in on the first
   view only, so it never flashes on navigation and never blocks usability if
   JS is absent. prefers-reduced-motion collapses it to an instant dismiss. */
.boot-splash {
  --boot-navy: #0a0e1a;
  --boot-navy-2: #11182d;
  --boot-gold: #d8b24a;
  --boot-gold-soft: #f0d68a;
  display: none;
  position: fixed;
  inset: 0;
  z-index: 9999;
  place-items: center;
  background:
    radial-gradient(900px 600px at 50% 38%,
      color-mix(in srgb, var(--boot-gold) 16%, transparent), transparent 62%),
    linear-gradient(160deg, var(--boot-navy) 0%, var(--boot-navy-2) 100%);
  color: var(--boot-gold-soft);
  cursor: pointer;
}
.boot-splash.boot-splash-play { display: grid; }
.boot-splash.boot-splash-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 520ms ease, visibility 0s linear 520ms;
}
.boot-splash-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 24px;
  text-align: center;
  /* The parent is `display: grid; place-items: center`, which sizes this item
     to its own max-content and lets it OVERFLOW the grid area rather than
     shrink — so at 390px the classic register laid out a 476px splash, off
     centre, with the wordmark running from x=24 to x=452 and the motto clipped
     mid-word. `max-width: 100%` caps the box at the viewport; the wordmark's
     own break opportunity (skin-classic.css §3) then does the wrapping. */
  max-width: 100%;
}
.boot-crest {
  width: 132px;
  height: 132px;
  filter: drop-shadow(0 0 20px color-mix(in srgb, var(--boot-gold) 45%, transparent));
}
.boot-crest circle,
.boot-crest path {
  fill: none;
  stroke: var(--boot-gold);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.boot-crest-ring { stroke-width: 1.6; opacity: 0.85; }
.boot-crest-glyph { stroke: var(--boot-gold-soft); }
.boot-splash-play .boot-crest-ring   { animation: boot-draw 900ms ease both; }
.boot-splash-play .boot-crest-shield { animation: boot-draw 1000ms ease 220ms both; }
.boot-splash-play .boot-crest-glyph  { animation: boot-draw 800ms ease 620ms both; }
.boot-wordmark {
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #f6efdc;
}
.boot-wordmark-accent { color: var(--boot-gold); margin-left: 0.4ch; }
.boot-motto {
  font-size: 0.74rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--boot-gold-soft) 75%, transparent);
}
.boot-splash-play .boot-wordmark { animation: boot-rise 700ms ease 980ms both; }
.boot-splash-play .boot-motto    { animation: boot-rise 700ms ease 1180ms both; }
/* Ceremonial gold rule between wordmark and motto: draws outward from the
   centre once the crest has finished tracing. */
.boot-rule {
  width: 150px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--boot-gold), transparent);
}
.boot-splash-play .boot-rule { animation: boot-rule-grow 600ms ease 1080ms both; }

@keyframes boot-draw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}
@keyframes boot-rise {
  from { opacity: 0; transform: translateY(10px); letter-spacing: 0.4em; }
  to   { opacity: 1; transform: none; }
}
@keyframes boot-rule-grow {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* Reduced motion: portal.js never shows the splash at all (instant reveal);
   these rules are belt-and-braces so a stray `-play` class can't animate. */
@media (prefers-reduced-motion: reduce) {
  .boot-splash { display: none !important; }
  .boot-splash-play .boot-crest-ring,
  .boot-splash-play .boot-crest-shield,
  .boot-splash-play .boot-crest-glyph,
  .boot-splash-play .boot-rule,
  .boot-splash-play .boot-wordmark,
  .boot-splash-play .boot-motto { animation: none; }
}

/* ── Inter-menu transition (fast page-enter on top-nav navigation) ────────
   Much shorter than the boot splash: the OLD document fades slightly on a
   nav-link click (html.nav-leave), and the NEW one plays a ~240ms enter —
   <main> rises in and the brand mark gives one subtle gold-tinged pulse.
   portal.js only applies these classes for plain left-clicks on nav links
   and skips them entirely under prefers-reduced-motion. */
html.nav-leave main,
html.nav-leave .site-footer {
  opacity: 0.25;
  transition: opacity 140ms ease;
}
html.nav-enter main { animation: page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1) both; }
html.nav-enter .brand-mark { animation: mark-pulse 320ms ease both; }
@keyframes page-enter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes mark-pulse {
  0%   { transform: none; }
  45%  { transform: scale(1.18); color: #d8b24a; }
  100% { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  html.nav-leave main,
  html.nav-leave .site-footer { opacity: 1; transition: none; }
  html.nav-enter main,
  html.nav-enter .brand-mark { animation: none; }
}

/* ── About: institutional identity page ──────────────────────────────────
   Centered crest + name + motto + blurb, with gold accents and a staggered
   reveal. The crest draws in on load (reduced-motion shows it instantly). */
.about {
  max-width: 760px;
  margin: 0 auto;
  padding: var(--space-lg) var(--space) var(--space-xl);
  text-align: center;
}
.about-crest-wrap { display: flex; justify-content: center; margin-bottom: var(--space-md); }
.about-crest {
  width: 150px;
  height: 150px;
  filter: drop-shadow(0 0 16px color-mix(in srgb, var(--band-pass, #b8860b) 35%, transparent));
}
.about-crest circle,
.about-crest path {
  fill: none;
  stroke: #c9a23a;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.about-crest-ring { stroke-width: 1.6; opacity: 0.8; }
.about-crest-glyph { stroke: color-mix(in srgb, #c9a23a 80%, var(--fg)); }
.about-crest-ring   { animation: boot-draw 1000ms ease both; }
.about-crest-shield { animation: boot-draw 1100ms ease 200ms both; }
.about-crest-glyph  { animation: boot-draw 900ms ease 640ms both; }
.about-name {
  font-size: 2.3rem;
  margin: 0 0 0.2rem;
  letter-spacing: -0.01em;
  background: linear-gradient(135deg, var(--fg), #c9a23a);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.about-motto {
  margin: 0 0 var(--space);
  font-size: 0.82rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold-text);
  font-weight: 600;
}
.about-divider {
  width: 64px;
  height: 2px;
  margin: var(--space) auto var(--space-md);
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, #c9a23a, transparent);
}
.about-lede {
  font-size: 1.12rem;
  line-height: 1.6;
  color: var(--fg);
  max-width: 56ch;
  margin: 0 auto var(--space);
}
.about-body {
  color: var(--muted);
  max-width: 60ch;
  margin: 0 auto var(--space-lg);
}
.about-pillars {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space);
  margin: 0;
  text-align: left;
}
.about-pillar {
  border: 1px solid var(--border);
  border-top: 2px solid #c9a23a;
  border-radius: var(--radius-lg);
  padding: var(--space) var(--space-md);
  background: var(--bg-elev);
  box-shadow: var(--shadow-sm);
}
.about-pillar dt {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--gold-text);
  letter-spacing: 0.02em;
  margin-bottom: 0.3rem;
}
.about-pillar dd { margin: 0; font-size: 0.9rem; color: var(--muted); }

/* Staggered reveal for the textual blocks. Initial hidden state lives only in
   the keyframe's `from` (with fill `both`), so when reduced-motion cancels the
   animation the elements fall back to their fully-visible default. */
.reveal { animation: about-reveal 620ms cubic-bezier(0.22, 0.61, 0.36, 1) both; }
.reveal-1 { animation-delay: 120ms; }
.reveal-2 { animation-delay: 240ms; }
.reveal-3 { animation-delay: 360ms; }
@keyframes about-reveal {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@media (max-width: 40rem) {
  .about-name { font-size: 1.8rem; }
  .about-lede { font-size: 1rem; }
}
/* Dark-theme lift for the About gold inks (same contrast rule as .band-warn).
   `:not([data-ui="classic"])` because this lift is a statement about the
   GROUND, not about the theme attribute: base.html hard-codes
   `data-theme="dark"` on <html>, and the classic register then repaints the
   page onto cream paper without clearing it. So a pale "lift for dark" ink
   landed on parchment — measured 1.77:1 (.about-motto) and 1.87:1
   (.about-section-title), both far under AA. Excluding the classic register
   here lets skin-classic.css's own paper inks apply; without it those rules
   lose on specificity, (0,3,0) beating (0,2,1). */
:root[data-theme="dark"]:not([data-ui="classic"]) .about-motto,
:root[data-theme="dark"]:not([data-ui="classic"]) .about-section-title { color: #e0b341; }

/* About — 4-section tab navigation (#118). Progressive enhancement: the
   nav.about-tabs is [hidden] at render and revealed by about-nav.js; without JS
   every panel renders in a long scroll. WAI-ARIA Tabs pattern. */
.about-tabs { margin: var(--space-lg) 0 0; }
.about-tablist {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: center;
  border-bottom: 1px solid var(--border);
}
.about-tab {
  appearance: none;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  padding: 0.5rem 0.9rem;
  margin-bottom: -1px;
  border-radius: var(--radius) var(--radius) 0 0;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.about-tab:hover { color: var(--fg); background: var(--bg-elev); }
.about-tab[aria-selected="true"] { color: var(--fg); border-bottom-color: #c9a23a; }
.about-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
:root[data-theme="dark"] .about-tab[aria-selected="true"] { border-bottom-color: #e0b341; }
.about-panel { margin-top: var(--space-lg); }
.about-panel:focus { outline: none; }
.about-panel:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: var(--radius);
}

/* About — heraldry, honor marks, and license (Wave 1). New sections keep the
   centered gold-accent language; the crest artwork is photographic-on-dark,
   so it always sits on a fixed dark plate that holds in both themes. */
.about-section { margin-top: var(--space-xl); }
.about-section-title {
  font-size: 0.8rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-text);
  font-weight: 700;
  margin: 0 0 var(--space-sm);
}
.about-section-lede {
  color: var(--muted);
  font-size: 0.92rem;
  max-width: 60ch;
  margin: 0 auto var(--space-md);
}
.about-mark-card {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(120% 90% at 50% 0%, #1d2027 0%, #101318 70%);
  padding: var(--space-md) var(--space) var(--space);
  box-shadow: var(--shadow-sm);
}
.about-mark-card img {
  display: block;
  margin: 0 auto;
  height: 150px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
.about-mark-card figcaption {
  margin-top: 0.7rem;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: #cdd3da; /* fixed ink — the plate is dark in both themes */
}
/* Coat of arms — the single traditional plate, centered (P19 dropped the
   modern digital emblem that used to sit beside it). */
.about-arms {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-md);
}
.about-arm { max-width: 280px; width: 100%; }
.about-arm img { height: 200px; }
/* Institutional brand marks — the src-8 icon family (also the favicon). */
.about-brand-marks {
  list-style: none;
  margin: 0 0 var(--space-lg);
  padding: var(--space) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space) var(--space-lg);
  justify-content: center;
}
.about-brand-marks li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.about-brand-marks img { width: 48px; height: 48px; object-fit: contain; }

.about-gem-note { margin: var(--space) 0 0; font-size: 0.82rem; color: var(--muted); }
.about-license {
  margin-top: var(--space-xl);
  text-align: left;
  border: 1px solid var(--border);
  border-top: 2px solid #c9a23a;
  border-radius: var(--radius-lg);
  background: var(--bg-elev);
  padding: var(--space-md);
  box-shadow: var(--shadow-sm);
}
.about-license h2 { margin: 0 0 0.45rem; font-size: 1.05rem; }
.spdx-chip {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--gold-text);
  border: 1px solid #c9a23a;
  border-radius: var(--radius-pill);
  padding: 0.05rem 0.6rem;
  margin-left: 0.5rem;
  vertical-align: 0.12em;
}
:root[data-theme="dark"] .spdx-chip { color: #e0b341; }
.about-license p { margin: 0.45rem 0; font-size: 0.9rem; color: var(--muted); }
.about-license-copy { font-weight: 600; color: var(--fg); }
.about-license details { margin-top: var(--space-sm); }
.about-license summary {
  cursor: pointer;
  font-size: 0.86rem;
  color: var(--accent);
  font-weight: 600;
}
.about-license pre {
  margin: var(--space-sm) 0 0;
  font-size: 0.72rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ── Cortex Press (scoped .press-*) ─────────────────────────────────── */
/* The press pages shipped with semantic class hooks but no styles: the
   Library facets rendered as huge full-width stacked fields, long
   verification hashes overflowed phones, and the author/editor action
   cells were unspaced. Everything here is scoped under .press-* — no
   global element rules, no leaks into other pages. */

/* Library filter facets: one compact filter bar, wrapping on narrow. */
.press-facets {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space);
  margin: var(--space) 0 var(--space-lg);
}
.press-facets > label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 11rem;
  min-width: 0;
  color: var(--muted);
  font-size: 0.82em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.press-facets select,
.press-facets input {
  font-size: 1rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.press-facets button { flex: 0 0 auto; }
.press-facets > a { padding: 9px 0; white-space: nowrap; }

/* Search bar: input + button + back-link on one wrapping line. */
.press-search-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin: var(--space) 0 var(--space-lg);
}
.press-search-form input[type="text"] { flex: 1 1 16rem; width: auto; min-width: 0; }
.press-search-form a { white-space: nowrap; }

/* Publication lists (Library “Latest” / “Distinguished” / search results). */
.press-list { margin: var(--space-sm) 0 var(--space-lg); padding-left: 1.2em; }
.press-list li { margin: 8px 0; }

/* Long verification ids / content hashes must wrap instead of widening the
   page on phones (the only unbreakable strings on these pages). */
.press-publication code,
.press-verify code,
.press-honesty code {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Tag chips on a publication page. */
.press-tags { display: flex; flex-wrap: wrap; gap: 6px; }

/* Verification record: label/value grid instead of UA-indented dl. */
.press-verify dl {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 8px var(--space-lg);
  margin: var(--space) 0;
}
.press-verify dt {
  color: var(--muted);
  font-size: 0.82em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding-top: 3px;
}
.press-verify dd { margin: 0; min-width: 0; }
@media (max-width: 480px) {
  .press-verify dl { grid-template-columns: 1fr; gap: 2px; }
  .press-verify dd { margin-bottom: 8px; }
}

/* ── Status & admin dataviz (STATUS_UX_SPEC) ─────────────────────────────
   Shared component CSS for /admin/university-status + /admin: the promoted
   ust-st-* status pills, tier-metal pills, donut/gauge/bars, ONGOING pulse,
   stat tiles, zebra/sortable tables. All selectors are single-class .ust-*
   (never fights .card/.phase-table); theme switching is token-only except
   the four pill families, which keep their shipped per-theme blocks. */

:root {
  /* Heraldic metals (heraldry.py _METAL_STOPS — hi/mid/lo, verbatim) */
  --metal-gold-hi: #f0d68a;  --metal-gold: #d8b24a;  --metal-gold-lo: #a07818;
  --metal-silver-hi: #e8ecf2; --metal-silver: #b8bcc4; --metal-silver-lo: #7e858f;
  --metal-slate-hi: #a9b4c4;  --metal-slate: #64748b; --metal-slate-lo: #3b4554;
  /* Chart chrome */
  --viz-track: color-mix(in srgb, var(--accent) 16%, transparent);
  --viz-grid:  color-mix(in srgb, var(--fg) 8%, transparent);
}

/* Smooth anchor scrolling (reduced-motion kills it); anchored headings never
   land under the (possibly 2–3-row) logged-in nav. */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
.ust-anchor { scroll-margin-top: 140px; }

/* Screen-reader-only text (tier letters/status words stay visible; this is
   for control labels like the expand chevron). Deliberately NOT the classic
   `position: absolute` recipe: main.container is transformed (page-in
   animation), so an absolute sr-span inside a `.table-scroll` would resolve
   its containing block OUTSIDE the scroll container and leak the table's
   width into the page's horizontal scroll (Chromium probe, 390px). The
   in-flow 1px inline-block variant stays contained. */
.ust-sr {
  display: inline-block;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Status pill anatomy + the ledger status scale (promoted from the
   university_status template so /admin can reuse it — spec §5.4). */
.ust-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 0.08em 0.55em;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 0.85em;
  line-height: 1.5;
  white-space: nowrap;
  margin-right: 0.35em;
}
.ust-badge .ust-emoji { font-size: 0.95em; }
:root[data-theme="dark"] .ust-st-done { background: rgba(63,185,80,.16); color: #3fb950; border-color: rgba(63,185,80,.45); }
:root[data-theme="light"] .ust-st-done { background: #dafbe1; color: #1a7f37; border-color: #aceebb; }
:root[data-theme="dark"] .ust-st-wip { background: rgba(219,109,40,.16); color: #e8905c; border-color: rgba(219,109,40,.45); }
:root[data-theme="light"] .ust-st-wip { background: #fff1e5; color: #bc4c00; border-color: #ffd8b5; }
:root[data-theme="dark"] .ust-st-designed { background: rgba(163,113,247,.16); color: #bc8cff; border-color: rgba(163,113,247,.45); }
:root[data-theme="light"] .ust-st-designed { background: #fbefff; color: #8250df; border-color: #ecd8ff; }
:root[data-theme="dark"] .ust-st-planned { background: rgba(88,166,255,.16); color: #79b8ff; border-color: rgba(88,166,255,.45); }
:root[data-theme="light"] .ust-st-planned { background: #ddf4ff; color: #0969da; border-color: #b6e3ff; }
:root[data-theme="dark"] .ust-st-gated { background: rgba(139,148,158,.16); color: #b1bac4; border-color: rgba(139,148,158,.45); }
:root[data-theme="light"] .ust-st-gated { background: #eff1f3; color: #57606a; border-color: #d0d7de; }
:root[data-theme="dark"] .ust-st-not-started { background: transparent; color: var(--muted); border-color: var(--border); }
:root[data-theme="light"] .ust-st-not-started { background: transparent; color: var(--muted); border-color: var(--border); }
:root[data-theme="dark"] .ust-st-open { background: rgba(210,153,34,.16); color: #e3b341; border-color: rgba(210,153,34,.45); }
:root[data-theme="light"] .ust-st-open { background: #fff8c5; color: #9a6700; border-color: #eed888; }

/* Tier-metal pills (program tiers — NOT the partner .badge-t* set; partner t3
   is bronze, program T3 is slate). The tier letter is always rendered text. */
.ust-tier-1 {
  background: color-mix(in srgb, var(--metal-gold) 26%, transparent);
  border-color: color-mix(in srgb, var(--metal-gold) 60%, var(--border));
  color: #5a4708;
  font-weight: 600;
}
.ust-tier-2 {
  background: color-mix(in srgb, var(--metal-silver) 22%, transparent);
  border-color: color-mix(in srgb, var(--metal-silver) 55%, var(--border));
  color: var(--fg);
  font-weight: 600;
}
.ust-tier-3 {
  background: color-mix(in srgb, var(--metal-slate) 18%, transparent);
  border-color: color-mix(in srgb, var(--metal-slate) 50%, var(--border));
  color: var(--metal-slate-lo);
  font-weight: 600;
}
.ust-tier-low {
  background: transparent;
  border-color: var(--border);
  color: var(--muted);
  font-weight: 600;
}
:root[data-theme="dark"] .ust-tier-1 {
  background: color-mix(in srgb, var(--metal-gold) 16%, transparent);
  color: var(--metal-gold-hi);
}
:root[data-theme="dark"] .ust-tier-2 {
  background: color-mix(in srgb, var(--metal-silver) 16%, transparent);
  color: var(--metal-silver-hi);
}
:root[data-theme="dark"] .ust-tier-3 {
  background: color-mix(in srgb, var(--metal-slate) 16%, transparent);
  color: var(--metal-slate-hi);
}
/* The "· Gold" word part shows ≥720px only; the letter is always present. */
@media (max-width: 719.98px) { .ust-tier-word { display: none; } }

/* Hero donut (spec §2.1.1) */
.ust-donut-track { stroke: var(--viz-track); }
.ust-donut-value { stroke: var(--accent); }
.ust-donut-wrap  { position: relative; width: 148px; margin: 0 auto; }
/* The SVG always fills its wrap: a wrap resized by page CSS (e.g. the status
   dashboard's 124px density override) must scale the ring with it, or the
   center overlay's numbers drift off the ring's true center. */
.ust-donut-wrap svg { width: 100%; height: auto; display: block; }
.ust-donut-center{ position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.ust-donut-pct   { font-size: 2rem; font-weight: 650; line-height: 1; }
.ust-donut-sub   { font-size: 0.78rem; color: var(--muted); }

/* Remaining-effort gauge (spec §2.1.3) */
.ust-gauge-track { stroke: var(--viz-track); }
.ust-gauge-value { stroke: var(--accent); }
.ust-gauge-tick  { stroke: var(--viz-grid); }
.ust-gauge-sub { font-size: 0.78rem; color: var(--muted); text-align: center; max-width: 22ch; }

/* Status-count chips (legend + filter in one element — spec §2.1.2) */
.ust-chips { display: flex; flex-wrap: wrap; gap: 6px; }
button.ust-chip { cursor: pointer; font: inherit; font-size: 0.85em; }
button.ust-chip:focus-visible { outline: none; box-shadow: var(--ring); }
button.ust-chip[aria-pressed="true"] { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent); }
.ust-chip-zero { opacity: 0.55; }
.ust-chip-count { font-weight: 650; font-variant-numeric: tabular-nums; }

/* Per-level progress bars (spec §2.2): one hue, 10px track, staggered reveal.
   Final state never depends on the animation (reduced-motion kill is global). */
.ust-bar-row {
  display: grid;
  grid-template-columns: 9rem 1fr 6.5rem;
  gap: var(--space-sm);
  align-items: center;
  margin: 8px 0;
}
@media (max-width: 560px) { .ust-bar-row { grid-template-columns: 6rem 1fr 6.5rem; } }
.ust-bar-label { font-size: 0.9em; }
.ust-bar-track { height: 10px; border-radius: 5px; background: var(--viz-track); overflow: hidden; }
.ust-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 0 5px 5px 0;
  transform-origin: left;
  animation: ust-fill-in 700ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
  animation-delay: calc(var(--i, 0) * 60ms);
}
@keyframes ust-fill-in { from { transform: scaleX(0); } }
.ust-bar-value { font-size: 0.85em; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }

/* ONGOING pulse pill (spec §2.4): danger hue, sonar dot. Reduced motion →
   solid dot + the word (the global kill cancels the keyframes). */
.ust-ongoing {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, var(--border));
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.ust-ongoing-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger);
  box-shadow: 0 0 0 2px var(--bg-elev);
  animation: ust-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes ust-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 45%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
/* One-shot row highlight when the poller changes a badge (spec §2.4). */
@keyframes ust-row-flash {
  from { background: color-mix(in srgb, var(--accent) 14%, transparent); }
  to   { background: transparent; }
}
.ust-row-flash { animation: ust-row-flash 1.2s ease-out both; }

/* Refresh spinner (↻ glyph spins while a soft refresh is in flight). */
.ust-refresh-btn { display: inline-flex; align-items: center; gap: 6px; }
.ust-spin-glyph { display: inline-block; }
.ust-refreshing .ust-spin-glyph { animation: ust-spin 900ms linear infinite; }
@keyframes ust-spin { to { transform: rotate(360deg); } }
/* Refetch keeps the frame: charts hold at reduced opacity, no skeleton swap. */
.ust-hold { opacity: 0.55; }

/* Collapsible section cards (native <details> — spec §2.6). */
details.ust-section > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
details.ust-section > summary::-webkit-details-marker { display: none; }
details.ust-section > summary h2 { margin: 0; }
details.ust-section > summary .ust-sum-meta {
  display: inline-flex; align-items: center; gap: 8px; margin-left: auto;
}
details.ust-section > summary:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 6px; }
details.ust-section .chev,
.ust-expand .chev {
  display: inline-block;
  font-weight: 700;
  color: var(--muted);
  transition: transform var(--transition);
}
details[open].ust-section > summary .chev { transform: rotate(90deg); }
details.ust-section[open] > summary { margin-bottom: var(--space-sm); }

/* "Submit a problem" capture box (backlog P28 — _partials/problem_button.html).
   A plain <details class="card">; the summary is styled to read as a button
   so the collapsed state matches the other .btn-link CTAs on the page. */
.problem-report-box > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.9em;
  font-weight: 600;
}
.problem-report-box > summary::-webkit-details-marker { display: none; }
.problem-report-box > summary:hover {
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  border-color: color-mix(in srgb, var(--fg) 30%, var(--border));
}
.problem-report-box[open] > summary { margin-bottom: var(--space-sm); }
.problem-report-box form.stack { margin-top: var(--space-sm); }

/* Expandable rows (details-on-demand — spec §2.8). */
.ust-expand-cell { width: 2.5em; }
.ust-expand-form { margin: 0; }
button.ust-expand {
  background: transparent;
  border: 1px solid transparent;
  padding: 2px 8px;
  border-radius: var(--radius);
  cursor: pointer;
}
button.ust-expand:hover { border-color: var(--border); background: color-mix(in srgb, var(--fg) 6%, transparent); }
button.ust-expand[aria-expanded="true"] .chev { transform: rotate(90deg); }
tbody tr.ust-detail > td {
  background: color-mix(in srgb, var(--fg) 3%, transparent);
}

/* Zebra + hover + sortable tables (spec §3.4). `.ust-zebra` stripes plain
   tables; `.ust-zebra-x` stripes tables whose data rows interleave with
   hidden detail rows (2 DOM rows per item → 4n+1 keeps honest striping). */
.ust-zebra tbody tr:nth-child(odd) { background: color-mix(in srgb, var(--fg) 3%, transparent); }
.ust-zebra-x tbody tr:nth-child(4n+1) { background: color-mix(in srgb, var(--fg) 3%, transparent); }
.ust-zebra tbody tr:hover,
.ust-zebra-x tbody tr:not(.ust-detail):hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }
th[data-sort] button.ust-sort {
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
th[data-sort] button.ust-sort:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 3px; }
.ust-sort-glyph { font-size: 0.9em; }

/* Admin stat tiles (spec §3.1). Plain HTML/CSS — numbers, not charts. */
.ust-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space);
  margin: var(--space-sm) 0 var(--space-md);
}
.ust-tile {
  display: block;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space) var(--space-md);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--transition);
  min-width: 0;
}
a.ust-tile:hover { border-color: var(--accent); text-decoration: none; color: inherit; }
.ust-tile-label {
  display: block;
  color: var(--muted);
  font-size: 0.78em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.ust-tile-value { display: block; font-size: 1.6rem; font-weight: 650; line-height: 1.25; }
.ust-tile-sub { display: block; color: var(--muted); font-size: 0.8em; overflow-wrap: anywhere; }
.ust-tile-delta-up { color: var(--ok); }
.ust-tile-delta-down { color: var(--danger); }
/* Admin System panel — grouped stat-tile sections (roster / assessments) with a
   heading + caption above each tile grid (Borja 2026-07-21). */
.sys-group { margin: 1.1rem 0 0; }
.sys-group-title {
  margin: 0 0 0.15rem;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.sys-group-caption { margin: 0 0 0.2rem; }
.sys-group .ust-tiles { margin-top: 0.5rem; }
.sys-group .ust-tile-sub .badge { margin-top: 2px; }

/* Roles mini-meter (spec §3.2): one-hue ordinal stacked bar, 2px surface gaps. */
.ust-meter { display: flex; column-gap: 2px; height: 8px; margin-top: 6px; border-radius: 4px; overflow: hidden; }
.ust-meter-seg { min-width: 3px; }

/* ── Partners admin table (compact layout, logo column leads) ─────────────
   7 columns: LOGO (own fixed-width column, ~2× the old inline slot), COMPANY
   (one-line name + sector·country), STUDENTS (accepted/rejected/ongoing/total
   stat cluster), RATIO (+ rejection streak subline), LINKS (icon pair), TIER
   and RECORD (inline forms that wrap, full option text visible). Wide rows
   pan inside .table-scroll — never the page. Below 860px the rows degrade to
   stacked cards (each cell labelled via data-label). */
.partners-table { table-layout: auto; }
/* The registry gained a dedicated 2× logo column: widen the page canvas so
   all 7 columns sit without an inner pan on desktop (same :has() idiom as the
   status dashboard's wide canvas; no-:has browsers keep 1080 + inner scroll).
   `.table-wide-canvas` (G2) generalises the same idiom: apply it to any other
   dense table that needs the roomier 1400px canvas instead of writing a new
   bespoke `:has()` rule per page (admin/catalogue is the first reuse). */
main.container:has(.partners-table),
main.container:has(.table-wide-canvas) { max-width: 1400px; }
.partners-table th,
.partners-table td { padding: 10px 8px; }
.partners-table td { vertical-align: middle; }
/* The generic first-column treatment (mono, 3em — meant for phase numbers)
   would crush the LOGO column; restore normal text metrics. */
.partners-table td:first-child {
  font-family: inherit;
  font-size: inherit;
  width: auto;
}

/* Logo column: fixed-width so Company can flex; art is 2× the old 20px slot
   (40px tall, aspect preserved via object-fit) and never grows past 192px. */
.partners-table th.partner-logo-col { width: 200px; }
.partner-logo-cell { white-space: nowrap; }

/* Company identity cell: the name holds ONE line (never wraps), with the
   sector·country meta beneath it. */
.partner-co {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
}
.partner-co-name { font-weight: 600; line-height: 1.2; white-space: nowrap; }
.partner-co-meta { color: var(--muted); font-size: 0.78em; line-height: 1.2; white-space: nowrap; }
.partner-logo {
  display: flex;
  align-items: center;
  height: 40px;
}
.partner-logo img {
  height: 40px;
  max-width: 192px;
  width: auto;
  object-fit: contain;
  object-position: left center;
  display: block;
}
/* Monogram fallback when no /static/partner_logos/<slug>.(svg|png) asset
   exists: company initials in a tier-tinted rounded box. Same 40px slot. */
.partner-monogram {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  min-width: 52px;
  padding: 0 10px;
  border-radius: 8px;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.partner-monogram-t1 {
  border-color: color-mix(in srgb, #d8b24a 60%, var(--border));
  color: var(--gold-text);
  background: color-mix(in srgb, #d8b24a 22%, transparent);
}
:root[data-theme="dark"] .partner-monogram-t1 { color: #f0d68a; }
.partner-monogram-t2 {
  border-color: color-mix(in srgb, #b8bcc4 55%, var(--border));
  color: var(--fg);
  background: color-mix(in srgb, #b8bcc4 18%, transparent);
}
.partner-monogram-t3 {
  border-color: color-mix(in srgb, #c08a4a 55%, var(--border));
  color: #8a5c2a;
  background: color-mix(in srgb, #c08a4a 18%, transparent);
}
:root[data-theme="dark"] .partner-monogram-t3 { color: #e0b07a; }
.partner-monogram-special {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
}
.partner-monogram-low,
.partner-monogram-excluded {
  border-color: var(--border);
  color: var(--muted);
  background: color-mix(in srgb, var(--muted) 10%, transparent);
}

/* Accepted/rejected/ongoing/total cluster: four labelled mini-stats in one
   cell. Colour reinforces, never carries — each figure keeps a text label. */
.pstats { display: flex; gap: 10px; white-space: nowrap; }
.pstat { display: inline-flex; flex-direction: column; align-items: center; min-width: 1.6em; }
.pstat-n { font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
.pstat-l {
  color: var(--muted);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.pstat-accepted .pstat-n { color: var(--ok); }
.pstat-rejected .pstat-n { color: var(--danger); }
.pstat-ongoing .pstat-n { color: var(--accent); }

/* Ratio cell: percentage + optional rejection-streak subline. */
.pratio { display: flex; flex-direction: column; gap: 2px; white-space: nowrap; }
.pratio-streak { color: var(--danger); font-size: 0.72em; }

/* Link icons (website / careers): compact 26px hit targets, labelled for AT. */
.plinks { display: flex; gap: 4px; }
.plink {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 6px;
  color: var(--muted);
  border: 1px solid transparent;
}
.plink:hover,
.plink:focus-visible {
  color: var(--accent);
  border-color: var(--border);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* Tier / record forms: wrap instead of clipping; selects size to content. */
.partners-cell-action { min-width: 0; }
.partners-cell-action form.row { flex-wrap: wrap; gap: 6px; align-items: center; }
.partners-cell-action .tier-select,
.partners-cell-action .outcome-select {
  width: auto;
  min-width: 0;
  max-width: 100%;
}

/* Narrow viewports: stacked cards instead of a sideways pan. Each cell shows
   its column label (data-label) as an eyebrow; the company cell leads. */
@media (max-width: 860px) {
  .partners-table thead { display: none; }
  .partners-table,
  .partners-table tbody { display: block; }
  .partners-table tbody tr {
    display: block;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-elev);
    padding: 6px 12px;
    margin-bottom: var(--space);
  }
  .partners-table tbody td {
    display: block;
    border-bottom: 0;
    padding: 6px 0;
  }
  .partners-table tbody td[data-label]::before {
    content: attr(data-label);
    display: block;
    color: var(--muted);
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: 2px;
  }
  .partners-table .partner-co { min-width: 0; }
}

/* Heraldic section eyebrow on /admin cards (spec §3.5): the one gold echo. */
.ust-eyebrow {
  display: block;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 1.6px;
  font-weight: 700;
  color: var(--metal-gold-lo);
  margin: 0 0 2px;
}
:root[data-theme="dark"] .ust-eyebrow { color: color-mix(in srgb, var(--metal-gold-hi) 80%, transparent); }

/* Standing verdict (verify page): metal is never colour-alone — the word
   itself is rendered; colour only reinforces it. */
.press-standing-published,
.press-standing-archived { color: var(--ok); }
.press-standing-revoked { color: var(--danger); }

/* Author/editor action cells: stack each action with breathing room. */
td.press-actions > a { display: inline-block; margin: 2px 0 6px; }
td.press-actions form { margin: 0 0 6px; }
td.press-actions form:last-child { margin-bottom: 0; }
.press-publish-form { display: grid; gap: 6px; justify-items: start; }
.press-publish-form label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.9em;
}
.press-publish-form select { width: auto; }

/* ── Cortex Newsletter (/newsletter) ─────────────────────────────────────
   Announcement surfaces: masthead + signature, type-coded badges, the
   permanent v0 test ribbon, filter tabs, cards and partner chips. Colors
   ride the theme variables so light + dark both read cleanly. */
.nl-masthead {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-bottom: 12px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.nl-crest { width: 52px; height: 52px; color: var(--accent); flex: none; }
.nl-masthead-text { display: grid; gap: 2px; }
.nl-wordmark { font-size: 1.3em; font-weight: 700; letter-spacing: 0.02em; }
/* The seam between the two wordmark spans. There is no whitespace between them
   in the markup, so without a margin "Cortex" and "University" are set flush —
   the header's `.brand-text-accent` has carried `margin-left: 2px` for exactly
   this reason and the masthead never got it. skin-classic.css widens it again
   for the classic register, whose tracking is wide enough to swallow 2px. */
.nl-wordmark-accent { color: var(--accent); margin-left: 2px; }
.nl-signature { color: var(--muted); font-size: 0.85em; }
.nl-filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
.nl-filter {
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 0.9em;
  text-decoration: none;
}
.nl-filter.is-active {
  border-color: var(--accent-strong);
  color: var(--accent);
  font-weight: 600;
}
.nl-cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.nl-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  background: var(--bg-elev);
}
.nl-card-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.nl-card-title { margin: 6px 0 4px; font-size: 1.15em; }
.nl-datetime { font-variant-numeric: tabular-nums; }
.nl-signed { color: var(--muted); font-style: italic; margin-bottom: 0; }
.nl-title { margin-top: 10px; }
/* Type badges: launch / deadlines / partners / notice, color-coded. */
.nl-badge-program_launch { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.nl-badge-deadlines { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.nl-badge-partner_update { background: color-mix(in srgb, #b58900 22%, transparent); color: var(--gold-text); }
:root[data-theme="dark"] .nl-badge-partner_update { color: #e0c060; }
.nl-badge-custom { background: color-mix(in srgb, var(--muted) 20%, transparent); color: var(--fg); }
/* The permanent v0 development/test ribbon (spec §4.3). */
.nl-ribbon {
  display: inline-block;
  border: 1px dashed #b58900;
  color: var(--gold-text);
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75em;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
:root[data-theme="dark"] .nl-ribbon { color: #e0c060; border-color: #e0c060; }
.nl-ribbon-block { display: block; padding: 6px 10px; margin: 10px 0; }
.nl-highlight {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  border: 1px solid var(--border);
  border-left: 3px solid var(--ok);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 12px 0;
  background: var(--bg-elev);
}
.nl-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.nl-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px 3px 4px;
  font-size: 0.9em;
  background: var(--bg-elev);
}
.nl-chip-distinguished { border-color: #b58900; }
:root[data-theme="dark"] .nl-chip-distinguished { border-color: #e0c060; }
.nl-body table { border-collapse: collapse; margin: 10px 0; }
.nl-body th, .nl-body td { border: 1px solid var(--border); padding: 5px 10px; }
.nl-inline-form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.nl-inline-form select, .nl-inline-form input[type="text"] { width: auto; }
.nl-inline-form input[type="tel"] { width: auto; }

/* University phone number, in the site-mode panel. The warning state has to
   read as unfinished business at a glance — this is the last screen before a
   one-way flip, so a calm grey note would be the wrong design. */
.inst-phone { border-left: 3px solid var(--border, #d0d0d0); padding: 8px 0 8px 12px; margin: 14px 0; }
.inst-phone h3 { margin: 0 0 4px; font-size: 1rem; }
.inst-phone p { margin: 4px 0; }
.inst-phone-ok { border-left-color: #2e7d32; }
.inst-phone-warn { border-left-color: #c62828; background: rgba(198, 40, 40, 0.05); }
.inst-phone-ack { display: flex; align-items: flex-start; gap: 8px; flex-basis: 100%; margin: 4px 0; }
.inst-phone-ack input[type="checkbox"] { margin-top: 3px; flex: none; }
.inst-phone-ack span { font-size: 0.9rem; }
.nl-generators { display: grid; gap: 10px; }

/* ── P1 · Modern chrome re-skin (register: reg-modern) ─────────────────────
   Restyles the app shell to the MODERN register (GAMIFICATION_USAGE_PLAN §1.3;
   src-10 UI kit + id-authenticator UI language). Purely visual, no markup or
   logic change. Everything is scoped to `body.reg-modern` so classic/print
   surfaces (id_card, certificate, transcript, letters) are untouched. Colours
   flow from the modern tokens (tokens-modern.css); the neon-cyan --accent is
   already inherited, so links, focus rings, active nav and badges pick it up. */

/* Header bar: a cyan hairline glow under the sticky nav. */
body.reg-modern .topnav {
  border-bottom-color: color-mix(in srgb, var(--m-cyan) 32%, var(--border));
  box-shadow: 0 1px 0 color-mix(in srgb, var(--m-cyan) 20%, transparent);
}

/* Footer: matching cyan hairline on top. */
body.reg-modern .site-footer {
  border-top-color: color-mix(in srgb, var(--m-cyan) 24%, var(--border));
}

/* Primary buttons: cyan→violet gradient. The raw cyan is too light to carry
   white text at AA, so the gradient starts at the deep cyan and resolves into
   the brand violet — high contrast and on-register (both modern accents). */
body.reg-modern button.primary,
body.reg-modern .btn-primary {
  background: linear-gradient(135deg, var(--m-cyan-deep), var(--m-violet));
  border-color: var(--m-violet);
  color: #fff;
}
body.reg-modern button.primary:hover,
body.reg-modern .btn-primary:hover {
  filter: brightness(1.1);
  box-shadow: 0 4px 16px color-mix(in srgb, var(--m-violet) 45%, transparent);
}

/* Flash / notification boxes: HUD-style tinted panels (src-10 alert kit) —
   a 4px semantic left rail over a faint same-hue wash. */
body.reg-modern .flash {
  border-left-width: 4px;
  background: color-mix(in srgb, var(--m-panel) 45%, var(--bg-elev));
}
body.reg-modern .flash-info {
  border-left-color: var(--m-cyan);
  background: color-mix(in srgb, var(--m-cyan) 8%, var(--bg-elev));
}
body.reg-modern .flash-success {
  background: color-mix(in srgb, var(--ok) 9%, var(--bg-elev));
}
body.reg-modern .flash-error {
  background: color-mix(in srgb, var(--danger) 9%, var(--bg-elev));
}

/* ── P21 · Classic chrome re-skin (register: reg-classic) ──────────────────
   The counterpart to the modern chrome block above, for the CLASSIC register
   (old-fashion / field-heraldic: gold–navy–parchment, engraved serif display).
   Colours flow from the classic tokens (tokens-classic.css) which also repoint
   --accent to heraldic gold, so links / focus rings / active nav pick it up.
   Purely visual, scoped to `body.reg-classic`; modern surfaces are untouched.
   Theme-aware — every colour is a token or a color-mix over the shared theme
   tokens, so light + dark both render correctly. */

/* Header bar: a gold hairline + faint gold glow under the sticky nav (the
   engraved-plate look), replacing the modern cyan glow. */
body.reg-classic .topnav {
  border-bottom-color: color-mix(in srgb, var(--c-gold) 42%, var(--border));
  box-shadow: 0 1px 0 color-mix(in srgb, var(--c-gold) 26%, transparent);
}
/* Footer: matching gold hairline on top. */
body.reg-classic .site-footer {
  border-top-color: color-mix(in srgb, var(--c-gold) 34%, var(--border));
}
/* Brand wordmark accent: gold on the classic register (was the modern cyan). */
body.reg-classic .brand-text-accent {
  color: var(--c-gold);
}

/* Primary buttons: a wax-seal red→gold heraldic gradient with a gold rim. */
body.reg-classic button.primary,
body.reg-classic .btn-primary {
  background: linear-gradient(135deg, var(--c-red), var(--c-gold-deep));
  border-color: var(--c-gold);
  color: #fff;
}
body.reg-classic button.primary:hover,
body.reg-classic .btn-primary:hover {
  filter: brightness(1.08);
  box-shadow: 0 4px 16px color-mix(in srgb, var(--c-gold-deep) 45%, transparent);
}

/* Flash / notification boxes: a parchment-tinted panel with a gold left rail —
   the "official notice" look, mirroring the modern HUD panels structurally. */
body.reg-classic .flash {
  border-left-width: 4px;
  border-left-color: var(--c-gold-deep);
  background: color-mix(in srgb, var(--c-parchment) 10%, var(--bg-elev));
}
body.reg-classic .flash-info {
  border-left-color: var(--c-gold-deep);
  background: color-mix(in srgb, var(--c-gold) 8%, var(--bg-elev));
}
body.reg-classic .flash-success {
  border-left-color: var(--c-green);
  background: color-mix(in srgb, var(--ok) 9%, var(--bg-elev));
}
body.reg-classic .flash-error {
  border-left-color: var(--c-red);
  background: color-mix(in srgb, var(--danger) 9%, var(--bg-elev));
}

/* Cards get a subtle double-rule gold inner border — the framed-certificate
   motif — without changing layout (inset box-shadow only). */
body.reg-classic .cat-card,
body.reg-classic .ov-card,
body.reg-classic .lib-card {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c-gold) 18%, transparent);
}

/* =========================================================================
   Catalogue · Guide · Overview · Path infographics (GAMIFICATION_USAGE_PLAN
   P8 + P9). Theme-aware (uses the shared --bg/--fg/--border tokens + the
   modern --m-* accents); animation is gated behind prefers-reduced-motion.
   CSP-safe: styling only, all behaviour lives in portal.js.
   ========================================================================= */

/* Shared button skins (base layout — .btn-primary colour is set above). */
.btn-primary,
.btn-secondary,
.btn-tertiary {
  display: inline-block;
  padding: 0.5rem 1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  color: var(--fg);
  background: var(--bg-elev);
  transition: filter 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.btn-secondary:hover,
.btn-tertiary:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-elev)); }

/* Per-field password reveal (task #115): an eye toggle overlaid at the right
   edge of each password box. The input reserves padding so its text never runs
   under the icon; the icon swaps eye <-> eye-off on aria-pressed. */
.pw-field { position: relative; display: block; }
.pw-field .js-pw { width: 100%; box-sizing: border-box; padding-right: 2.8rem; }
.pw-eye {
  position: absolute;
  top: 50%;
  right: 0.35rem;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--muted, var(--fg));
  cursor: pointer;
}
.pw-eye:hover { color: var(--accent); }
.pw-eye:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pw-eye .icon-eye-off { display: none; }
.pw-eye[aria-pressed="true"] .icon-eye { display: none; }
.pw-eye[aria-pressed="true"] .icon-eye-off { display: inline; }
.btn-tertiary { background: transparent; }
.btn-primary { color: #fff; }
.btn-primary:disabled,
.btn-secondary:disabled { opacity: 0.55; cursor: not-allowed; filter: grayscale(0.3); }

/* ---- Catalogue ---------------------------------------------------------- */
.cat-page, .guide-page, .ov-page, .admin-cat { margin: 1.5rem auto 3rem; }
.cat-head, .guide-head, .ov-head, .admin-cat-head { margin-bottom: 1.5rem; }
.cat-title, .guide-title, .ov-title { margin: 0 0 0.4rem; font-size: 1.9rem; }
.cat-intro, .guide-intro, .ov-intro { color: var(--muted); max-width: 62ch; }
.cat-actions { margin-top: 0.8rem; }

.cat-grid, .ov-grid {
  list-style: none; padding: 0; margin: 0;
  display: grid; gap: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.cat-card, .ov-card {
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--bg-elev); padding: 1.1rem 1.2rem 1.2rem;
  display: flex; flex-direction: column; gap: 0.7rem;
}
.cat-card.is-doctoral, .ov-card.is-doctoral {
  border-color: color-mix(in srgb, var(--m-violet, #7b2fff) 40%, var(--border));
}
/* Cards are clean/text-only (catalogue rework 2026-07-13): no herald/emblem
   art on a card — the status chip right-aligns alone on the top row. */
.cat-card-top, .ov-card-top { display: flex; align-items: center; justify-content: flex-end; gap: 0.6rem; }
.cat-card-title, .ov-card-title { margin: 0; font-size: 1.15rem; }
/* Whole-card click target (catalogue only): the title link stretches over the
   card; links/CTAs below sit above it so they stay clickable themselves. */
.cat-card { position: relative; }
.cat-card-link { color: inherit; text-decoration: none; }
.cat-card .cat-card-link::after { content: ""; position: absolute; inset: 0; border-radius: 14px; }
.cat-card:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.cat-card-links, .cat-card-foot { position: relative; z-index: 1; }
.cat-card-desc { margin: 0; font-size: 0.88rem; line-height: 1.45; }
.cat-card-links { margin: 0; font-size: 0.82rem; color: var(--muted); }
.cat-ladder { margin-top: 2.2rem; padding-top: 1.4rem; border-top: 1px solid var(--border); }
.cat-card-sector, .ov-card-sector { margin: 0; color: var(--muted); font-size: 0.85rem; }
.cat-vacancy-btn { padding: 2px 8px; font-size: 0.85rem; }
/* Enrollment price badge (CATALOGUE_LIFECYCLE_ENQUIRY_SPEC §11). Theme-aware
   via existing tokens: strikethrough original muted, discounted prominent, the
   percentage-off pill accented; FREE reads as a success chip. */
.cat-price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
  font-size: 0.95rem;
}
.cat-price-caption {
  color: var(--muted);
  font-size: 0.82rem;
}
.cat-price-was {
  color: var(--muted);
  text-decoration: line-through;
  font-size: 0.85rem;
}
.cat-price-now {
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--fg);
}
.cat-price-off {
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
}
.cat-price-free .cat-price-free,
.cat-price-free {
  color: var(--ok);
}
.cat-price-free .cat-price-free {
  font-weight: 800;
  letter-spacing: 0.04em;
  padding: 1px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 40%, var(--border));
}
/* Admin catalogue pricing cell: preview badge + the two number fields. */
.admin-cat-price-preview { margin-bottom: 0.4rem; }
.admin-cat-price-form { display: flex; flex-direction: column; gap: 0.35rem; }
.admin-cat-price-field { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.78rem; color: var(--muted); }
.admin-cat-price-field input { width: 8.5rem; }
/* Admin-only visibility of a hidden program (§2): dimmed, dashed outline. */
.is-hidden-admin { opacity: 0.62; outline: 1px dashed var(--border); outline-offset: 3px; }
/* Bootcamp Level-1 card (§1). */
.cat-bootcamp {
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  border-radius: 14px;
  padding: 1rem 1.2rem;
  margin: 0.6rem 0 1rem;
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
/* The bootcamp's own engraved crest (BOOTCAMP_TRACK_2026-07-20): a small emblem
   floated to the card's top-right, like the tier heading emblems. */
.cat-bootcamp-emblem {
  float: right;
  height: 48px;
  width: 48px;
  object-fit: contain;
  margin: 0 0 0.3rem 0.6rem;
}
.cat-bootcamp-tag {
  margin: 0 0 0.25rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--accent);
}
.cat-bootcamp-title { margin: 0 0 0.35rem; font-size: 1.1rem; }
.cat-bootcamp-desc { margin: 0 0 0.4rem; font-size: 0.9rem; line-height: 1.45; }
.cat-bootcamp-for { margin: 0; font-size: 0.85rem; color: var(--muted); }
.cat-bootcamp-cta { margin: 0.6rem 0 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* On-ramp bootcamp track (BOOTCAMP_TRACK_2026-07-20): a responsive grid of
   enrollable bootcamp cards, each keeping the single-card look above. */
.cat-bootcamp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
  gap: 0.8rem;
  margin: 0.6rem 0 1rem;
}
.cat-bootcamp-grid .cat-bootcamp { margin: 0; display: flex; flex-direction: column; }
.cat-bootcamp-grid .cat-bootcamp-cta { margin-top: auto; }
.cat-card-companion { margin: 0.35rem 0 0; font-size: 0.82rem; color: var(--muted); }
/* Course length (catalogue & levels rework 2026-07-26, part 4) — a plain fact
   line on the card, same register as the credits/language lines. */
.cat-card-length, .cat-bootcamp-length { margin: 0.25rem 0 0; font-size: 0.85rem; }
.cat-bootcamp-length { margin: 0 0 0.25rem; }
/* Card meta-field lines (Path type · Requirements · Language · Cortex Credits):
   single-spaced (CATALOGUE_PATHS_2026-07-27 part 3). These <p> rows carried the
   UA default 1em block margin, stacking into a doubled gap; a tight uniform
   top-margin gives one consistent line rhythm, still readable. */
.cat-card-path, .cat-card-reqs, .cat-card-lang, .cat-card-credits,
.cat-bootcamp-lang { margin: 0.25rem 0 0; font-size: 0.85rem; }
.cat-card-reqs a, .cat-card-path a { color: var(--accent); }
/* Content-language availability as plain TEXT, never a badge/chip (rework part
   5 — revises the 14abd93 badge treatment). No pill, no border: it reads as
   ordinary copy next to its label. */
.cat-lang-text { font-weight: 600; }
/* Card action area: a flex row rather than a single hard-wired button, so a
   second CTA (the upcoming per-program "Syllabus" button) can join the Apply
   CTA without restructuring the card foot. */
.cat-card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.cat-card-actions .cat-retired-note { margin: 0; }
/* One-click apply (ENROLMENT_LEVELS_PROFILE_2026-07-27 §2). The disabled state
   is a REAL ``disabled`` attribute (announced, not merely greyed): this only
   makes it look the part. ``not(:disabled)`` keeps the live button untouched. */
.cat-apply-btn[disabled] { opacity: 0.62; cursor: default; }
.cat-apply-btn[disabled]:hover { transform: none; }
/* The gate's "why not" (e.g. "Requires M.Sc") beside the disabled button —
   wraps onto its own line on a narrow card rather than squeezing the buttons. */
.cat-apply-reason { flex: 1 1 100%; margin: 0; font-size: 0.8rem; line-height: 1.4; }
/* Onward link from the detail page's syllabus row to the programme's own page. */
.catd-syllabus-link { margin: 0.5rem 0 0; font-size: 0.88rem; }

/* ---- Per-programme syllabus page (/catalogue/{slug}/syllabus) ------------ */
/* One page per programme — never a combined list. Headlines only. */
.syl-head {
  display: flex; flex-wrap: wrap; gap: 1.2rem; align-items: flex-start;
  margin: 0 0 1.4rem; padding-bottom: 1.1rem; border-bottom: 1px solid var(--border);
}
.syl-herald { flex: 0 0 auto; width: 96px; height: 96px; object-fit: contain; }
.syl-head-text { flex: 1 1 24rem; min-width: min(100%, 18rem); }
.syl-title { margin: 0.15rem 0 0.35rem; font-size: 1.6rem; line-height: 1.2; }
.syl-sub { margin: 0 0 0.6rem; font-size: 0.9rem; color: var(--muted); }
.syl-kicker { font-weight: 700; color: var(--fg); }
.syl-intro { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.55; max-width: 68ch; }
.syl-links { margin: 0; font-size: 0.88rem; }
/* Tier switcher — rendered ONLY for a programme offering more than one rung
   under one slug (MSc / Ph.D / PD·Fellow …). Each rung teaches its own span, so
   these are different documents, not filters of one. */
.syl-tiers {
  display: flex; flex-wrap: wrap; gap: 0.35rem 0.5rem; align-items: baseline;
  margin: 0 0 0.55rem; font-size: 0.82rem;
}
.syl-tiers-label { color: var(--muted); }
.syl-tier {
  padding: 0.15rem 0.55rem; border: 1px solid var(--border); border-radius: 999px;
  text-decoration: none; line-height: 1.5;
}
.syl-tier.is-current { background: var(--bg-elev); font-weight: 700; color: var(--fg); }
/* Markers are suppressed because each row prints the programme's REAL unit
   number (phase-00 vs unit 1), which an auto-incrementing marker contradicts. */
.syl-list { list-style: none; margin: 0; padding: 0; counter-reset: none; }
.syl-unit {
  display: flex; flex-wrap: wrap; gap: 0.15rem 0.9rem; align-items: baseline;
  padding: 0.5rem 0.2rem; border-bottom: 1px solid var(--border);
}
.syl-unit:last-child { border-bottom: 0; }
.syl-unit-n {
  flex: 0 0 7.5rem; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--muted);
}
.syl-unit-title { flex: 1 1 20rem; min-width: min(100%, 16rem); line-height: 1.5; }
.syl-unit-title code {
  font-size: 0.92em; padding: 0.05em 0.32em; border-radius: 5px;
  background: var(--bg-elev); border: 1px solid var(--border);
}
.syl-pending {
  margin: 0; padding: 1rem 1.2rem; border: 1px solid var(--border); border-radius: 14px;
  background: var(--bg-elev); font-size: 0.92rem; line-height: 1.55; max-width: 68ch;
}
/* Post-doctoral portfolio panel (2026-08-09 dead-end fix, part b): the rung has
   no unit list, so the panel states what IS on record as a label/value ledger.
   It shipped with its classes defined nowhere, which left the `<dl>` on UA
   defaults: `dd { margin-left: 40px }`, and — the real defect — NO margin
   between fact groups while the `.cat-label` `dt` keeps its own 0.35rem bottom
   margin. Every label therefore sat closer to the PREVIOUS value than to its
   own, so proximity read the grouping backwards. Fixed here rather than in a
   register skin because the panel is undressed in BOTH registers.
   Each fact is a STACKED pair — label, its value, then a hairline before the next
   group — rather than the side-by-side column `.syl-unit` uses. Two measurements
   decided that: the panel lives inside `.syl-pending`, whose 68ch cap is 563px in
   classic against `.syl-unit`'s full 1043px, and the classic skin letter-spaces
   `.cat-label` at 0.2em — so a label column wide enough for "ADMISSION /
   ACCEPTANCE" takes over half the panel, and measured at 1280 the values wrapped
   under their labels regardless. Stacked is also the rhythm `.catd-fact` already
   uses for this exact shape, and it is immune to label length, which matters
   with the Spanish labels being the longer set.
   The ratio is the point: 0.2rem from a label to ITS value, against ~1.15rem
   plus a rule to the next group. */
.syl-portfolio-lead { margin: 0 0 0.9rem; }
.syl-portfolio-facts { margin: 0; }
.syl-portfolio-fact {
  padding: 0.55rem 0 0.6rem;
  border-bottom: 1px solid var(--border);
}
.syl-portfolio-fact:last-child { border-bottom: 0; padding-bottom: 0; }
.syl-portfolio-fact dt { margin: 0 0 0.2rem; }
/* Kills the UA's `dd { margin-left: 40px }`, which indented every value away
   from the label it belongs to. */
.syl-portfolio-fact dd { margin: 0; line-height: 1.5; }
.syl-portfolio-note { margin: 0.9rem 0 0; }
/* Wins over `.syl-tiers`' own margin on equal specificity by source order. */
.syl-portfolio-links { margin: 0.9rem 0 0; }

/* ---- Level 0: recommended prior education (rework part 3) ---------------- */
/* Level 0 holds NO programme — it explains what a Bachelor's degree and an HND
   are and why holding one (or equivalent knowledge) is recommended. */
/* `.cat-level1-boost` (Level 1 "boost your preparation", backlog G4) joins the
   rule rather than getting its own: its template comment already claims it
   "reuses the L0 figure styling", but it only ever reused `-heading` and
   `-figure` — the frame itself was never defined anywhere, so the panel
   rendered with `border: 0 none`, no ground and no padding, leaving the heading
   outside any chrome. Same panel, same frame, one selector list. */
.cat-prior-ed,
.cat-level1-boost {
  margin: 0.6rem 0 1rem;
  padding: 1rem 1.2rem;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg-elev);
}
/* In L0 the figure is a child of `.cat-prior-ed-art`, a `flex: 0 0 auto` column
   that shrink-wraps it. The boost panel has no text column, so it has no `-art`
   wrapper either and the figure was a full-width block — the crest centred in
   ~1043px of dead space with its 18ch caption under it. `fit-content` restores
   the shrink-wrap L0 gets from its parent, so the crest sits over its caption at
   the panel's text edge, directly under the heading. */
.cat-level1-boost .cat-prior-ed-figure { width: fit-content; }
/* …which shrink-wrapped the figure but left it stacked, and the L0 caption is
   capped at `max-width: 18ch` because in L0 it sits UNDER a badge in a narrow
   `-art` column. Reused here it set a ~125px ribbon of text below the crest
   with ~900px of the panel empty beside it — the whole point of the panel
   (what the bootcamps are for) reduced to the least readable line on the page.
   The boost panel has no text column to sit beside, so the figure becomes the
   row: crest first, caption alongside at prose measure, centred on it. Scoped
   to `.cat-level1-boost` — L0's own stacked badges are untouched. */
.cat-level1-boost .cat-prior-ed-figure {
  flex-direction: row;
  align-items: center;
  gap: var(--space);
  max-width: 100%;
}
.cat-level1-boost .cat-prior-ed-caption {
  max-width: 60ch;
  text-align: left;
  font-size: 0.86rem;
}
.cat-prior-ed-heading { margin: 0 0 0.5rem; font-size: 1.05rem; }
.cat-prior-ed-body { display: flex; flex-wrap: wrap; gap: 1.2rem; align-items: flex-start; }
.cat-prior-ed-text { flex: 1 1 22rem; min-width: min(100%, 18rem); }
.cat-prior-ed-intro { margin: 0 0 0.7rem; font-size: 0.9rem; line-height: 1.55; max-width: 68ch; }
.cat-prior-ed-list { margin: 0; }
.cat-prior-ed-term { margin: 0.6rem 0 0.15rem; font-weight: 700; font-size: 0.92rem; }
.cat-prior-ed-term:first-child { margin-top: 0; }
.cat-prior-ed-def {
  margin: 0; font-size: 0.88rem; line-height: 1.55; color: var(--muted); max-width: 68ch;
}
.cat-prior-ed-link { white-space: nowrap; }
.cat-prior-ed-art {
  flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;
}
.cat-prior-ed-figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
/* L0 prior-education badges — equal size (CATALOGUE_PATHS_2026-07-27 part 1).
   The bootcamp emblem and the historical engraving previously rendered at
   different sizes (96x96 vs 128x192); both now share ONE common 96x96 box, the
   smaller (bootcamp) baseline — no badge sits with the Bachelor's text explainer,
   so the smaller footprint is the reference. object-fit: contain preserves each
   image's aspect ratio inside the shared box (the portrait engraving letterboxes
   rather than distorting). Alt text unchanged in the template. */
.cat-prior-ed-bootcamp,
.cat-prior-ed-historical {
  width: 96px; height: 96px; object-fit: contain;
}
.cat-prior-ed-historical { border-radius: 6px; }
.cat-prior-ed-caption {
  margin: 0; max-width: 18ch; text-align: center; font-size: 0.74rem; color: var(--muted); line-height: 1.4;
}

/* Level sections L0–L10 (LEVELS_CATALOGUE_GUIDE_REWORK §A/§C, addendum (b)) */
.cat-level { margin: 1.8rem 0 0; padding-top: 1.1rem; border-top: 1px solid var(--border); }
.cat-level-head { margin: 0 0 0.7rem; font-size: 1.25rem; display: flex; align-items: center; gap: 0.55rem; }
/* Classic tier emblem beside the heading — uniform height, auto width (the
   medallions are not square, so never force a square that would distort). */
.cat-level-emblem { height: 40px; width: auto; max-width: 64px; object-fit: contain; flex: 0 0 auto; }
/* No max-width cap: let these blocks span the full .cat-level container width,
   matching the card grid width below them (Borja 2026-08-02). */
.cat-level-note {
  margin: 0 0 0.4rem; color: var(--muted); font-size: 0.9rem;
  padding: 0.6rem 0.9rem; border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent); border-radius: 4px;
}
.cat-level-howto { margin: 0 0 0.6rem; color: var(--fg); font-size: 0.92rem; line-height: 1.55; }
.cat-level-cta { margin: 0 0 0.4rem; display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* Honorary L8–L10 light outline (PORTAL_POLISH_2026-07-27 §G9) — a skeletal
   definition list giving the conferred levels structural parity with the lower
   levels' cards, minus enrolment. */
/* No max-width cap: aligns with the card grid width (Borja 2026-08-02). */
.cat-honorary-detail { margin: 0.2rem 0 0.4rem; }
.cat-honorary-heading { margin: 0 0 0.4rem; font-size: 0.95rem; }
.cat-honorary-list { margin: 0; }
.cat-honorary-term { margin: 0.55rem 0 0.15rem; font-weight: 700; font-size: 0.9rem; }
.cat-honorary-term:first-child { margin-top: 0; }
/* No max-width cap: aligns with the card grid width (Borja 2026-08-02). */
.cat-honorary-def {
  margin: 0; font-size: 0.88rem; line-height: 1.55; color: var(--muted);
}
.cat-honorary-note { margin: 0.5rem 0 0; font-size: 0.84rem; }
.cat-card-level {
  margin: 0; font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
}

.cat-status {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.15rem 0.5rem; border-radius: 999px; white-space: nowrap;
}
.cat-status-live { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.cat-status-coming_soon { background: color-mix(in srgb, var(--muted) 20%, transparent); color: var(--muted); }

.cat-section { margin-top: 0.2rem; }
.cat-label, .ov-path-heading {
  margin: 0 0 0.35rem; font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
}
.cat-label-inline { color: var(--muted); font-size: 0.8rem; }
.cat-tiers, .cat-grades { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.cat-tier, .cat-grade {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.2rem 0.5rem; border: 1px solid var(--border); border-radius: 999px; font-size: 0.8rem;
}
.cat-prereq-body { margin: 0; font-size: 0.85rem; }
.cat-prereq-note { display: block; color: var(--muted); font-size: 0.76rem; margin-top: 0.2rem; }
.cat-card-foot, .ov-card-foot {
  margin-top: auto; padding-top: 0.7rem; border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap;
}
.cat-professor, .ov-professor { margin: 0; font-size: 0.85rem; }

/* ---- Catalogue detail (one program × tier) ------------------------------ */
.catd-page { max-width: 720px; }
.catd-back { margin: 0 0 1rem; }
.catd-head {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 1.1rem 1.2rem; border: 1px solid var(--border); border-radius: 14px;
  background: var(--bg-elev);
}
.catd-head.is-doctoral {
  border-color: color-mix(in srgb, var(--m-violet, #7b2fff) 40%, var(--border));
}
/* The discipline herald — the ONE catalogue surface it renders on (catalogue
   rework 2026-07-13): a proper, larger emblem in a framed panel beside the
   programme facts. Theme-aware via the shared tokens; scales down politely. */
.catd-herald {
  margin: 0; flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 0.45rem;
  padding: 0.8rem 0.9rem; border: 1px solid var(--border); border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 5%, var(--bg));
}
.catd-herald img {
  width: 128px; height: 128px; max-width: 128px; max-height: 128px;
  object-fit: contain; border-radius: 10px;
}
.catd-herald-caption {
  font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted); text-align: center;
}
@media (max-width: 560px) {
  .catd-head { flex-direction: column; align-items: flex-start; }
  .catd-herald { align-self: center; }
  .catd-herald img { width: 104px; height: 104px; }
}
.catd-title { margin: 0 0 0.25rem; font-size: 1.45rem; }
/* Detail facts list — single-spaced (CATALOGUE_PATHS_2026-07-27 part 3): the
   inter-box gap and the per-row label→value margin were stacking into a doubled
   rhythm; a tighter grid gap plus a zero top-margin on the value gives one clean
   line of separation, still readable. */
.catd-facts { margin: 1.2rem 0 0; display: grid; gap: 0.55rem; }
.catd-fact {
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg-elev); padding: 0.7rem 1rem;
}
.catd-fact .cat-label { margin: 0; }
.catd-fact-body { margin: 0.2rem 0 0; font-size: 0.95rem; }
.catd-syllabus summary { cursor: pointer; }
.catd-syllabus-list { margin: 0.6rem 0 0; padding-left: 2.2rem; font-size: 0.88rem; }
.catd-syllabus-list li { margin: 0.15rem 0; }
.catd-prereqs { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.3rem; }
.catd-foot { margin-top: 1.2rem; border-top: 0; }

/* ---- Path overview map (CATALOGUE_PATHS_2026-07-27 §5) ------------------- */
/* One inline SVG generated from catalog.py (programs/paths.py). Geometry lives
   in the SVG; every colour is a CSS class here so both themes stay token-driven
   and the full-screen clone the lightbox opens inherits identical styling. */
.pm-figure { margin: 1.3rem 0 0; }
/* Wide diagram scrolls inside its own container — the page body never h-scrolls. */
.pm-scroll { overflow-x: auto; }
.pm-svg { width: 100%; height: auto; display: block; font-family: var(--font-body); }
.pm-caption { margin: 0.55rem 0 0; text-align: center; color: var(--muted); font-size: 0.85rem; }
.pm-box rect { fill: var(--bg-elev); stroke: var(--border); stroke-width: 1.5; }
.pm-box-base rect {
  fill: color-mix(in srgb, var(--accent) 9%, var(--bg-elev));
  stroke: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.pm-box-doctoral rect {
  fill: color-mix(in srgb, var(--m-violet, #7b2fff) 11%, var(--bg-elev));
  stroke: color-mix(in srgb, var(--m-violet, #7b2fff) 45%, var(--border));
}
.pm-box-apex rect {
  fill: color-mix(in srgb, var(--m-violet, #7b2fff) 20%, var(--bg-elev));
  stroke: color-mix(in srgb, var(--m-violet, #7b2fff) 72%, var(--border));
  stroke-width: 2;
}
.pm-box-title { fill: var(--fg); font-size: 11px; font-weight: 600; }
.pm-box-range { fill: var(--muted); font-size: 9.5px; font-weight: 700; letter-spacing: 0.03em; }
.pm-path-head { fill: var(--fg); font-size: 15px; font-weight: 800; }
.pm-ceiling {
  fill: var(--muted); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.pm-legend { fill: var(--muted); font-size: 11px; }
.pm-arrow { stroke: var(--muted); stroke-width: 1.8; }
.pm-arrowhead { fill: var(--muted); }
/* Text legend beneath the map — colour is never the only channel (a11y). */
.pm-legend-list {
  list-style: none; padding: 0; margin: 1rem 0 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.3rem;
  font-size: 0.85rem; color: var(--muted);
}
.pm-legend-list li { display: flex; align-items: center; gap: 0.45rem; }
.pm-swatch {
  width: 14px; height: 14px; border-radius: 4px;
  border: 1px solid var(--border); display: inline-block; flex: 0 0 auto;
}
/* Level swatches mirror the map's own fixed palette (the SVG does not
   re-theme, so neither do these). */
.pm-swatch-l0 { background: #e0f2fe; }
.pm-swatch-l1 { background: #0e7490; }
.pm-swatch-l2 { background: #1e40af; }
.pm-swatch-l3 { background: #6d28d9; }
.pm-swatch-postdoc { background: #15803d; }
.pm-swatch-honours { background: #b45309; }

/* ---- Guide -------------------------------------------------------------- */
.guide-section { margin: 2rem 0; padding-top: 1.2rem; border-top: 1px solid var(--border); }
.guide-h2 { font-size: 1.35rem; margin: 0 0 0.6rem; }
.guide-body { color: var(--muted); max-width: 68ch; }
.guide-clause {
  margin-top: 0.8rem; padding: 0.8rem 1rem; border-left: 4px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, var(--bg-elev)); border-radius: 6px; font-size: 0.9rem;
}
.guide-matrix-wrap { overflow-x: auto; }
.guide-matrix, .guide-acronyms, .guide-journey, .admin-cat-table {
  width: 100%; border-collapse: collapse; margin-top: 0.6rem; min-width: 380px;
}
.guide-matrix th, .guide-matrix td,
.guide-acronyms th, .guide-acronyms td,
.guide-journey th, .guide-journey td,
.admin-cat-table th, .admin-cat-table td {
  border: 1px solid var(--border); padding: 0.5rem 0.7rem; text-align: left; vertical-align: middle;
}
.guide-matrix thead th, .guide-acronyms thead th, .guide-journey thead th, .admin-cat-table thead th {
  background: var(--bg-elev); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em;
}
.guide-journey td { font-size: 0.88rem; }
.guide-acronym-sym { font-family: var(--m-font-body, monospace); font-weight: 700; white-space: nowrap; }
.guide-band-word { font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; color: var(--muted); }
.guide-h3 { font-size: 1.05rem; margin: 1.2rem 0 0.4rem; }
.guide-delta-note { margin-top: 0.8rem; font-size: 0.88rem; }
/* One-by-one explainers (rework §F): attribute / herald / badge rows. */
.guide-attr-list, .guide-emblem-list { list-style: none; padding: 0; margin: 0.6rem 0 0;
  display: grid; gap: 0.7rem; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.guide-attr-item, .guide-emblem-item {
  display: flex; align-items: flex-start; gap: 0.7rem;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg-elev); padding: 0.7rem 0.8rem;
}
.guide-attr-item img, .guide-emblem-item img {
  border-radius: 8px; flex: 0 0 auto; object-fit: contain;
}
.guide-item-name { margin: 0 0 0.15rem; font-size: 0.92rem; }
.guide-item-desc { margin: 0; font-size: 0.8rem; line-height: 1.4; color: var(--muted); }
/* Supreme-honorary crest showcase (L8 → L9 → L10, own coat of arms each,
   grander as they climb) — P17 UX pass (2026-07-17): same card language as
   the herald/badge "one by one" rows above (bordered rectangle, bg-elev,
   text inside the box) instead of floating art, so the section reads as one
   consistent system. Crests now carry transparent-background art (audit
   docs/audits/EMBLEM_AUDIT.md), so no radial-gradient backdrop is needed.
   Each card fixes the SAME image-slot height; the Special Honorific Seat
   (L10) art is natively landscape, so it simply fills that slot wider —
   no special-casing needed, and no crest is oversized. */
.guide-crest-showcase { list-style: none; padding: 0; margin: 0.9rem 0 1.2rem;
  display: grid; gap: 0.7rem; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.guide-crest-item { display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: 0.3rem;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg-elev); padding: 0.7rem 0.8rem; }
.guide-crest-img-wrap { display: flex; align-items: center; justify-content: center;
  width: 100%; height: 96px; }
/* L10 Special Honorific Seat = the show-off apex: a taller image slot makes its
   1.5-aspect art render widest (L8 ~1:1 → ~102px < L9 1.5 → ~144px < L10 → ~192px),
   so its card is intentionally the grandest of the three. */
.guide-crest-item--l10 .guide-crest-img-wrap { height: 128px; }
.guide-crest-img { max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; }
.guide-crest-level { margin: 0.15rem 0 0; font-size: 0.72rem; font-weight: 700;
  /* Was --metal-gold-lo (#a07818): 3.80:1 on the light ground, and the metal
     ramp is for rendering METAL, not for text somebody has to read at 0.72rem. */
  text-transform: uppercase; letter-spacing: 0.09em; color: var(--gold-text); }
/* Same ground-not-theme carve-out as the About gold inks above: the pale
   --metal-gold-hi is a lift for a dark plate, and on the classic register's
   cream paper it measured 1.36:1. */
:root[data-theme="dark"]:not([data-ui="classic"]) .guide-crest-level { color: var(--metal-gold-hi); }
.guide-crest-item .guide-item-name { margin: 0; font-size: 0.92rem; }

/* ---- Overview ----------------------------------------------------------- */
.ov-meter { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.8rem; }
.ov-meter-bar { display: inline-flex; gap: 0.25rem; }
.ov-slot { width: 34px; height: 8px; border-radius: 999px; background: var(--border); }
.ov-slot.is-used { background: linear-gradient(90deg, var(--m-cyan-deep, #0099cc), var(--m-violet, #7b2fff)); }
.ov-meter-text { color: var(--muted); font-size: 0.82rem; }
.ov-flash { padding: 0.7rem 1rem; border-radius: 8px; margin: 0.8rem 0; }
.ov-flash.ok { background: color-mix(in srgb, var(--ok) 12%, var(--bg-elev)); border: 1px solid var(--ok); }
.ov-flash.err { background: color-mix(in srgb, var(--danger) 12%, var(--bg-elev)); border: 1px solid var(--danger); }
.ov-advisory { margin: 0; font-size: 0.8rem; color: var(--muted);
  padding: 0.4rem 0.6rem; border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent); border-radius: 4px; }
.ov-request-form { margin: 0; }
.ov-guide-link { margin-top: 0.8rem; }
/* Overview programme table (rework §J): one simple row per program. */
.ov-table-wrap { overflow-x: auto; }
.ov-table { width: 100%; border-collapse: collapse; margin-top: 0.6rem; min-width: 560px; }
.ov-table th, .ov-table td {
  border: 1px solid var(--border); padding: 0.55rem 0.7rem; text-align: left; vertical-align: middle;
}
.ov-table thead th {
  background: var(--bg-elev); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em;
}
.ov-row-title a { font-weight: 600; color: inherit; }
.ov-row-title .ov-advisory { display: block; margin-top: 0.35rem; }
.ov-row-levels { font-size: 0.85rem; white-space: nowrap; }
.ov-row-professor { font-size: 0.88rem; }
.ov-row-action .btn-primary, .ov-row-action .btn-secondary { white-space: nowrap; }

/* ---- Path infographic --------------------------------------------------- */
.path-track { margin: 0.6rem 0 0; }
.path-track-title { margin: 0 0 0.6rem; font-size: 0.82rem; color: var(--muted); font-weight: 600; }
/* Wrapping ladder: the L0→L10 path flows onto 2-3 centred lines instead of a
   horizontal scrollbar (Borja 2026-07-13) — the rows wrap responsively and
   stay centred at every width. */
.path-nodes { list-style: none; margin: 0; padding: 0.2rem 0; display: flex; align-items: stretch;
  flex-wrap: wrap; justify-content: center; gap: 0.55rem 0.1rem; overflow: visible; }
.path-node-wrap { display: inline-flex; align-items: center; gap: 0.1rem; flex: 0 0 auto; }
.path-link { flex: 0 0 auto; width: 22px; }
.path-link-line { stroke: var(--border); stroke-width: 2; stroke-dasharray: 1;
  stroke-dashoffset: 1; animation: pathDraw 0.6s ease forwards; }
.path-node {
  display: inline-flex; flex-direction: column; align-items: center; gap: 0.2rem;
  min-width: 62px; padding: 0.45rem 0.4rem; border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg-elev); cursor: pointer; font: inherit; color: var(--fg);
  transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.path-node:hover, .path-node:focus-visible {
  border-color: var(--m-cyan, var(--accent)); transform: translateY(-2px);
  box-shadow: 0 4px 14px color-mix(in srgb, var(--m-cyan, #00e5ff) 22%, transparent);
}
/* Keyboard focus needs its own signal. Sharing hover's glow — and cancelling the
   outline with it — left a keyboard user unable to tell focus from a stray
   pointer, on a graph whose whole purpose is navigation. */
.path-node:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.path-node.is-locked { opacity: 0.55; filter: grayscale(0.6); }
.path-node-badge { display: inline-flex; }
/* Level emblem medallions are not square, and must not be distorted (this
   overrides the macro's 46x46 attrs). A FIXED BOX, not a fixed height: the
   previous rule pinned the height and left the width auto, which is not the
   same thing. The eleven level crests are cut at three different aspect
   ratios in the modern set (260×211 for L0–L4, 260×188 for
   L5–L9, 300×189 for L10), so pinning only the height let each group compute
   its own width and the path row rendered at 54px / 61px / 70px — three
   different badge widths in one row of equals. The classic set is square and
   was already uniform, which is why this only ever showed in modern. The box
   is sized off the widest ratio (300×189 → 44px tall needs 69.8px) so no art
   is scaled DOWN by the change; `object-fit: contain` letterboxes the rest. */
.path-node-badge img { width: 70px; height: 44px; object-fit: contain; }
.path-node-label { font-weight: 700; font-size: 0.82rem; }
.path-node-sub { font-size: 0.68rem; color: var(--muted); }

.path-dialog {
  border: 1px solid var(--border); border-radius: 14px; background: var(--bg-elev); color: var(--fg);
  max-width: 460px; width: calc(100% - 2rem); padding: 0;
}
.path-dialog::backdrop { background: rgba(4, 8, 16, 0.62); }
.path-dialog-form { padding: 1.2rem 1.3rem; margin: 0; }
.path-dialog-title { margin: 0 0 0.5rem; font-size: 1.15rem; }
.path-dialog-detail { margin: 0 0 1rem; color: var(--muted); }
.path-dialog-menu { margin: 0; padding: 0; display: flex; justify-content: flex-end; }

@keyframes pathDraw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .path-link-line { animation: none; stroke-dashoffset: 0; }
  .path-node { transition: none; }
  .path-node:hover, .path-node:focus-visible { transform: none; }
}

/* ---- Admin catalogue (professor picker P27 + lifecycle P23/P26) --------- */
.admin-cat-prog code { display: inline-block; margin-top: 0.2rem; }
/* Compact catalogue rows (Borja 2026-07-20): tighter padding, top-aligned
   cells, and the folded version/lifecycle meta line under the programme name. */
.admin-cat-table-compact th,
.admin-cat-table-compact td { padding: 0.5rem 0.7rem; vertical-align: top; }
.admin-cat-meta {
  display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; margin-top: 0.35rem;
}
.admin-cat-lifecycle-label { color: var(--muted); font-size: 0.78rem; }
/* Collapsible per-row pricing editor: the live badge is the summary, the edit
   form only appears when expanded — keeps rows short. */
.admin-cat-price-details > summary {
  cursor: pointer; display: flex; align-items: center; gap: 0.5rem;
  list-style-position: inside; user-select: none;
}
.admin-cat-price-details .admin-cat-price-preview { margin: 0; }
.admin-cat-price-edit {
  color: var(--muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
}
.admin-cat-price-details[open] > summary { margin-bottom: 0.45rem; }
.admin-cat-actions { margin: 0.6rem 0 0; }
.admin-cat-form { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0; }
.admin-cat-form input[type="text"],
.admin-cat-form select { flex: 1 1 220px; min-width: 180px; padding: 0.4rem 0.6rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; }
.admin-cat-eff { color: var(--muted); font-size: 0.8rem; }
.admin-cat-lifecycle-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.admin-cat-inline-form { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0; }
.admin-cat-sync .badge { white-space: nowrap; }

/* Compact catalogue admin table (Borja 2026-07-21; widened + row-shortened
   G2 2026-08-03): smaller row text and tighter, better-aligned inline
   controls so the dense grid reads cleanly. The table now opts into the
   1400px `.table-wide-canvas` (partners idiom, see above), so the toggle
   cells run their badge + save control on ONE line instead of stacking —
   the extra width means they no longer need to wrap to stay legible. */
.admin-cat-table-compact { font-size: 0.8rem; }
.admin-cat-table-compact th,
.admin-cat-table-compact td { padding: 0.4rem 0.6rem; }
.admin-cat-table-compact .btn-primary,
.admin-cat-table-compact .btn-secondary,
.admin-cat-table-compact .btn-tertiary {
  padding: 0.24rem 0.6rem;
  font-size: 0.74rem;
  border-radius: 6px;
}
.admin-cat-table-compact select,
.admin-cat-table-compact input[type="number"] {
  padding: 0.24rem 0.5rem;
  font-size: 0.76rem;
  border-radius: 6px;
}
/* The visibility / enrol toggle cells run the status badge + its save/flip
   control on one row (G2: the 1400px canvas leaves room; no more stacking). */
.admin-cat-toggle {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.admin-cat-toggle .badge { display: inline-flex; margin-bottom: 0; }
.admin-cat-toggle .admin-cat-inline-form { display: flex; }
.admin-cat-onboardable { margin-top: 1.6rem; }
.admin-cat-onboardable-list { list-style: none; margin: 0.6rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.5rem; }
.admin-cat-onboardable-list li { display: flex; align-items: center; justify-content: space-between;
  gap: 0.8rem; border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.8rem;
  background: var(--bg-elev); }

/* ── Email signature generator (P7) ─────────────────────────────────────── */
.emailsig-preview {
  margin: 0.8rem 0 1rem;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: #fff;
  overflow-x: auto;
}
.emailsig-source {
  width: 100%;
  font-family: 'Courier New', monospace;
  font-size: 0.78rem;
  resize: vertical;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--bg-elev);
  color: var(--fg);
}

/* ── State-page illustrations (P9: 404 / suspended / request-pending) ────
   A single, generously-sized illustration above the existing card copy —
   theme-neutral (the source art already reads on light or dark chrome), with
   a soft rounded frame so it doesn't collide with the hard page edges. */
.state-hero {
  display: block;
  width: 100%;
  /* Gate P §7: the request-received hero fills the card (was capped at 480px). */
  max-width: none;
  height: auto;
  margin: 0 auto 1.2rem;
  border-radius: 0.9rem;
  box-shadow: 0 10px 30px rgba(0,0,0,0.35);
}

/* ── Gate P §9: dashboard horizontal phase timeline ─────────────────────── */
.phase-timeline-card { padding-top: 1rem; padding-bottom: 1rem; }
.phase-timeline-head { margin: 0 0 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; }
.phase-timeline {
  list-style: none;
  display: flex;
  gap: 0;
  margin: 0;
  padding: 0.2rem 0 0.4rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.phase-timeline-node {
  position: relative;
  flex: 0 0 auto;
  min-width: 4.2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0 0.35rem;
}
/* The connecting rule between nodes. */
.phase-timeline-node::before {
  content: "";
  position: absolute;
  top: 1.05rem;
  left: -50%;
  width: 100%;
  height: 2px;
  /* Was rgba(255,255,255,0.16) — invisible on the light theme, which is where
     the dashboard timeline stopped reading as a timeline at all. */
  background: var(--border);
  z-index: 0;
}
.phase-timeline-node:first-child::before { display: none; }
.phase-timeline-dot {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
  /* `--surface-2` and `--text` are defined NOWHERE in this stylesheet, the brand
     token files or base.html, so the dark-navy fallbacks always won: on the light
     theme the dashboard timeline was a row of near-black chips with off-white
     numerals sitting on a white card. The real tokens are --bg-elev and --fg. */
  border: 2px solid var(--border);
  background: var(--bg-elev);
  color: var(--fg);
}
a.phase-timeline-dot:hover { border-color: rgba(255, 255, 255, 0.55); }
.phase-timeline-node.is-done .phase-timeline-dot {
  background: #24485f; border-color: #3f7ea0; color: #dcecf6;
}
.phase-timeline-node.is-current .phase-timeline-dot {
  background: #b5892f; border-color: #f0c761; color: #1a1305;
  box-shadow: 0 0 0 3px rgba(240, 199, 97, 0.25);
}
.phase-timeline-node.is-locked .phase-timeline-dot {
  opacity: 0.5; cursor: not-allowed;
}
.phase-timeline-label {
  margin-top: 0.4rem;
  font-size: 0.68rem;
  line-height: 1.15;
  color: var(--muted, #9aa4b8);
  max-width: 5.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.phase-timeline-node.is-current .phase-timeline-label { color: var(--fg); font-weight: 600; }
/* The end-of-path exam node (D6): a distinct accent diamond so the assessment
   reads as the destination of the journey, not another section. */
.phase-timeline-node.is-exam .phase-timeline-exam {
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-elev));
  border-color: var(--accent);
  color: var(--fg);
  border-radius: 22%;
  transform: rotate(45deg);
}
.phase-timeline-node.is-exam .phase-timeline-exam { font-size: 1rem; }
a.phase-timeline-exam:hover { border-color: var(--accent-strong); }

/* ── Phase flow (D6): Read → Quiz → Exam steps on the phase reading page ──── */
.phase-flow { margin: 0 0 var(--space, 1rem); }
.phase-flow-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  align-items: stretch;
}
.phase-flow-step {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-elev);
  font-size: 0.9rem;
}
.phase-flow-step .phase-flow-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--fg);
}
.phase-flow-step .phase-flow-link:hover { color: var(--accent-strong); }
.phase-flow-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-elev));
  border: 1px solid var(--border);
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
.phase-flow-step.is-current { border-color: var(--accent); }
.phase-flow-step.is-current .phase-flow-n {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
.phase-flow-step.is-done .phase-flow-n {
  background: color-mix(in srgb, var(--ok) 22%, var(--bg-elev));
  border-color: var(--ok);
  color: var(--fg);
}
.phase-flow-step.is-exam .phase-flow-n {
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-elev));
  border-radius: 22%;
}

/* ── Gate P §4b: Student Records — Cortex Credits breakdown ──────────────── */
.records-credit-total { font-size: 1.05rem; margin: 0 0 0.8rem; }
.records-credit-program { margin: 0 0 1rem; }
.records-credit-program h3 { margin: 0.4rem 0 0.3rem; font-size: 1rem; }
.records-credit-cc { text-align: right; font-variant-numeric: tabular-nums; width: 6rem; }
.records-credit-subtotal td { border-top: 1px solid rgba(255, 255, 255, 0.14); }

/* ── Splash / hero banners (P9: login bootsplash page, loading, About) ──── */
.splash-hero {
  display: block;
  width: 100%;
  max-height: 260px;
  object-fit: cover;
  border-radius: 0.9rem;
  margin: 0 0 1.4rem;
  box-shadow: 0 10px 30px rgba(0,0,0,0.35);
}

/* Full-bleed banner (login): break the hero out of the 1080px main.container so
   the Cortex University banner spans the entire page width. Scoped to
   .hero-fullbleed so other .hero/.splash-hero uses are untouched. The 100vw
   container + 100%-width image (not a second 100vw) avoids scrollbar overflow. */
.hero-fullbleed {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding-top: 0;
  padding-left: 0;
  padding-right: 0;
}
.hero-fullbleed .splash-hero {
  width: 100%;
  max-width: none;
  max-height: 340px;
  border-radius: 0;
  margin: 0 0 var(--space-lg);
  box-shadow: 0 14px 36px rgba(0,0,0,0.42);
}

/* ── Welcome splash backdrop (P9): the once-per-session boot-splash gains a
   full-bleed Welcome_* / Welcome_public_boot backdrop behind the existing
   crest-draw animation, selected server-side by role/program (or the public
   boot art when signed out). Low-opacity + dark scrim keeps the gold crest
   and wordmark legible over any source photo. */
.boot-splash-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  opacity: 0.34;
  filter: saturate(0.85);
}
.boot-splash-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(160deg, var(--boot-navy) 0%, color-mix(in srgb, var(--boot-navy-2) 78%, transparent) 100%);
}
.boot-splash-inner { position: relative; z-index: 2; }

/* One background-image class per welcome_splash_key() value — kept as CSS
   classes (never an inline style) so the boot-splash element stays free of
   inline style attributes. */
.boot-splash-art.wa-student          { background-image: url('/static/emblems/welcome/student.webp'); }
.boot-splash-art.wa-professor        { background-image: url('/static/emblems/welcome/professor.webp'); }
.boot-splash-art.wa-admin            { background-image: url('/static/emblems/welcome/admin.webp'); }
.boot-splash-art.wa-ceo              { background-image: url('/static/emblems/welcome/ceo.webp'); }
.boot-splash-art.wa-hacker           { background-image: url('/static/emblems/welcome/hacker.webp'); }
.boot-splash-art.wa-cryptographer    { background-image: url('/static/emblems/welcome/cryptographer.webp'); }
.boot-splash-art.wa-security-defense { background-image: url('/static/emblems/welcome/security-defense.webp'); }
.boot-splash-art.wa-other-programs   { background-image: url('/static/emblems/welcome/other-programs.webp'); }
.boot-splash-art.wa-public-boot      { background-image: url('/static/emblems/welcome/public-boot.webp'); }

@media (prefers-reduced-motion: reduce) {
  .state-hero, .splash-hero { box-shadow: none; }
}

/* ── Country flag chips (profile residence/citizenship pickers, §2.7) ── */
.flag-field { display: flex; align-items: center; gap: 8px; }
.flag-field select { flex: 1 1 auto; min-width: 0; }
.flag-chip {
  width: 28px;
  height: 21px;
  border-radius: 3px;
  box-shadow: 0 0 0 1px var(--border, rgba(127, 127, 127, 0.35));
  flex: 0 0 auto;
}

/* ── Founder page (/founder) — flagship public biography ─────────────────────
   Scholarly, non-gamified register: left-aligned prose, gold institutional
   inks, year-columned timelines, and archival photos. Every in-flow photo
   sits in an IDENTICAL 4:3 mat frame (object-fit: contain — never cropped,
   never stretched; portraits letterbox on the mat, which reads intentional);
   the hero portrait keeps the same mat language in a 3:4 portrait frame.
   Theme-aware via the shared tokens; gold accents match the About page
   (#c9a23a / #b8860b, dark lift #e0b341). */
.founder {
  max-width: 880px;
  margin: 0 auto;
  padding: var(--space-lg) var(--space) var(--space-xl);
}

/* Hero — portrait beside the identity block; stacks and centers when narrow. */
.founder-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
}
.founder-hero-id { flex: 1 1 340px; min-width: 260px; }
.founder-eyebrow {
  margin: 0 0 0.4rem;
  font-size: 0.78rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--gold-text);
  font-weight: 700;
}
.founder-name {
  margin: 0 0 0.3rem;
  font-family: var(--font-display);
  font-size: 2.6rem;
  line-height: 1.1;
  letter-spacing: -0.01em;
  background: linear-gradient(135deg, var(--fg), #c9a23a);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.founder-role { margin: 0; font-size: 0.95rem; color: var(--muted); }
.founder-divider {
  width: 64px;
  height: 2px;
  margin: var(--space) 0;
  border-radius: 2px;
  background: linear-gradient(90deg, #c9a23a, transparent);
}
.founder-tagline {
  margin: 0;
  font-size: 1.12rem;
  line-height: 1.6;
  max-width: 46ch;
  color: var(--fg);
}

/* Sections — small-caps gold title with a fading hairline. */
.founder-section { margin-top: var(--space-xl); }
.founder-section-title {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0 0 var(--space-md);
  font-size: 0.8rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold-text);
  font-weight: 700;
}
.founder-section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, #c9a23a 45%, transparent), transparent);
}
:root[data-theme="dark"] .founder-eyebrow,
:root[data-theme="dark"] .founder-section-title,
:root[data-theme="dark"] .founder-year { color: #e0b341; }

/* Prose */
.founder-prose p {
  margin: 0 0 var(--space);
  max-width: 68ch;
  line-height: 1.7;
  color: var(--fg);
}
.founder-prose p:last-child { margin-bottom: 0; }
.founder-note {
  border-left: 2px solid #c9a23a;
  padding-left: var(--space);
  color: var(--muted);
  font-size: 0.95rem;
}

/* Timelines — tabular-nums year column + dotted-spine entries. */
.founder-timeline { list-style: none; margin: 0; padding: 0; }
.founder-timeline > li {
  display: grid;
  grid-template-columns: 6.8rem 1fr;
  gap: var(--space);
}
.founder-year {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--gold-text);
  text-align: right;
  white-space: nowrap;
  padding-top: 0.15rem;
}
.founder-entry {
  position: relative;
  padding: 0 0 var(--space-md) var(--space-md);
  border-left: 1px solid var(--border);
}
.founder-timeline > li:last-child .founder-entry { padding-bottom: 0; }
.founder-entry::before {
  content: "";
  position: absolute;
  left: -5px;
  top: 0.35rem;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #c9a23a;
  box-shadow: 0 0 0 3px color-mix(in srgb, #c9a23a 25%, transparent);
}
.founder-entry h4 { margin: 0 0 0.3rem; font-size: 1.02rem; line-height: 1.35; }
.founder-entry p {
  margin: 0 0 0.5rem;
  max-width: 62ch;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--muted);
}
.founder-entry ul {
  margin: 0.3rem 0 0.5rem;
  padding-left: 1.1rem;
  max-width: 62ch;
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--muted);
}
.founder-entry ul li { margin-bottom: 0.35rem; }

/* Photo mats — the uniform frame. Fixed 4:3 aspect at a shared max width, so
   every framed photo on the page is the same size; object-fit: contain keeps
   the full image visible (no crop, no stretch) centered on a subtle mat. */
.founder-photos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space);
  margin: var(--space-sm) 0 0;
}
.founder-photo {
  margin: 0;
  /* GROW to the text measure. The old `flex: 0 1 340px` + `max-width: 340px`
     pinned every plate to 340px whatever the column was, so a lone 4:3
     landscape sat in its own trough of whitespace beside prose twice its width.
     Growing means one photo spans the measure and two still share the row —
     `flex-wrap` and the 340px basis do the arranging, not a hard cap. */
  flex: 1 1 340px;
  width: 100%;
  max-width: 100%;
  min-width: 0;
}
.founder-photo-frame {
  display: block;
  aspect-ratio: 4 / 3;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--fg) 6%, var(--bg-elev)),
      color-mix(in srgb, var(--fg) 3%, var(--bg-elev)));
  box-shadow: var(--shadow-sm);
}
.founder-photo-frame img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  object-fit: contain;
  border-radius: calc(var(--radius-lg) - 4px);
}
.founder-photo figcaption {
  margin-top: 0.5rem;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--muted);
}
/* Hero portrait — same mat language, portrait orientation. */
.founder-hero-photo { flex: 0 1 300px; max-width: 300px; }
.founder-hero-photo .founder-photo-frame { aspect-ratio: 3 / 4; }
.founder-hero-photo figcaption { text-align: center; }

/* Education / research-and-craft cards — the About pillar language. */
.founder-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space);
}
.founder-card {
  border: 1px solid var(--border);
  border-top: 2px solid #c9a23a;
  border-radius: var(--radius-lg);
  background: var(--bg-elev);
  padding: var(--space) var(--space-md);
  box-shadow: var(--shadow-sm);
}
.founder-card h4 { margin: 0 0 0.3rem; font-size: 1rem; }
.founder-card-where {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gold-text);
}
:root[data-theme="dark"] .founder-card-where { color: #e0b341; }
.founder-card p { margin: 0 0 0.4rem; font-size: 0.92rem; line-height: 1.6; color: var(--muted); }
.founder-card ul {
  margin: 0.3rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.92rem;
  line-height: 1.6;
  color: var(--muted);
}
.founder-card ul li { margin-bottom: 0.4rem; }
.founder-backlink { margin: var(--space-lg) 0 0; font-size: 0.9rem; }

/* Narrow viewports — the year column folds above its entry; the hero centers.
   Flex-wrapped mats already shrink to 100%; the body never h-scrolls. */
@media (max-width: 40rem) {
  .founder-name { font-size: 2rem; }
  .founder-hero { text-align: center; }
  .founder-hero-id { min-width: 100%; }
  .founder-divider { margin-left: auto; margin-right: auto; }
  .founder-tagline { margin: 0 auto; }
  .founder-timeline > li { grid-template-columns: 1fr; gap: 0.2rem; }
  .founder-year { text-align: left; padding-top: 0; }
  .founder-photo { flex-basis: 100%; max-width: 100%; }
}

/* About → Founder cross-link (centered page register). */
.about-founder-link { margin: var(--space-md) 0 0; font-size: 0.92rem; }

/* ════════════════════════════════════════════════════════════════════════
   HUD LOGIN (confidential design) — a sealed, "hermetic" dark-navy HUD login
   that reveals after the boot splash. Self-contained fixed dark scheme,
   deliberately independent of the light/dark data-theme toggle: a locked
   confidential screen must look identical regardless of viewer preference.
   Only the page BODY is themed here; base.html nav + footer are untouched.
   All colours are local (auth-scoped custom props) so no global token leaks.
   ════════════════════════════════════════════════════════════════════════ */
.auth-screen {
  /* Full-bleed navy that escapes the 1080px main.container. The 100vw box +
     100%-width children avoid the horizontal-scrollbar overflow trap. */
  --auth-navy: #01040b;
  --auth-navy-2: #061224;
  --auth-blue: #2f6fb0;
  --auth-blue-bright: #59b7ff;
  --auth-blue-glow: rgba(80, 180, 255, 0.55);
  --auth-silver: #cdd6e2;
  --auth-fg: #dbe6f2;
  --auth-muted: #7f93ad;

  /* Positioned ANCHOR for every full-screen state overlay (the ::before CRT
     scanlines and the LOCKED ::after vignette are absolute, inset: 0). Without
     this they escape to the initial containing block — i.e. the top of the
     document — instead of staying pinned to the confidential screen. */
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  margin-top: calc(-1 * var(--space-lg, 1.5rem));
  padding: clamp(1rem, 4vw, 3rem) 1rem clamp(2rem, 6vw, 4rem);
  min-height: calc(100vh - 220px);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  background:
    radial-gradient(120% 90% at 50% 22%, var(--auth-navy-2) 0%, var(--auth-navy) 62%, #000208 100%);
  color: var(--auth-fg);
  font-family: var(--font-sans, system-ui, sans-serif);
}

.auth-hud {
  width: 100%;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* ══ THE VAULT ══ a layered, robotic bank-vault / spaceship-airlock mechanism
   (inline SVG + CSS transforms only — no libs). The silver Cortex "C" emblem is
   the FIXED ANCHOR: it never moves or fades in any state; the machinery unlocks
   AROUND it. Every transition is a GPU-friendly transform/opacity change keyed
   off .auth-screen[data-auth-state]; portal.js enforces ≥3s per state so the
   choreography is always seen regardless of server latency. Precise, mechanical
   easing (no soft/organic curves). Depth is faked with stacked transform planes
   + pointer parallax (--px/--py, set by portal.js). */
.auth-vault {
  position: relative;
  width: clamp(240px, 62vw, 400px);
  aspect-ratio: 1 / 1;
  margin: 0 auto clamp(1rem, 3vw, 2rem);
  line-height: 0;
  isolation: isolate;
  --px: 0; --py: 0;                 /* pointer parallax offset (−1..1) */
  perspective: 900px;
}
.vault-svg { position: relative; z-index: 2; display: block; width: 100%; height: 100%; overflow: visible; }

/* Explicit transform box for every animated SVG part (SVG needs it). */
.vault-plane, .vault-wheel, .vault-bolts, .vault-rivets, .vault-ticks,
.vault-rim, .vault-rim-glow, .vault-scan, .vault-hex, .vault-progress,
.vault-deadbolts, .vault-core, .blade, .bolt, .deadbolt, .hex {
  transform-origin: 220px 220px;
  transform-box: view-box;
}
.blade { transition: transform 0.55s cubic-bezier(.3,0,.15,1), opacity 0.5s ease; }
.bolt  { transition: transform 0.35s cubic-bezier(.16,.8,.3,1); }

/* Parallax planes — each drifts a different amount for a layered-depth read. */
.vault-depth  { transform: translate(calc(var(--px) * 5px),  calc(var(--py) * 5px)); }
.vault-aperture { transform: translate(calc(var(--px) * 9px), calc(var(--py) * 9px)); }
.vault-frame  { transform: translate(calc(var(--px) * 3px),  calc(var(--py) * 3px)); }
.vault-wheel  { transform: translate(calc(var(--px) * 13px), calc(var(--py) * 13px)); }

/* Emblem "C" — the fixed anchor. Circular-masked, always on top of the aperture. */
.vault-emblem {
  position: absolute;
  z-index: 4;
  top: 50%; left: 50%;
  width: 35%; height: 35%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  object-fit: cover;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 60%, transparent 76%);
          mask-image: radial-gradient(circle at 50% 50%, #000 60%, transparent 76%);
  filter: drop-shadow(0 0 7px rgba(80, 180, 255, 0.4));
  animation: vault-breathe 5s ease-in-out infinite;
}

/* Full-vault colour flash overlay (core pulse / klaxon / lockdown). */
.vault-flash {
  position: absolute; inset: 0; z-index: 3;
  border-radius: 50%;
  opacity: 0; pointer-events: none;
  mix-blend-mode: screen;
}

/* Terminal deadbolt seal stamp — slams over centre on LOCKED. */
.vault-seal {
  position: absolute; z-index: 5;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(2.4);
  color: #05070c; opacity: 0;
  filter: drop-shadow(0 0 3px rgba(0,0,0,0.9));
  pointer-events: none;
}

/* Blade/steel default fill (paths carry no inline fill). */
.blade { fill: url(#vaultFace); stroke: #223244; stroke-width: 1; }
.bolt  { }
.vault-core { opacity: 0; }
.vault-scan, .vault-hex { opacity: 0; }
/* Gauge starts at 12 o'clock via CSS rotate (origin = vault centre, view-box).
   Keep the rotation HERE, never as an SVG transform attribute: the attribute's
   own rotation centre would compose with this transform-origin and displace the
   ring one vault-height down onto the login form (shipped bug, fixed 2026-07-18). */
.vault-progress { opacity: 0; stroke-dasharray: 100; stroke-dashoffset: 100; transform: rotate(-90deg); }
.deadbolt { transform: translateY(-64px); opacity: 0; }

/* ── IDLE: sealed, calm — slow structural drift + breathing glow/emblem. ── */
.vault-rivets { animation: vault-spin-slow 80s linear infinite; }
.vault-ticks  { animation: vault-spin-slow 120s linear infinite reverse; }
.vault-rim-glow {
  stroke: #2f6fb0;
  filter: drop-shadow(0 0 4px rgba(47,111,176,0.6));
  animation: vault-breathe 5s ease-in-out infinite;
}
@keyframes vault-spin-slow { to { transform: rotate(360deg); } }
@keyframes vault-breathe { 0%,100% { opacity: 0.62; } 50% { opacity: 1; } }

/* ── VERIFYING: security theatre — wheel spins, ticks race, blades illuminate
   in sequence, scan beam sweeps, hex/data streams cascade, gauge fills. ── */
.auth-screen[data-auth-state="verifying"] .vault-wheel { animation: vault-spin-fast 1.05s linear infinite; }
.auth-screen[data-auth-state="verifying"] .vault-ticks { animation: vault-spin-fast 2.2s linear infinite reverse; }
.auth-screen[data-auth-state="verifying"] .vault-rivets { animation: vault-spin-fast 6s steps(24) infinite; }
.auth-screen[data-auth-state="verifying"] .vault-rim-glow { stroke: #e0c43a; filter: drop-shadow(0 0 8px rgba(224,196,58,0.75)); }
.auth-screen[data-auth-state="verifying"] .vault-scan { opacity: 1; }
.auth-screen[data-auth-state="verifying"] .vault-scan rect { animation: vault-scan 1.5s cubic-bezier(.5,0,.5,1) infinite; }
.auth-screen[data-auth-state="verifying"] .vault-hex { opacity: 1; }
.auth-screen[data-auth-state="verifying"] .vault-hex .hex { animation: vault-hex-fall 1.4s linear infinite; }
.auth-screen[data-auth-state="verifying"] .vault-progress { opacity: 1; animation: vault-gauge-fill 2.8s cubic-bezier(.4,0,.2,1) forwards; }
.auth-screen[data-auth-state="verifying"] .blade { animation: vault-seg-scan 1.6s linear infinite; }
/* stagger blade illumination in sequence around the ring */
.auth-screen[data-auth-state="verifying"] .blade-0 { animation-delay: 0ms; }
.auth-screen[data-auth-state="verifying"] .blade-1 { animation-delay: 200ms; }
.auth-screen[data-auth-state="verifying"] .blade-2 { animation-delay: 400ms; }
.auth-screen[data-auth-state="verifying"] .blade-3 { animation-delay: 600ms; }
.auth-screen[data-auth-state="verifying"] .blade-4 { animation-delay: 800ms; }
.auth-screen[data-auth-state="verifying"] .blade-5 { animation-delay: 1000ms; }
.auth-screen[data-auth-state="verifying"] .blade-6 { animation-delay: 1200ms; }
.auth-screen[data-auth-state="verifying"] .blade-7 { animation-delay: 1400ms; }
.vault-hex .hex-0{--hd:0ms}.vault-hex .hex-1{--hd:120ms}.vault-hex .hex-2{--hd:240ms}
.vault-hex .hex-3{--hd:360ms}.vault-hex .hex-4{--hd:80ms}.vault-hex .hex-5{--hd:300ms}
.vault-hex .hex-6{--hd:180ms}.vault-hex .hex-7{--hd:420ms}.vault-hex .hex-8{--hd:60ms}
.vault-hex .hex-9{--hd:340ms}.vault-hex .hex-10{--hd:220ms}.vault-hex .hex-11{--hd:400ms}
.auth-screen[data-auth-state="verifying"] .vault-hex .hex { animation-delay: var(--hd); }
@keyframes vault-spin-fast { to { transform: rotate(360deg); } }
@keyframes vault-scan {
  0% { opacity: 0; transform: translateY(-160px); }
  15%,85% { opacity: 0.8; }
  100% { opacity: 0; transform: translateY(160px); }
}
@keyframes vault-hex-fall {
  0% { opacity: 0; transform: translateY(0); }
  20% { opacity: 0.9; }
  100% { opacity: 0; transform: translateY(300px); }
}
@keyframes vault-gauge-fill { to { stroke-dashoffset: 0; } }
@keyframes vault-seg-scan {
  0%, 100% { stroke: #223244; filter: none; }
  8% { stroke: #59b7ff; filter: drop-shadow(0 0 6px rgba(89,183,255,0.9)); }
}

/* ── GRANTED: robotic airlock unlock AROUND the fixed C. Servo bolts retract →
   rings snap to lock → blades iris open → core pulse → wipe. The C stays put. ── */
.auth-screen[data-auth-state="granted"] .vault-rim-glow { stroke: #3fb950; filter: drop-shadow(0 0 12px rgba(63,185,80,0.95)); }
.auth-screen[data-auth-state="granted"] .vault-progress { opacity: 1; stroke: #3fb950; stroke-dashoffset: 0; }
/* GRANTED timings ADAPT to the real length of auth-portal.mp3: JS sets
   --auth-dur to the audio's duration at runtime (fallback 2.86s baked into each
   var() default), and every delay/duration below is expressed as a FRACTION of
   that total via calc(). So the whole airlock → iris → core → TV-static burst
   sequence scales as one unit — swap in a 6s clip and it runs 6s. Fractions =
   the measured 2.86s pacing ÷ 2.86s. Same keyframes + end positions. */
/* 1. bolts retract inward, hard staggered servo (transitions on .bolt). */
.auth-screen[data-auth-state="granted"] .bolt { transition-duration: calc(var(--auth-dur, 2.86s) * 0.1224); }
.auth-screen[data-auth-state="granted"] .bolt-0 { transform: translateY(40px); transition-delay: 0s; }
.auth-screen[data-auth-state="granted"] .bolt-1 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.0175); }
.auth-screen[data-auth-state="granted"] .bolt-2 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.0350); }
.auth-screen[data-auth-state="granted"] .bolt-3 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.0524); }
.auth-screen[data-auth-state="granted"] .bolt-4 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.0699); }
.auth-screen[data-auth-state="granted"] .bolt-5 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.0874); }
.auth-screen[data-auth-state="granted"] .bolt-6 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.1049); }
.auth-screen[data-auth-state="granted"] .bolt-7 { transform: translateY(40px); transition-delay: calc(var(--auth-dur, 2.86s) * 0.1224); }
/* 2. rings counter-rotate and CLICK into aligned lock positions (stepped). */
.auth-screen[data-auth-state="granted"] .vault-ticks  { animation: vault-lock-cw  calc(var(--auth-dur, 2.86s) * 0.2063) steps(6) calc(var(--auth-dur, 2.86s) * 0.1014) forwards; }
.auth-screen[data-auth-state="granted"] .vault-rivets { animation: vault-lock-ccw calc(var(--auth-dur, 2.86s) * 0.2063) steps(4) calc(var(--auth-dur, 2.86s) * 0.1014) forwards; }
.auth-screen[data-auth-state="granted"] .vault-wheel  { animation: vault-wheel-snap calc(var(--auth-dur, 2.86s) * 0.1748) cubic-bezier(.2,.9,.1,1) calc(var(--auth-dur, 2.86s) * 0.1014) forwards; }
/* 3. blades iris open — retract radially outward, staggered (transforms below). */
.auth-screen[data-auth-state="granted"] .blade { transition: transform calc(var(--auth-dur, 2.86s) * 0.1748) cubic-bezier(.5,0,.1,1), opacity calc(var(--auth-dur, 2.86s) * 0.1469) ease; }
.auth-screen[data-auth-state="granted"] .blade-0{transform:translate(72.1px,29.8px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.2640);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-1{transform:translate(29.8px,72.1px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.2815);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-2{transform:translate(-29.8px,72.1px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.2972);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-3{transform:translate(-72.1px,29.8px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.3129);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-4{transform:translate(-72.1px,-29.8px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.3287);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-5{transform:translate(-29.8px,-72.1px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.3462);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-6{transform:translate(29.8px,-72.1px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.3619);opacity:0;}
.auth-screen[data-auth-state="granted"] .blade-7{transform:translate(72.1px,-29.8px);transition-delay:calc(var(--auth-dur, 2.86s) * 0.3776);opacity:0;}
/* 4. depth well + core light revealed through the open aperture, then wipe. */
.auth-screen[data-auth-state="granted"] .vault-core { animation: vault-core-pulse calc(var(--auth-dur, 2.86s) * 0.5594) ease-in calc(var(--auth-dur, 2.86s) * 0.3497) forwards; }
/* The old green balloon-flash (vault-flash → vault-wipe, scale→6 fill) is
   RETIRED. Keep the flash element inert in GRANTED so the final flourish is
   played solely by the retro-TV signal-lock overlay (.vault-tv) below. The
   flash element is still driven by the DENIED klaxon rule — untouched. */
.auth-screen[data-auth-state="granted"] .vault-flash { opacity: 0; animation: none; }
@keyframes vault-wheel-snap {
  0% { transform: rotate(0); } 60% { transform: rotate(46deg); } 100% { transform: rotate(45deg); }
}
@keyframes vault-lock-cw  { to { transform: rotate(30deg); } }
@keyframes vault-lock-ccw { to { transform: rotate(-30deg); } }
@keyframes vault-core-pulse {
  0% { opacity: 0; transform: scale(0.55); }
  40% { opacity: 0.85; }
  100% { opacity: 0.95; transform: scale(1.15); }
}
/* ── GRANTED signal-lock: retro analog-TV "losing signal" burst. A full-screen
   FIXED overlay that snaps on right as the airlock finishes irising open, then
   thrashes with turbulence snow + rolling/torn scanlines + a horizontal sync
   glitch + chroma-subcarrier noise, and SETTLES (fades) as the redirect fires.
   Replaces the retired green core-flash wipe. Self-contained (inline SVG
   feTurbulence, no external assets → CSP-safe). ── */
.vault-tv {
  position: fixed; inset: 0; z-index: 60;
  display: none; opacity: 0; pointer-events: none;
  overflow: hidden; background: #05070c;
  contain: strict;
}
.auth-screen[data-auth-state="granted"] .vault-tv {
  display: block;
  /* Duration + delay as fractions of --auth-dur (1.3s/2.86s = 0.4545,
     1.47s/2.86s = 0.5140) so the signal-lock burst always climaxes and settles
     right as the sound ends, whatever its length. */
  animation: vault-tv-burst calc(var(--auth-dur, 2.86s) * 0.4545) linear calc(var(--auth-dur, 2.86s) * 0.5140) both;
}
/* Snow bed — the turbulence SVG stretched past the edges, screen-blended over
   the near-black bed so the noise reads as bright analog "snow"; jitters + an
   unstable brightness flicker for a live-signal feel. */
.vault-tv-snow {
  position: absolute; inset: -8%; width: 116%; height: 116%;
  mix-blend-mode: screen; opacity: 0.92;
  animation: vault-tv-jump 0.26s steps(2) infinite,
             vault-tv-flick 0.4s steps(3) infinite;
}
/* Chroma / colour-subcarrier noise — drifting RGB bands, colour-dodged, with a
   stepped hue cycle so the tint crawls the way a de-synced chroma channel does. */
.vault-tv-chroma {
  position: absolute; inset: 0; mix-blend-mode: color-dodge; opacity: 0.5;
  background: repeating-linear-gradient(90deg,
    rgba(255,0,80,0.16) 0 3px, rgba(0,0,0,0) 3px 6px,
    rgba(0,200,255,0.16) 6px 9px, rgba(0,0,0,0) 9px 12px,
    rgba(80,255,120,0.12) 12px 15px, rgba(0,0,0,0) 15px 18px);
  background-size: 220% 100%;
  animation: vault-tv-chroma 0.55s linear infinite,
             vault-tv-hue 1.1s steps(6) infinite;
}
/* Dark CRT scanlines that slowly roll (vertical-hold slip). */
.vault-tv-scan {
  position: absolute; inset: 0;
  background: repeating-linear-gradient(0deg,
    rgba(0,0,0,0.55) 0 1px, rgba(0,0,0,0) 1px 3px);
  mix-blend-mode: multiply; opacity: 0.8;
  animation: vault-tv-rollscan 0.9s linear infinite;
}
/* Bright torn sync bar sweeping up the frame + a hard horizontal sync-tear. */
.vault-tv-roll {
  position: absolute; left: -6%; width: 112%; height: 26%; top: 110%;
  background: linear-gradient(0deg,
    rgba(255,255,255,0) 0%, rgba(255,255,255,0.13) 42%,
    rgba(255,255,255,0.34) 50%, rgba(255,255,255,0.13) 58%,
    rgba(255,255,255,0) 100%);
  mix-blend-mode: screen;
  animation: vault-tv-syncbar 0.75s linear infinite,
             vault-tv-tear 0.15s steps(2) infinite;
}
@keyframes vault-tv-burst {
  0%   { opacity: 0; }
  5%   { opacity: 1; }
  60%  { opacity: 1; }
  78%  { opacity: 0.85; }
  90%  { opacity: 0.4; }
  100% { opacity: 0; }
}
@keyframes vault-tv-jump {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(-2.2%, 1.6%); }
  100% { transform: translate(1.8%, -1.2%); }
}
@keyframes vault-tv-flick { 0% { opacity: 0.95; } 50% { opacity: 0.7; } 100% { opacity: 1; } }
@keyframes vault-tv-chroma { 0% { background-position: 0 0; } 100% { background-position: 220% 0; } }
@keyframes vault-tv-hue { to { filter: hue-rotate(360deg); } }
@keyframes vault-tv-rollscan { to { background-position: 0 14px; } }
@keyframes vault-tv-syncbar { 0% { top: 110%; } 100% { top: -34%; } }
@keyframes vault-tv-tear {
  0%   { transform: translateX(0); }
  50%  { transform: translateX(-3.5%); }
  100% { transform: translateX(2.6%); }
}

/* ── DENIED: red lockdown klaxon flash, mechanism recoils + SLAMS, glitch burst.
   The C holds. Then portal.js runs the 5s SECURITY HOLD countdown. ── */
.auth-screen[data-auth-state="denied"] .vault-svg { animation: vault-shudder 0.4s cubic-bezier(.36,.07,.19,.97) 3; }
.auth-screen[data-auth-state="denied"] .vault-wheel { animation: vault-recoil 0.55s cubic-bezier(.2,.9,.1,1); }
.auth-screen[data-auth-state="denied"] .bolt { transform: translateY(-6px); transition: transform 0.12s steps(2); }
.auth-screen[data-auth-state="denied"] .vault-rim-glow { stroke: #f85149; filter: drop-shadow(0 0 16px rgba(248,81,73,0.95)); animation: vault-red-flash 0.45s steps(2) 4; }
.auth-screen[data-auth-state="denied"] .vault-flash {
  background: radial-gradient(circle, rgba(255,60,50,0.85) 0%, rgba(180,10,10,0.2) 60%, transparent 75%);
  animation: vault-klaxon 0.5s steps(2) 4;
}
.auth-screen[data-auth-state="denied"] .blade { animation: vault-glitch 0.24s steps(2) 5; }
@keyframes vault-shudder {
  0%,100% { transform: translate(0,0); }
  20% { transform: translate(-9px,2px) rotate(-1.2deg); }
  60% { transform: translate(9px,-2px) rotate(1.2deg); }
  80% { transform: translate(-4px,1px); }
}
@keyframes vault-recoil { 0% { transform: rotate(0); } 35% { transform: rotate(-26deg); } 100% { transform: rotate(-4deg); } }
@keyframes vault-red-flash { 0%,100% { opacity: 0.45; } 50% { opacity: 1; } }
@keyframes vault-klaxon { 0%,100% { opacity: 0; } 50% { opacity: 1; } }
@keyframes vault-glitch {
  0% { transform: translate(0,0); } 50% { transform: translate(3px,-2px); } 100% { transform: translate(-2px,1px); }
}

/* ── LOCKED (most severe): heavy deadbolts DROP into the frame, mechanism seals,
   cold desaturated lockdown + red→black vignette. Terminal. The C holds. ── */
/* LOCKED reveal ADAPTS to the real length of auth-portal-locked.mp3: JS sets
   --auth-locked-dur to the audio's duration (fallback 14.76s baked into each
   var() default), and every timing below is a FRACTION of that total via
   calc(). The same seal choreography (cold desaturation → four heavy deadbolts
   DROP one at a time → terminal seal stamp → deepening red→black vignette)
   stretches and staggers to fill the whole sound, whatever its length. Fractions
   = the measured 14.76s pacing ÷ 14.76s. The emblem keeps its idle
   `vault-breathe` so the sealed screen never freezes. */
.auth-screen[data-auth-state="locked"] .vault-svg { filter: grayscale(0.9) brightness(0.5) contrast(1.05); transition: filter calc(var(--auth-locked-dur, 14.76s) * 0.2710) ease; }
.auth-screen[data-auth-state="locked"] .vault-emblem { filter: grayscale(0.92) brightness(0.55); transition: filter calc(var(--auth-locked-dur, 14.76s) * 0.2710) ease; }
.auth-screen[data-auth-state="locked"] .deadbolt { transform: translateY(0); opacity: 1; transition: transform calc(var(--auth-locked-dur, 14.76s) * 0.0745) cubic-bezier(.7,0,.85,.6), opacity calc(var(--auth-locked-dur, 14.76s) * 0.0339) ease; }
/* one heavy bolt drops at each ~1/5 of the lockdown (0.054 → 0.705 of total) */
.auth-screen[data-auth-state="locked"] .deadbolt-0 { transition-delay: calc(var(--auth-locked-dur, 14.76s) * 0.0542); }
.auth-screen[data-auth-state="locked"] .deadbolt-1 { transition-delay: calc(var(--auth-locked-dur, 14.76s) * 0.2710); }
.auth-screen[data-auth-state="locked"] .deadbolt-2 { transition-delay: calc(var(--auth-locked-dur, 14.76s) * 0.4878); }
.auth-screen[data-auth-state="locked"] .deadbolt-3 { transition-delay: calc(var(--auth-locked-dur, 14.76s) * 0.7046); }
/* the terminal seal stamps down after the last bolt lands (0.813 → ~0.935 of total) */
.auth-screen[data-auth-state="locked"] .vault-seal { animation: vault-seal-slam calc(var(--auth-locked-dur, 14.76s) * 0.1220) cubic-bezier(.2,.8,.2,1.2) calc(var(--auth-locked-dur, 14.76s) * 0.8130) forwards; }
.auth-screen[data-auth-state="locked"]::after {
  content: "";
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: radial-gradient(120% 90% at 50% 40%, transparent 38%, rgba(120,10,10,0.28) 70%, rgba(0,0,0,0.9) 100%);
  /* vignette keeps deepening across the full lockdown so the reveal fills the audio */
  animation: vault-lockdown-settle calc(var(--auth-locked-dur, 14.76s) * 0.9960) ease-out forwards;
}
@keyframes vault-seal-slam {
  0% { opacity: 0; transform: translate(-50%,-50%) scale(2.8) rotate(-8deg); }
  70% { opacity: 1; transform: translate(-50%,-50%) scale(0.88) rotate(2deg); }
  100% { opacity: 1; transform: translate(-50%,-50%) scale(1) rotate(0); }
}
@keyframes vault-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes vault-lockdown-settle {
  0%   { opacity: 0; }
  34%  { opacity: 0.6; }
  100% { opacity: 1; }
}

/* Always-on faint CRT scanline shimmer over the whole confidential screen. */
.auth-screen::before {
  content: "";
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(89,183,255,0.035) 0 1px, transparent 1px 3px);
  mix-blend-mode: screen;
  animation: vault-crt 8s linear infinite;
  opacity: 0.5;
}
@keyframes vault-crt { to { background-position: 0 120px; } }

/* ── Readout: neon state text, one colour per state. ── */
.auth-readout {
  min-height: 1.4em;
  margin: 0 0 clamp(0.6rem, 2vw, 1.2rem);
  text-align: center;
  font-size: clamp(0.95rem, 2.6vw, 1.25rem);
  font-weight: 700;
  letter-spacing: 0.34em;
  /* Centering compensation: letter-spacing adds a trailing gap after the LAST
     glyph too, so centered text sits half a letter-space left of true centre
     (reads as mis-set/mangled next to the vault axis). Padding-left of one
     letter-space re-centres the visible glyph run. */
  padding-left: 0.34em;
  text-transform: uppercase;
  overflow-wrap: normal;
  opacity: 0;
  transition: opacity 0.35s ease, color 0.35s ease;
}
.auth-screen[data-auth-state] .auth-readout { opacity: 0; }
.auth-screen[data-auth-state="verifying"] .auth-readout {
  opacity: 1; color: #e0c43a;
  text-shadow: 0 0 8px rgba(224,196,58,0.8), 0 0 20px rgba(224,196,58,0.5);
  animation: auth-readout-pulse 1.2s ease-in-out infinite;
}
.auth-screen[data-auth-state="granted"] .auth-readout {
  opacity: 1; color: #4be07a;
  text-shadow: 0 0 8px rgba(63,185,80,0.9), 0 0 22px rgba(63,185,80,0.6);
}
.auth-screen[data-auth-state="denied"] .auth-readout {
  opacity: 1; color: #ff5a52;
  text-shadow: 0 0 8px rgba(248,81,73,0.9), 0 0 22px rgba(248,81,73,0.6);
}
.auth-screen[data-auth-state="hold"] .auth-readout {
  opacity: 1; color: #ff5a52;
  text-shadow: 0 0 6px rgba(248,81,73,0.7);
}
.auth-screen[data-auth-state="error"] .auth-readout {
  opacity: 1; color: #e0a63a;
  text-shadow: 0 0 8px rgba(224,166,58,0.7);
}
.auth-screen[data-auth-state="locked"] .auth-readout {
  opacity: 1;
  color: #05070c;
  font-weight: 900;
  letter-spacing: 0.42em;
  padding-left: 0.42em; /* keep the wider-tracked stamp visually centred too */
  /* "Imperial" heavy black lettering — a hard, embossed stamp, no neon glow. */
  text-shadow: 0 1px 0 #3a3f47, 0 -1px 0 #000, 1px 0 0 #2a2e35, 0 0 2px rgba(0,0,0,0.9);
  position: relative;
  z-index: 6;
}
@keyframes auth-readout-pulse { 0%,100% { opacity: 0.75; } 50% { opacity: 1; } }

/* ── Form ── */
.auth-form {
  width: 100%;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  gap: clamp(0.8rem, 2.4vw, 1.15rem);
}
.auth-form-wide { max-width: 500px; }

.auth-field { display: flex; flex-direction: column; gap: 0.4rem; }

.auth-label {
  font-size: 0.82rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--auth-muted);
  font-weight: 600;
  padding: 0;
}

.auth-input-shell {
  position: relative;
  display: flex;
  align-items: center;
  border: 1px solid rgba(89, 155, 210, 0.45);
  border-radius: 4px;
  background: rgba(6, 18, 36, 0.55);
  box-shadow: inset 0 0 18px rgba(20, 60, 110, 0.25);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.auth-input-shell:focus-within {
  border-color: var(--auth-blue-bright);
  box-shadow: inset 0 0 18px rgba(20, 60, 110, 0.35),
              0 0 0 1px var(--auth-blue-bright),
              0 0 16px var(--auth-blue-glow);
}

.auth-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--auth-silver);
  flex: 0 0 auto;
}
.auth-icon-left  { padding: 0 0.55rem 0 0.7rem; }
.auth-icon-right { padding: 0 0.7rem 0 0.55rem; color: var(--auth-blue); }
.auth-hex { opacity: 0.85; }

.auth-input {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--auth-fg);
  font-size: 1rem;
  letter-spacing: 0.04em;
  padding: 0.78rem 0.4rem;
  font-family: inherit;
}
.auth-input::placeholder { color: rgba(127, 147, 173, 0.7); }
/* selects share the shell; give the arrow room */
select.auth-input { padding-right: 0.7rem; cursor: pointer; appearance: none; }
select.auth-input option { background: var(--auth-navy-2); color: var(--auth-fg); }
/* When a field has no left icon (selects/email), keep text off the border. */
.auth-input-shell > .auth-input:first-child { padding-left: 0.85rem; }

/* Autofill: keep the navy shell, override the browser's white box. */
.auth-input:-webkit-autofill,
.auth-input:-webkit-autofill:hover,
.auth-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--auth-fg);
  -webkit-box-shadow: 0 0 0 1000px rgba(6, 18, 36, 0.98) inset;
  caret-color: var(--auth-fg);
}

/* ── Wave-B mode radios ── */
.auth-fieldset { border: 0; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.auth-modes { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; }
.auth-mode {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.9rem; color: var(--auth-fg); cursor: pointer;
}
.auth-mode input { accent-color: var(--auth-blue-bright); }

/* ── Octagonal submit ── */
.auth-submit-row { display: flex; justify-content: center; margin-top: 0.4rem; }
.auth-submit {
  --oct: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 118px;
  height: 54px;
  color: var(--auth-blue-bright);
  background: rgba(6, 18, 36, 0.5);
  border: 1px solid rgba(89, 155, 210, 0.6);
  /* octagon */
  clip-path: polygon(var(--oct) 0, calc(100% - var(--oct)) 0, 100% var(--oct),
                     100% calc(100% - var(--oct)), calc(100% - var(--oct)) 100%,
                     var(--oct) 100%, 0 calc(100% - var(--oct)), 0 var(--oct));
  cursor: pointer;
  transition: color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.auth-submit:hover,
.auth-submit:focus-visible {
  color: #fff;
  background: rgba(31, 90, 150, 0.35);
  box-shadow: 0 0 22px var(--auth-blue-glow);
}
.auth-submit svg { transition: transform 0.18s ease; }
.auth-submit:hover svg { transform: translateX(3px); }

/* ── Links + tagline ── */
.auth-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem 1.4rem;
  margin-top: clamp(1.2rem, 3vw, 1.8rem);
}
.auth-links a {
  color: var(--auth-muted);
  text-decoration: none;
  font-size: 0.86rem;
  letter-spacing: 0.06em;
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.auth-links a:hover,
.auth-links a:focus-visible {
  color: var(--auth-blue-bright);
  border-bottom-color: var(--auth-blue-bright);
}

.auth-tagline {
  margin-top: clamp(1.5rem, 5vw, 3rem);
  text-align: center;
  font-size: 0.78rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--auth-blue);
  opacity: 0.75;
}

/* Focus-visible on all interactive auth controls (a11y). */
.auth-screen a:focus-visible,
.auth-screen button:focus-visible,
.auth-screen input:focus-visible,
.auth-screen select:focus-visible {
  outline: 2px solid var(--auth-blue-bright);
  outline-offset: 2px;
}

/* Visually-hidden but screen-reader-available heading. */
.auth-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* Disable the whole form while any animation runs (no double-submit). */
.auth-screen.auth-busy .auth-form { pointer-events: none; opacity: 0.55; }
.auth-screen.auth-busy .auth-submit { cursor: progress; }

/* Mobile: vault scales down, fields stack, no horizontal scroll. */
@media (max-width: 40rem) {
  .auth-screen { min-height: calc(100vh - 160px); padding-top: 1.5rem; }
  .auth-label { font-size: 0.76rem; letter-spacing: 0.16em; }
  .auth-tagline { letter-spacing: 0.22em; font-size: 0.7rem; }
  .auth-readout { letter-spacing: 0.2em; }
  .auth-modes { gap: 0.35rem 0.75rem; }
  .auth-vault { width: clamp(200px, 72vw, 320px); }
}

/* Reduced motion: keep every colour + text state + HUD readout, but replace all
   spin / iris / recoil / slam / parallax motion with fast static crossfades. No
   looping ambient motion, no rotation/scale/translate. The C anchor is fixed
   regardless. */
@media (prefers-reduced-motion: reduce) {
  /* Kill all ambient + per-state animations and parallax drift. */
  .vault-rivets, .vault-ticks, .vault-rim-glow, .vault-emblem, .vault-plane,
  .blade, .bolt, .deadbolt, .hex, .vault-core, .vault-flash, .vault-progress,
  .vault-scan rect, .vault-seal, .auth-readout,
  .auth-screen::before,
  .auth-screen[data-auth-state] .vault-wheel,
  .auth-screen[data-auth-state] .vault-svg {
    animation: none !important;
    transition: opacity 0.35s ease !important;
    transform: none !important;
  }
  /* The LOCKED vignette (`.auth-screen::after`) is the one exception, and it
     used to be in the list above. Its animation is opacity-only — a red→black
     wash deepening over a still screen — so cancelling it bought no vestibular
     safety and only turned a deepening into a hard slap of colour, while the
     `animation: none !important` also forced any register skin that wanted to
     re-ink the wash to fight it with an `!important` of its own. Collapsed to
     a 400ms cross-fade instead, per the --motion-fade-duration contract at the
     top of this file. */
  .auth-screen[data-auth-state="locked"]::after { --motion-fade-duration: 400ms; }
  /* GRANTED: no iris — just reveal the core light + fade the aperture out. The
     retro-TV signal-lock collapses to a calm, brief dark-grey flicker with NO
     snow/scanline/glitch motion (honour prefers-reduced-motion). */
  .auth-screen[data-auth-state="granted"] .blade { opacity: 0; }
  .auth-screen[data-auth-state="granted"] .vault-core { opacity: 0.9; }
  .auth-screen[data-auth-state="granted"] .vault-progress { opacity: 1; }
  .vault-tv-snow, .vault-tv-chroma, .vault-tv-scan, .vault-tv-roll {
    display: none !important;
  }
  .auth-screen[data-auth-state="granted"] .vault-tv {
    display: block; background: #12161d;
    animation: vault-tv-calm 0.9s ease-in 1.5s both !important;
  }
  /* VERIFYING: hold the beam + gauge visible (no sweep/fill motion). */
  .auth-screen[data-auth-state="verifying"] .vault-scan,
  .auth-screen[data-auth-state="verifying"] .vault-progress { opacity: 1; }
  .auth-screen[data-auth-state="verifying"] .vault-progress { stroke-dashoffset: 0; }
  /* LOCKED: deadbolts + seal shown in place, no drop/slam. */
  .auth-screen[data-auth-state="locked"] .deadbolt { opacity: 1; }
  .auth-screen[data-auth-state="locked"] .vault-seal { opacity: 1; transform: translate(-50%,-50%) scale(1) !important; }
  .auth-submit svg, .auth-submit:hover svg { transition: none; transform: none; }
  @keyframes vault-tv-calm { 0% { opacity: 0; } 45% { opacity: 0.4; } 100% { opacity: 0; } }
}

/* Public shareable credential page (H2). Theme-aware; the anchor badge uses a
   high-contrast green in both light and dark (WCAG AA). */
.credential-public-title { margin: .2rem 0 .4rem; }
.credential-public-holder { font-size: 1.05rem; margin: 0 0 1rem; }
.credential-public-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .8rem 1.4rem; margin: 1rem 0; }
.credential-public-facts dt { margin-bottom: .15rem; }
.credential-public-facts dd { margin: 0; }
.credential-public-clause { border-left: 3px solid var(--border); padding-left: .8rem; }
.credential-anchor { display: block; margin: 1rem 0; }
.credential-anchor-value { word-break: break-all; }
.credential-anchor-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.4rem; height: 1.4rem; border-radius: 50%;
  background: #1a7f37; color: #ffffff; font-weight: 700; margin-right: .4rem;
}
.credential-public-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.2rem; }

/* Account-enablement banner (PROGRAM_VISIBILITY_AND_ACCOUNT_GATE §2) — shown to
   a logged-in but not-yet-active account browsing the limited surface. */
.account-pending-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1rem;
  padding: .7rem 1rem; margin: 0; border-bottom: 1px solid var(--border);
  background: var(--bg-elev); color: var(--fg); font-size: .95rem;
}
.account-pending-banner-text { flex: 1 1 20rem; }

/* Admin catalogue: bulk set-visibility toolbar + per-row visibility badge. */
.admin-cat-bulk {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .8rem;
  margin: 0 0 1rem; padding: .6rem .8rem;
  border: 1px solid var(--border); border-radius: .4rem; background: var(--bg-elev);
}
.admin-cat-bulk-label { font-weight: 600; }
.admin-cat-vis-badge { text-transform: capitalize; }
.admin-cat-vis-unpublished { opacity: .75; }
/* Under review is public-hidden like unpublished, but on its way OUT — muted the
   same amount, italic so the two hidden tiers are distinguishable at a glance. */
.admin-cat-vis-under_review { opacity: .75; font-style: italic; }

/* ── "Part of Lince" ownership attribution (about page) — #116 refined
   presentation, folded inline into the About Cortex section (#118, replacing
   the old <dialog>). The square brand logo renders undistorted (object-fit:
   contain over an equal-sided box) on a clean white, rounded plate so its own
   near-white edges blend away. CSP-safe: no inline style. Theme-aware. */
.lince-attrib {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-elev);
  padding: 1rem 1.25rem;
  box-shadow: var(--shadow-sm);
}
/* White plate behind the logo — its rounded corners + padding let the logo's
   own near-white edges dissolve into a clean, undistorted mark. */
.lince-attrib-plate {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .5rem;
  background: #fff;
  border-radius: 14px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .14), inset 0 0 0 1px rgba(0, 0, 0, .04);
}
.lince-attrib-logo {
  display: block;
  width: 96px;
  height: 96px;
  object-fit: contain; /* square source (1254×1254) — never stretched */
}
.lince-attrib-body { flex: 1 1 240px; min-width: 220px; }
.lince-attrib-note {
  margin: 0 0 .5rem;
  font-size: .95rem;
  line-height: 1.5;
}
.lince-attrib-facts {
  display: flex;
  flex-wrap: wrap;
  /* The About page centres its text (.about { text-align: center }), but this
     row is a flex container — text-align does not position flex items, so it
     defaulted to flex-start and sat off-axis against the centred note/line
     above and below it. Centre the items explicitly to match. */
  justify-content: center;
  gap: .3rem 1.4rem;
  margin: 0 0 .4rem;
  font-size: .8rem;
}
.lince-attrib-facts > div { display: flex; flex-direction: column; gap: .05rem; }
.lince-attrib-facts dt {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .68rem;
}
.lince-attrib-facts dd { margin: 0; font-weight: 600; }
.lince-attrib-line { margin: 0; }

/* Consent checkboxes (register/apply). The label must sit on the SAME line as
   its box — a checkbox whose text wraps to the next line reads as an unrelated
   paragraph — and smaller than the form fields, because these are assents
   rather than data entry. `align-items: flex-start` keeps a wrapped two-line
   consent aligned to the box rather than centring it oddly. */
.consent-row {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 8px;
  font-size: 0.86em;
  line-height: 1.4;
  margin: 0;
}
.consent-row > input[type="checkbox"] {
  width: auto;
  flex: 0 0 auto;
  margin: 2px 0 0;
}
.consent-row > span { display: inline; }
.consent-row.has-error { color: var(--danger, #e5484d); }
.consent-error { margin: -4px 0 0 24px; }

/* ── Register: reg-traditional ─────────────────────────────────────────────
   A deliberately old-fashioned document. Not a "theme" (that axis is
   light/dark) but a third REGISTER, orthogonal to it: this is what the portal
   looks like when it stops trying to look like anything.

   The rules are subtractive on purpose — no gradients, no shadows, no
   transitions, no rounded corners, no web fonts, no sticky chrome. System
   fonts at browser-default sizing, blue underlined links, visited links
   purple, boxed tables with visible rules. The reference is a university
   department page circa 2003, which is not a joke: that idiom is legible at
   any zoom, prints correctly, degrades to plain HTML, and asks nothing of a
   slow connection or an old machine.

   Accessibility note: this register keeps WCAG AA contrast on purpose. Period
   authenticity stops where legibility starts — a 2003 page nobody can read is
   a costume, not an option.

   Gamification is NOT hidden here (see gamification section below): hiding it
   would create a second content model to maintain forever. It is rendered as
   plain text and tables instead — the information without the arcade.
   ───────────────────────────────────────────────────────────────────────── */

body.reg-traditional {
  font-family: Georgia, "Times New Roman", Times, serif;
  font-size: 16px;
  line-height: 1.5;
  background: #ffffff;
  color: #111111;
  max-width: 62em;
  margin: 0 auto;
  padding: 0 1em;
}

/* Kill every modern affordance in one sweep, then re-add only what a document
   genuinely needs. Cheaper and more honest than overriding rule by rule. */
body.reg-traditional *,
body.reg-traditional *::before,
body.reg-traditional *::after {
  border-radius: 0 !important;
  box-shadow: none !important;
  text-shadow: none !important;
  transition: none !important;
  animation: none !important;
  backdrop-filter: none !important;
  background-image: none !important;
  letter-spacing: normal !important;
}

body.reg-traditional h1,
body.reg-traditional h2,
body.reg-traditional h3 {
  font-family: Georgia, "Times New Roman", Times, serif;
  font-weight: bold;
  color: #000;
  margin: 1em 0 0.4em;
  text-transform: none;
}
body.reg-traditional h1 { font-size: 1.8em; border-bottom: 2px solid #000; }
body.reg-traditional h2 { font-size: 1.4em; border-bottom: 1px solid #666; }
body.reg-traditional h3 { font-size: 1.15em; }

/* Links behave the way a 2003 reader expects: blue, underlined, purple once
   visited. The visited state is genuinely useful navigation information that
   modern designs threw away. */
body.reg-traditional a { color: #0000ee; text-decoration: underline; }
body.reg-traditional a:visited { color: #551a8b; }
body.reg-traditional a:hover { color: #cc0000; }

/* Cards become boxes; the grid becomes a stack. */
body.reg-traditional .card,
body.reg-traditional .panel,
body.reg-traditional .tile,
body.reg-traditional section.card {
  background: #ffffff;
  border: 1px solid #999999;
  padding: 0.75em;
  margin: 0 0 1em;
}
body.reg-traditional .grid,
body.reg-traditional .cards,
body.reg-traditional .tile-grid {
  display: block !important;
}

/* Tables get visible rules — the point of a table in this idiom. */
body.reg-traditional table { border-collapse: collapse; width: 100%; margin: 1em 0; }
body.reg-traditional th,
body.reg-traditional td { border: 1px solid #999; padding: 4px 8px; text-align: left; }
body.reg-traditional th { background: #e8e8e8; font-weight: bold; }

/* Buttons look like form controls, because that is what they are. */
body.reg-traditional button,
body.reg-traditional .btn,
body.reg-traditional button.primary,
body.reg-traditional .btn-primary,
body.reg-traditional input[type="submit"] {
  font-family: inherit;
  font-size: 1em;
  background: #e0e0e0;
  color: #000;
  border: 2px outset #cccccc;
  padding: 2px 10px;
  cursor: pointer;
}
body.reg-traditional button:active,
body.reg-traditional .btn:active { border-style: inset; }

body.reg-traditional input[type="text"],
body.reg-traditional input[type="email"],
body.reg-traditional input[type="password"],
body.reg-traditional input[type="tel"],
body.reg-traditional select,
body.reg-traditional textarea {
  font-family: inherit;
  font-size: 1em;
  background: #fff;
  color: #000;
  border: 1px inset #999;
  padding: 2px 4px;
}

/* Navigation: a plain horizontal list, not sticky chrome. */
body.reg-traditional .topnav,
body.reg-traditional .site-footer {
  background: #f0f0f0;
  border: 1px solid #999;
  position: static !important;
  padding: 0.4em 0.6em;
}
body.reg-traditional .nav-links { display: block; }
body.reg-traditional .nav-links a { margin-right: 0.8em; }

/* Badges and pills become bracketed text — the information, not the sticker. */
body.reg-traditional .badge,
body.reg-traditional .chip,
body.reg-traditional .pill {
  background: none;
  border: none;
  color: inherit;
  font-size: 1em;
  padding: 0;
}
body.reg-traditional .badge::before,
body.reg-traditional .chip::before { content: "["; }
body.reg-traditional .badge::after,
body.reg-traditional .chip::after { content: "]"; }

/* Gamification, rendered as a register rather than removed. Progress bars
   become a labelled number; crests and heralds shrink to inline marks. The
   learner still sees their standing — they just see it the way a printed
   transcript would show it. */
body.reg-traditional .progress,
body.reg-traditional .xp-bar,
body.reg-traditional .progress-track {
  height: auto !important;
  background: none !important;
  border: none !important;
}
body.reg-traditional .progress-fill,
body.reg-traditional .xp-fill { display: none !important; }
body.reg-traditional .crest,
body.reg-traditional .herald,
body.reg-traditional .emblem { max-width: 48px; height: auto; }
body.reg-traditional .lightbox-trigger { cursor: default; }

/* Splash and decorative motion have no place in a document. */
body.reg-traditional .bootsplash,
body.reg-traditional .boot-splash,
body.reg-traditional .page-transition,
body.reg-traditional .parallax,
body.reg-traditional .hero-video { display: none !important; }

@media print {
  body.reg-traditional .topnav,
  body.reg-traditional .site-footer { display: none; }
}

/* A verified sender's own email, shown but not editable (enquiry modal + the
   About contact form). Muted so it reads as "this is who you are", not as a
   field someone forgot to fill in. */
input.is-locked {
  background: var(--bg-elev);
  color: var(--muted);
  cursor: default;
}

/* Theory chapters on the phase page. The body is the course itself, so it gets
   reading measure and generous rhythm; the <summary> stays compact so a phase
   with a dozen chapters still scans as a table of contents when collapsed. */
.theory-chapter {
  border-top: 1px solid var(--border);
  padding: 0.35rem 0;
}
.theory-chapter:first-of-type { border-top: 0; }
.theory-chapter > summary {
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 0.2rem;
  list-style: revert;
}
.theory-chapter > summary:hover { color: var(--accent); }
.theory-chapter-title { font-weight: 600; }
.theory-chapter-file { opacity: 0.7; }
/* WIDTH: the course body fills its card, like every other panel on the page.
   It was capped at 68ch — a reading measure borrowed from prose typography —
   which left the widest content on the site (diagrams, wide tables, 100-column
   code) squeezed into a ribbon with a third of the card empty beside it, and
   made the theory card visibly narrower than "Your result" directly above it.
   Long-line comfort is handled where it belongs: `p`/`li` get their own measure
   below, so paragraphs stay readable without shrinking figures and tables. */
.theory-body {
  padding: 0.25rem 0.2rem 1.25rem;
}
.theory-body img { max-width: 100%; height: auto; }
/* Inline figures (LGE). `img` had a rule and `svg` never did, so a figure with a
   viewBox and no width/height was laid out at the CSS default for a replaced
   element rather than at the width of its column — which is why the first
   diagrams to ship read at roughly half the size of the prose beside them.
   Filling the container is the whole fix: the figure is authored at ~1200 units
   wide, so at this width its labels land within a point or two of body text. */
.theory-body svg {
  display: block;
  width: 100%;
  height: auto;
  margin: var(--space) 0;
}
.theory-body pre,
.theory-body table { overflow-x: auto; display: block; max-width: 100%; }

/* ── Program identity on the study surfaces (dashboard + phase) ───────────
   "Which degree am I in?" had no answer anywhere on either page. The answer is
   set as a rubric line ABOVE the page title — small-caps label, then the
   program, then its level — the way a faculty name sits above the heading of a
   printed academic document. Deliberately not a card, a chip or a banner: it is
   a line of the document's own front matter, in the classic register the rest
   of these pages use (cf. .phase-group-head, .phase-nav-dir). */
.study-program-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.3rem;
}
.study-program-label {
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--muted);
}
.study-program-name {
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 700;
  color: var(--accent);
}
.study-program-level {
  font-size: 0.78rem;
  color: var(--muted);
}
/* Hairline separator rather than a bullet glyph: it reads as typography, not
   as punctuation the learner has to parse. */
.study-program-level::before {
  content: "";
  display: inline-block;
  width: 1px;
  height: 0.72em;
  margin-right: 0.5rem;
  vertical-align: baseline;
  background: var(--border);
}

/* Program switcher — the learner's own enrollments, one row each. Ruled rows in
   the register of the phase table, not a grid of marketing cards. */
.program-switch-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.program-switch-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 0.2rem;
  border-top: 1px solid var(--line, var(--border));
}
.program-switch-item:first-child { border-top: 0; }
.program-switch-item.is-current { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.program-switch-link {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  flex: 1 1 auto;
  text-decoration: none;
  color: var(--fg);
}
.program-switch-link:hover .program-switch-title { color: var(--accent); }
.program-switch-title { font-weight: 600; }
.program-switch-level { font-size: 0.78rem; color: var(--muted); }
.program-switch-chip { flex: 0 0 auto; }
.program-switch-foot { margin: 0.7rem 0 0; }

/* Scope warning on a phase read outside the learner's active program: their
   quizzes and marks still belong to the other program (routes/dashboard.py). */
.program-scope-note {
  display: block;
  border-left: 3px solid var(--accent);
  font-size: 0.86rem;
}

/* ══ Heraldic register: classic cuts on the crest wall (2026-08-12) ═══════ */
/*
   Two defects on `/me/dashboard`, both the same mistake — a MODERN emblem
   printed on the CLASSIC, cream-paper register:

   1. the role pill wore the neon `badges/role/*` badge on parchment;
   2. the programme crest was the procedural shield drawn for a dark page, so
      "MSc Applied AI Systems" read as a pale-lilac outline with an invisible
      near-white charge.

   Python now resolves BOTH from `resolve_ui(request)` (gamification/ui.py), so
   what is left here is only the presentation of the raster that arrives in its
   place. Rules live in portal.css rather than gamification.css so they load
   before it and lose to nothing they should not; the `body.reg-classic`
   prefixes carry their own specificity where it matters.
*/

/* The programme's own cut crest, standing in for `.gx-crest-svg`. Same box, so
   a strip mixing cut crests and procedural fallbacks still aligns. */
.gx-crest-img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: contain;
}
/* State is carried by the SAME file — greyed / dimmed, never a second asset
   (GAMIFICATION_USAGE_PLAN §5.2), matching `.gx-badge.is-locked`. */
.gx-crest[data-state="earned"] .gx-crest-img {
  filter: drop-shadow(var(--gx-crest-shadow));
}
/* Barely softened, deliberately. "Washed out" was the DEFECT being fixed here,
   and the caption already names the state in words — the dimming is a nicety,
   not the signal (spec §8.1). */
.gx-crest[data-state="in_progress"] .gx-crest-img { filter: opacity(0.9); }
.gx-crest[data-state="locked"] .gx-crest-img {
  filter: grayscale(1) brightness(0.9) opacity(0.55);
}

/* Role pill: the classic herald is not square (≈1024×925), so it must letterbox
   inside the 34px slot instead of being squashed to it. Harmless on the modern
   badge, which already is square. */
.gx-role-badge-img { object-fit: contain; }
/* On parchment the pill's own plate is the paper — an elevated chip behind an
   engraved herald reads as a sticker on a printed page. */
body.reg-classic .gx-role-badge {
  background: transparent;
  border-color: var(--o-rule-faint, var(--border));
}

/* ── Theory maths + fence language (theory_render.py) ─────────────────────
   Formulas are server-rendered MathML — no MathJax, no KaTeX, no webfont, no
   script: the CSP is `script-src 'self'` and a client-side renderer would have
   meant vendoring ~1.2 MB of JS and fonts under static/. The browser draws
   these natively, so there is very little to style — but two things it will
   not do for us:

   1. A display formula is a `<math display="block">`, which is a block box that
      does NOT scroll. `\begin{pmatrix}` over four columns, or a long derivation
      with `\qquad` spacing, is wider than 68ch and would push the whole phase
      page sideways on a phone. Same treatment `.theory-body pre` already gets,
      for the same reason.
   2. Inline maths inherits the paragraph's line-height, and a fraction or a
      `\sum` with limits is taller than a line of prose, so consecutive lines
      collide. Letting the line box grow is the fix; setting a fixed larger
      line-height on the paragraph is not, because it would loosen the prose. */
.theory-body math[display="block"] {
  display: block;
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
  margin: 1rem 0;
  padding: 0.15rem 0;
}
.theory-body math { line-height: normal; }

/* ── The other half of (1): INLINE maths, which cannot be given a scrollport
   of its own ──────────────────────────────────────────────────────────────
   The rule above only ever caught `<math display="block">`. It works — a wide
   display formula clips and scrolls inside its own box and never widens the
   page. What went unguarded is *inline* maths, and that is what the QA pass of
   2026-08-13 measured as page-level horizontal scroll on a 390px viewport:
   30px on /phase/5, 179px on /phase/14, 296px on /phase/15, 204px on
   /phase/16, 489px on /phase/18, 422px on /phase/21. Authors write genuinely
   long expressions inline — `32^2 + 32·16 + 64·32 + 32 + 64 = 3680` measures
   516px in a 283px column — and an inline box has no break opportunity inside
   it, so it pushes the DOCUMENT sideways. (Verified by elimination: hiding
   every `display="block"` formula left the overflow untouched; hiding inline
   maths as well took it to zero.)

   The scrollport therefore has to go on the CONTAINING BLOCK, not on the
   formula. `math { display: inline-block; overflow-x: auto }` is the obvious
   alternative and is wrong: CSS makes the baseline of an inline-block whose
   overflow is not `visible` its bottom MARGIN edge instead of its last line
   box, so every inline formula with a descender or a subscript rides up off
   the text baseline — measured at 4–5px on 171 of the 220 inline formulas on
   /phase/14 alone. Scrolling the paragraph moved nothing: 0 of 220 drifted,
   and the document height was identical to the pixel at both widths.

   `li` is deliberately NOT in this list even though list items overflow too:
   its ::marker is painted OUTSIDE the content box, so `overflow` on the `li`
   clips every bullet away (screenshot-verified). The scrollport goes on the
   `ul`/`ol` instead — the markers live inside ITS padding box, so they survive
   and the offending row still scrolls. */
.theory-body p,
.theory-body ul,
.theory-body ol {
  overflow-x: auto;
}
/* Prose fills the card. The 78ch reading measure that used to sit here is the
   textbook advice, but at this body size it wrapped around 800px inside a
   ~1050px card, leaving a quarter of every course page visibly empty to the
   right of the text while the card's border ran on past it. Owner's call: use
   the space. Figures, tables and code already took the full width, so the prose
   was also the only thing that looked narrower than everything around it. */
.theory-body > p,
.theory-body > ul,
.theory-body > ol,
.theory-body > blockquote {
  max-width: 100%;
}
/* Headings and figure captions carry the same hazard from a different source:
   a long inline `<code>` path (`src/minimodel/positional/sinusoidal.py`, 529px)
   in an `h2`, and arrow-joined phrases (`logits→temperature→top-k`) in a
   caption. Neither is a scroll candidate — a heading that scrolls sideways
   reads as broken — so these get to BREAK instead. `break-word`, never
   `anywhere`: `anywhere` also shrinks min-content and chops ordinary prose
   mid-syllable, which is the same conclusion the handbook tables reached
   (tests/test_ux_chrome.py::test_handbook_body_contains_a_wide_table…). */
.theory-body h1,
.theory-body h2,
.theory-body h3,
.theory-body h4,
.theory-body h5,
.theory-body h6,
.theory-body figcaption {
  overflow-wrap: break-word;
}
/* The reader-visible half of the LaTeX fallback: when a formula does not
   convert, theory_render emits its SOURCE in a `language-latex` block rather
   than a blank space. Marking it as *source* stops it being mistaken for the
   rendered formula. */
.theory-body code.language-latex { opacity: 0.85; }
/* Mermaid diagrams (243 files) render as their source today — nothing draws
   them. The class is what makes that a later, purely additive decision; until
   then, label the block so a reader knows they are looking at diagram source
   and not at a code sample they are meant to run. */
.theory-body pre:has(> code.language-mermaid)::before {
  content: "diagram source";
  display: block;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.6;
  margin-bottom: 0.35rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   Owner-supplied heraldic marks (2026-08-12)
   ──────────────────────────────────────────────────────────────────────────
   Two marks were handed over as artwork rather than as code: the shield-and-L
   crest for the header (beside the LynxCortex wordmark, which is UNCHANGED)
   and the column-"L" for the About masthead. Both replace hand-drawn inline
   SVG. They are painted as backgrounds, not <img>, for one reason: the ground
   the portal paints is not constant, so the mark has to swap with it, and a
   background swaps in CSS while an `src` does not.

   The source art is a flat-ink alpha silhouette — every visible pixel is the
   same near-black navy (#001b43 / #09172c), all of the drawing lives in the
   alpha channel. That makes the light-on-dark cut a straight inversion of the
   ink rather than new art: #001b43 -> #ffe4bc, #09172c -> #f6e8d3, alpha
   untouched. Shipped as baked files (`*-ivory.png`), not a CSS filter, so the
   drop-shadow below can stay a drop-shadow instead of fighting an invert().

   WHICH GROUND, NOT WHICH THEME. `data-theme="dark"` alone is the wrong test:
   skin-classic.css repoints --bg onto cream for `body.reg-classic` in BOTH
   themes ("dark mode is not a thing a printed prospectus has"), so the ivory
   cut under classic+dark would be ivory on cream — invisible. The dark cut is
   therefore gated on dark theme AND a register that is not the always-cream
   one.
   ══════════════════════════════════════════════════════════════════════════ */

/* Header crest. The SVG children are hidden rather than removed because the
   markup lives in _partials/nav.html; the element, its class, its
   aria-hidden and the wordmark next to it are all untouched. 22x30 box for a
   0.713 portrait mark = 21.4x30 painted, +3px on the 27px square it replaces.
   The art ships at 3x (64x90) so it stays crisp to DPR 3. */
.brand-mark.brand-crest {
  width: 22px;
  height: 30px;
  background: url("/static/brand/l-mark-navy.png") center / contain no-repeat;
  /* The inherited gold glow muddies a keyline mark on paper — it belongs to
     the dark ground only, where it reads as candlelight rather than as fringe. */
  filter: none;
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

:root[data-theme="dark"] body:not(.reg-classic) .brand-mark.brand-crest {
  background-image: url("/static/brand/l-mark-ivory.png");
  filter: drop-shadow(0 0 5px color-mix(in srgb, #d8b24a 40%, transparent));
}

/* About masthead mark. 116x170 box for a 0.680 portrait mark = 115.6x170
   painted; taller than the 150px square crest it replaces because the art is
   a single narrow glyph and a 150px cap would have read as smaller, not
   equal. Art ships at 2x (231x340). */
.about-crest {
  display: block;
  width: 116px;
  height: 170px;
  background: url("/static/brand/about-mark-navy.png") center / contain no-repeat;
  filter: none;
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

:root[data-theme="dark"] body:not(.reg-classic) .about-crest {
  background-image: url("/static/brand/about-mark-ivory.png");
  filter: drop-shadow(0 0 18px color-mix(in srgb, #d8b24a 28%, transparent));
}

/* Paper is paper: print always gets the navy ink, whatever the screen ground
   was, and print-color-adjust above keeps the background from being dropped. */
@media print {
  .brand-mark.brand-crest {
    background-image: url("/static/brand/l-mark-navy.png");
    filter: none;
  }
  .about-crest {
    background-image: url("/static/brand/about-mark-navy.png");
    filter: none;
  }
}

/* ── Admonitions (MkDocs Material syntax) ─────────────────────────────────
   2,722 callouts across 1,338 files use `!!! note` / `!!! warning` / `???`.
   Until theory_render learned the syntax they printed as literal `!!! …`
   prose with their bodies in an indented code block; this is the styling half
   of that fix. The look is deliberately close to Material's, because that is
   what the go-deep reference sites render and the owner asked the course pages
   to match them.

   Per-kind hue via a single `--adm` custom property, so a new kind is one line
   and the shared box rules never fork. `color-mix` against `--card` keeps the
   tint legible in both the classic and modern registers instead of hard-coding
   a pale background that only works on one. */
.theory-body .admonition {
  --adm: var(--accent);
  border-left: 4px solid var(--adm);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--adm) 8%, var(--card));
  padding: var(--space-sm) var(--space);
  margin: var(--space) 0;
}
.theory-body .admonition > .admonition-title {
  font-weight: 700;
  color: var(--adm);
  margin: 0 0 var(--space-xs);
  /* Not uppercase: authored titles are whole sentences ("It threw an error"),
     and small-caps on a sentence reads as shouting rather than as a label. */
  letter-spacing: 0.01em;
}
/* The last child's bottom margin would otherwise push the callout's floor away
   from its text and make every box look bottom-heavy. */
.theory-body .admonition > :last-child { margin-bottom: 0; }
.theory-body .admonition > pre,
.theory-body .admonition > table { max-width: 100%; }

.theory-body .admonition-note     { --adm: var(--accent); }
.theory-body .admonition-abstract,
.theory-body .admonition-info     { --adm: #0288d1; }
.theory-body .admonition-tip,
.theory-body .admonition-success  { --adm: #00897b; }
.theory-body .admonition-question { --adm: #64b5f6; }
.theory-body .admonition-warning  { --adm: #ef6c00; }
.theory-body .admonition-failure,
.theory-body .admonition-danger,
.theory-body .admonition-bug      { --adm: #c62828; }
.theory-body .admonition-example  { --adm: #7b1fa2; }
.theory-body .admonition-quote    { --adm: var(--muted); }

/* ── Authored unit timings ────────────────────────────────────────────────
   `<span class="cx-time">⏱ 45 minutes</span>` opens 14 units. It survives the
   sanitiser but portal.css had no rule for it, so it rendered as an
   undifferentiated line of prose directly under the unit title — which is what
   made the bootcamp headers look unfinished next to a go-deep page. A quiet
   chip is enough: it is metadata about the unit, not part of its argument. */
.theory-body .cx-time {
  display: inline-block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 0.15em 0.7em;
  white-space: nowrap;
}

/* Mermaid renders into a <figure class="mermaid-figure">; the SVG itself is
   covered by the .theory-body svg rule, so this only owns the surround. The
   source <pre> is left in place when a render fails, which is why there is no
   rule hiding it. */
.theory-body .mermaid-figure {
  margin: var(--space) 0;
  padding: var(--space-sm);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
}
/* Size is set by mermaid's own font scale, NOT by stretching the SVG.
   Stretching was tried first and is wrong: an SVG scaled to the column
   multiplies the text with the boxes, so a five-node flowchart with a ~230px
   natural width blew up 4.5x and put one node per screen (screenshot-verified).
   A bigger base font grows the diagram proportionally instead, and mermaid's
   own useMaxWidth still shrinks a genuinely wide diagram to fit.

   `.theory-body svg { width: 100% }` — the LGE rule — must NOT reach these, for
   the same reason: those figures are authored at ~1200 units and need the
   column; a mermaid diagram carries its own intrinsic size. */
.theory-body .mermaid-figure svg {
  width: auto;
  height: auto;
  margin: 0 auto;
}

/* Contrast safety net (C3 + BL visual-fix #5/#7). The JS pins mermaid's
   themeVariables, but node LABEL text is the ink that actually goes missing:
   with htmlLabels:false mermaid emits SVG <text> and its own stylesheet sets
   `.label { color: … }` yet NO `fill` on the node's <text>, so every node
   label falls back to the SVG default (black). classDef nodes survive only
   because their authored fills are light; an un-classDef'd DECISION rhombus
   ("NX / DEP on the stack?") takes the themeVariables node fill — dark blue on
   the dark theme — and reads black-on-dark, unreadable.

   The fix is universal, not per-theme: map the SVG text's `fill` to the
   `color` mermaid already computes (`currentColor`). For a default node that
   color is nodeTextColor (light in dark theme, dark in light theme); for a
   classDef node it is the authored `color:` (applied !important on the label).
   So EVERY node type — decision, special-fill, default — gets label text that
   contrasts with its own fill in all four theme×register combinations, and any
   diagram-authored colour is honoured rather than stomped. Not scoped to
   `.theory-body`, so it also covers the go-deep / reference render path, which
   reuses the same portal.js mermaid init and the same `.mermaid-figure`. */
.mermaid-figure svg .node .label,
.mermaid-figure svg .node .label text,
.mermaid-figure svg .node .nodeLabel,
.mermaid-figure svg .node text {
  fill: currentColor;
}

/* Edge / arrow inks and label chips only go pale on a LIGHT page (the classic
   register's cream, or the light theme). Pin them dark there; in the dark
   theme mermaid's own light lineColor already clears contrast, so this must
   NOT fire (it would paint dark strokes on a dark ground). */
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .edgePath .path,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .flowchart-link,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg line,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .relation {
  stroke: #1f2937 !important;
}
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .arrowheadPath,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg marker path,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .marker {
  fill: #1f2937 !important;
  stroke: #1f2937 !important;
}
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .edgeLabel,
:root:not([data-theme="dark"]) .theory-body .mermaid-figure svg .edgeLabel rect {
  background-color: #ffffff !important;
  fill: #ffffff !important;
}
/* Node outlines: a mermaid node whose fill is close to the page needs a visible
   border on both registers. */
.theory-body .mermaid-figure svg .node rect,
.theory-body .mermaid-figure svg .node circle,
.theory-body .mermaid-figure svg .node polygon,
.theory-body .mermaid-figure svg .node path {
  stroke-width: 1.5px;
}

/* ── Programme version changelog + enrolment pin (BL-025) ──────────────────
   The version chip said WHICH version a programme is on; these two blocks say
   what changed between versions, and which version the learner's own enrolment
   bought. No new palette: the release badges reuse the lifecycle `.badge-*`
   variants the catalogue chip already uses, so a release reads the same colour
   wherever it appears. */
.catd-pin {
  margin: 0 0 var(--space-4, 1rem);
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  background: color-mix(in srgb, var(--ok) 6%, transparent);
}
.catd-pin-line { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.catd-pin-note { margin: 6px 0 0; font-size: 0.9rem; }
.catd-pin-since-heading {
  margin: 10px 0 4px;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.catd-releases { margin: var(--space-5, 1.5rem) 0 0; }
.catd-releases-heading { margin: 0 0 4px; font-size: 1.1rem; }
/* A release list is a table of facts read left to right — version, state, date,
   then the prose. Flex rather than a grid so a long note wraps under itself
   instead of stretching every other row's columns. */
.catd-changelog { margin: 8px 0 0; padding: 0; list-style: none; }
.catd-changelog-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 6px 0;
  border-top: 1px solid var(--border);
}
.catd-changelog-entry time { color: var(--muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.catd-changelog-state { font-size: 0.85rem; }
.catd-changelog-note { flex: 1 1 16rem; min-width: 0; }

/* The dashboard header carries the pin inline beside the programme name — a
   chip, not a panel: the full history lives on the catalogue detail page. */
.study-program-version { margin-left: 6px; vertical-align: middle; }
.study-program-version-note { margin-left: 6px; font-size: 0.85rem; }

/* ── "What changed if I re-enrol" (BL-027) ─────────────────────────────────
   The version page reuses BL-025's pin panel and changelog list wholesale —
   same `.catd-pin`, same `.catd-changelog` rows — so a release reads identically
   here and on the programme's detail page. What is new is only the pair of
   labels being compared, and the consent block under them. */
.verup-pins .catd-pin-line { font-size: 1.05rem; }
.verup-arrow { color: var(--muted); }

/* The consent block is deliberately quiet: it is a relabel, not a purchase, and
   styling it as a call to action would oversell what the button does. */
.verup-choice {
  margin: var(--space-5, 1.5rem) 0 0;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
}
.verup-consent { margin: 6px 0 12px; font-size: 0.92rem; color: var(--muted); }
.verup-form { gap: 12px; }
.verup-confirm { display: flex; align-items: flex-start; gap: 8px; }
.verup-confirm input { margin-top: 3px; flex: none; }
/* The no-op state. It has to be legible, not apologetic — "you are already on
   the newest release" is an answer, and rendering it as an error would tell a
   learner something went wrong when nothing did. */
.verup-noop { margin: var(--space-5, 1.5rem) 0 0; }

.catd-pin-cta { margin: 12px 0 0; }
.study-program-version-link { margin-left: 6px; font-size: 0.85rem; }
/* ── Study velocity (BL-033): the pace a learner sets for THEMSELVES ────────
   Lives in portal.css rather than a velocity.css because the stylesheet link
   would have to go in base.html, and the pace picker renders on three surfaces
   (/apply, the onboarding wizard, /me/velocity) that already load this file.
   Deliberately quiet: this is a preference, not an achievement, and it must not
   read as a scoreboard. */
.velocity-choice {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.9rem 1rem 1rem;
  margin: 0;
  display: grid;
  gap: 0.5rem;
}
.velocity-choice > legend {
  padding: 0 0.4rem;
  font-weight: 600;
}
.velocity-option {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.velocity-option:hover,
.velocity-option.is-chosen {
  border-color: var(--accent, currentColor);
}
.velocity-option > input {
  margin-top: 0.25rem;
  flex: 0 0 auto;
}
.velocity-option-body {
  display: grid;
  gap: 0.15rem;
}
.velocity-option-name {
  font-weight: 600;
}
.velocity-actions,
.velocity-clear {
  margin-top: 0.75rem;
}
.velocity-eta-line {
  margin: 0.25rem 0 0.35rem;
}

/* ── Guided onboarding (BL-042) ─────────────────────────────────────────────
   The first-visit strip and the two wizard steps. The banner is a note, not an
   alert: a new learner should read it as a shortcut being offered, not as
   something being wrong with their account. */
.onboarding-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.7rem 1rem;
  margin-bottom: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
}
.onboarding-banner-text {
  margin: 0;
}
.onboarding-domains {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 0.6rem;
}
.onboarding-domain {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
}
.onboarding-domain:hover {
  border-color: var(--accent, currentColor);
}
.onboarding-domain-name {
  font-weight: 600;
}

/* Phase-page action row (task: per-phase graded quiz access). "Submit a problem"
   (a <details> that expands to a form) sits on the left; the primary "Take the
   test" call-to-action sits opposite on the right. space-between keeps them at
   the two ends; align-items:flex-start keeps the button pinned to the top edge
   while the problem form expands downward; it wraps to a stack on narrow
   viewports. */
.phase-actions {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 1.5rem 0;
}
.phase-actions > .problem-report-box { flex: 1 1 auto; margin: 0; }
.phase-take-test { flex: 0 0 auto; white-space: nowrap; align-self: flex-start; }
