:root {
  --bg: #ddf0fb;
  --ink: #23311f;
  --soft: rgba(35, 49, 31, 0.14);
  --line: #24371f;
  --a: #274624;
  --b: #eef5d7;
  --accent: #cc9134;
  --accent-strong: #93591d;
  --phosphor: #9fda7a;
  --screen-glow: rgba(169, 221, 132, 0.14);
  --panel-bg: rgba(251, 247, 225, 0.9);
  --panel-bg-strong: rgba(239, 231, 196, 0.96);
  --cursor-default: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M5 3.5 16.6 13.1H11.5l2.5 6.4-2.8 1.1-2.5-6.4-3.5 3.1Z' fill='%23b7d5c0' stroke='%23253a2e' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 5 3, default;
  --cursor-crosshair: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Ccircle cx='14' cy='14' r='2.6' fill='%2398c3a6' fill-opacity='0.22' stroke='%23253a2e' stroke-width='1'/%3E%3Cpath d='M14 2v7M14 19v7M2 14h7M19 14h7' stroke='%2398c3a6' stroke-width='1.6' stroke-linecap='round'/%3E%3Cpath d='M14 4.5v5M14 18.5v5M4.5 14h5M18.5 14h5' stroke='%23253a2e' stroke-width='0.8' stroke-linecap='round' opacity='0.6'/%3E%3C/svg%3E") 14 14, crosshair;
  --cursor-pointer: pointer;
}

* {
  box-sizing: border-box;
}

.hidden {
  display: none !important;
}

html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 55%, rgba(167, 217, 119, 0.16), transparent 32%),
    linear-gradient(180deg, #72c5f0, #c9edf8 52%, #ebf4d0);
  font-family: "Trebuchet MS", "Avenir Next Condensed", "Gill Sans", sans-serif;
  color: var(--ink);
  cursor: var(--cursor-default);
}

/* The full-screen scanline overlay that used to sit here (body::before, a
   1px/5px soft-light gradient at 0.45 opacity — invisible at any normal
   viewing distance) is gone: a fixed full-screen layer over a canvas that
   repaints every frame makes the display compositor re-composite the whole
   screen each frame. Together with the canvas `filter` below it kept Chrome's
   Viz compositor thread ~87% busy on the loss scenes (traced 2026-09-26,
   7.1 s of an 8.2 s window; 1.0 s with both removed), which on an old
   integrated GPU showed up as the playfield and side desks flickering (Erik's
   1280x585 laptop). The vignette (body::after) stays: visible, and cheap
   (~0.6 s of that window). */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 91;
  background:
    radial-gradient(circle at 50% 48%, transparent 48%, rgba(40, 63, 34, 0.15) 100%),
    radial-gradient(circle at 50% -10%, rgba(255, 255, 255, 0.32), transparent 38%);
  opacity: 0.78;
}

body.title-active canvas {
  cursor: var(--cursor-default);
}

canvas {
  position: fixed;
  inset: 0;
  display: block;
  width: 100vw;
  height: 100vh;
  cursor: var(--cursor-crosshair);
  touch-action: none;
  /* No CSS filter on the game canvas (was saturate(1.02) contrast(1.04) —
     imperceptible). A filtered full-screen canvas is re-rasterized by the
     compositor on every frame; see the body::after note above for the trace. */
}

#plot {
  z-index: 1;
}

#fx-overlay {
  /* Sits above the HUD docks (z-index 21) so point bursts and +N popups stay
     visible, but BELOW the title screen (80), offline report (85), and
     milestone overlays (100) so those modal panels aren't pierced by
     particles flying out of the workbench. */
  z-index: 75;
  pointer-events: none;
  cursor: default;
  filter: none;
}

.title-screen {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background:
    radial-gradient(circle at 20% 20%, rgba(96, 120, 106, 0.18), transparent 28%),
    radial-gradient(circle at 78% 26%, rgba(53, 87, 70, 0.16), transparent 24%),
    linear-gradient(180deg, rgba(219, 228, 214, 0.66), rgba(219, 228, 214, 0.94));
}

.title-screen.hidden {
  display: none;
}

/* Invisible 80×80 corner trigger that opens the debug/cheat panel.
   No background, no border, no visible affordance — only the cursor
   change on hover hints at its existence. */
.title-screen__cheat-trigger {
  position: absolute;
  top: 0;
  right: 0;
  width: 80px;
  height: 80px;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  cursor: var(--cursor-pointer);
  z-index: 90;
  outline: none;
}
.title-screen__cheat-trigger:focus-visible {
  outline: 2px solid rgba(60, 90, 70, 0.6);
  outline-offset: -4px;
}

.cheat-panel {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 30, 22, 0.45);
}
.cheat-panel.hidden { display: none; }

.cheat-panel__panel {
  /* 420px was too narrow for the widest control in the panel: the game-version
     <select> measured 435px (it sizes to its longest option label), which
     overflowed the 418px body and produced a horizontal scrollbar on a panel
     that was already cramped. 480px clears the widest option with room to
     spare; the select rule below keeps it from ever overflowing again. */
  width: min(480px, 96vw);
  border: 1px solid rgba(26, 39, 31, 0.22);
  background: linear-gradient(180deg, rgba(232, 240, 226, 0.98), rgba(216, 226, 210, 0.98));
  box-shadow: 0 22px 54px rgba(20, 33, 25, 0.22);
  font-family: "JetBrains Mono", monospace;
  /* The panel had no height bound, so on anything shorter than ~950px it
     simply overflowed the viewport at BOTH ends — measured at 375x667 and at
     1024x600 (Chromebook), the header sat ~180px ABOVE the top, putting the
     close button out of reach, while the last sections ran ~160px below the
     fold. Cap the height, pin the header, and let the body scroll. */
  max-height: calc(100vh - 48px);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.cheat-panel__header {
  flex: 0 0 auto;
}

.cheat-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid rgba(26, 39, 31, 0.14);
}
.cheat-panel__title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(34, 52, 38, 0.85);
}
.cheat-panel__close {
  background: transparent;
  border: 0;
  font-size: 22px;
  line-height: 1;
  cursor: var(--cursor-pointer);
  color: rgba(34, 52, 38, 0.7);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .cheat-panel__close:hover { color: rgba(34, 52, 38, 1); }

.cheat-panel__body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px 16px 18px;
  /* min-height:0 is what actually lets a flex child shrink below its content
     height — without it the body refuses to scroll and the panel overflows
     regardless of max-height on the parent. */
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* Keep the scrollbar visible rather than overlay-only, so on a short screen
   it is obvious the panel continues below the fold. */
.cheat-panel__body::-webkit-scrollbar {
  width: 10px;
}
.cheat-panel__body::-webkit-scrollbar-thumb {
  background: rgba(26, 39, 31, 0.32);
  border-radius: 5px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.cheat-panel__body::-webkit-scrollbar-track {
  background: rgba(26, 39, 31, 0.08);
}

.cheat-panel__section {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cheat-panel__section-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(34, 52, 38, 0.55);
}

.cheat-panel__row {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: rgba(34, 52, 38, 0.78);
}
.cheat-panel__row span:last-child {
  font-variant-numeric: tabular-nums;
  color: rgba(34, 52, 38, 1);
}

.cheat-panel__btn {
  padding: 8px 12px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  border: 1px solid rgba(34, 52, 38, 0.35);
  background: rgba(244, 250, 234, 0.9);
  color: rgba(34, 52, 38, 0.95);
  cursor: var(--cursor-pointer);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .cheat-panel__btn:hover { background: rgba(232, 244, 220, 1); }
.cheat-panel__btn--active {
  background: rgba(74, 124, 84, 0.92);
  color: #fafff0;
  border-color: rgba(40, 70, 46, 0.7);
}
/* A <select> takes its intrinsic width from its longest <option>, and as a
   flex item it defaults to min-width:auto — so it refuses to shrink below that
   and pushes the body into horizontal scroll. Widening the panel fixes today's
   labels; this makes it structural, so adding a longer game-version name later
   truncates the control instead of reintroducing the scrollbar. */
.cheat-panel__row select.cheat-panel__btn {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
}

.cheat-panel__btn--secondary {
  background: rgba(228, 218, 200, 0.9);
}
.cheat-panel__btn--flash {
  animation: cheat-btn-flash 0.55s ease-out;
}
@keyframes cheat-btn-flash {
  0%   { background: rgba(244, 220, 120, 1); box-shadow: 0 0 0 0 rgba(244, 220, 120, 0.8); border-color: rgba(180, 130, 40, 0.85); }
  35%  { background: rgba(244, 232, 168, 1); box-shadow: 0 0 0 8px rgba(244, 220, 120, 0); }
  100% { background: rgba(228, 218, 200, 0.9); box-shadow: none; border-color: rgba(34, 52, 38, 0.35); }
}

.cheat-panel__hint {
  margin: 0;
  font-size: 11px;
  line-height: 1.4;
  color: rgba(34, 52, 38, 0.62);
}

/* Research-study consent screen — an in-game mockup of the CS 3780 IRB
   consent flow. Preview/screenshot only; the buttons just close the modal. */
.consent-overlay {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 30, 22, 0.55);
}
.consent-overlay.hidden { display: none; }

.consent-overlay__panel {
  display: flex;
  flex-direction: column;
  width: min(620px, 96vw);
  max-height: 90vh;
  border: 1px solid rgba(26, 39, 31, 0.22);
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(238, 244, 232, 0.99), rgba(224, 233, 218, 0.99));
  box-shadow: 0 26px 64px rgba(20, 33, 25, 0.32);
  font-family: "JetBrains Mono", monospace;
  overflow: hidden;
}

.consent-overlay__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid rgba(26, 39, 31, 0.16);
  background: rgba(74, 124, 84, 0.14);
}
.consent-overlay__title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(34, 52, 38, 0.9);
}
.consent-overlay__close {
  background: transparent;
  border: 0;
  font-size: 22px;
  line-height: 1;
  cursor: var(--cursor-pointer);
  color: rgba(34, 52, 38, 0.7);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .consent-overlay__close:hover { color: rgba(34, 52, 38, 1); }

.consent-overlay__body {
  padding: 18px 22px;
  overflow-y: auto;
  font-size: 12.5px;
  line-height: 1.5;
  color: rgba(34, 52, 38, 0.9);
}
.consent-overlay__body p { margin: 0 0 10px; }
.consent-overlay__meta {
  padding: 10px 12px;
  border-left: 3px solid rgba(74, 124, 84, 0.7);
  background: rgba(74, 124, 84, 0.08);
  font-size: 12px;
}
.consent-overlay__h {
  margin: 16px 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(40, 70, 46, 0.95);
}
.consent-overlay__list {
  margin: 0 0 10px;
  padding-left: 18px;
}
.consent-overlay__list li { margin-bottom: 6px; }
.consent-overlay__note {
  font-size: 11.5px;
  font-style: italic;
  color: rgba(34, 52, 38, 0.66);
}

.consent-overlay__consent {
  margin-top: 14px;
  padding: 14px;
  border: 1px solid rgba(74, 124, 84, 0.4);
  border-radius: 5px;
  background: rgba(244, 250, 234, 0.85);
}
.consent-overlay__check {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
  font-size: 12px;
  cursor: var(--cursor-pointer);
}
.consent-overlay__check input { margin-top: 2px; flex: 0 0 auto; }

.consent-overlay__footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 22px;
  border-top: 1px solid rgba(26, 39, 31, 0.16);
  background: rgba(216, 226, 210, 0.7);
}
.consent-overlay__btn {
  padding: 9px 16px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  border-radius: 4px;
  border: 1px solid rgba(34, 52, 38, 0.35);
  cursor: var(--cursor-pointer);
}
.consent-overlay__btn--ghost {
  background: transparent;
  color: rgba(34, 52, 38, 0.75);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .consent-overlay__btn--ghost:hover { background: rgba(34, 52, 38, 0.06); }
.consent-overlay__btn--primary {
  background: rgba(74, 124, 84, 0.95);
  color: #fafff0;
  border-color: rgba(40, 70, 46, 0.7);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .consent-overlay__btn--primary:hover { background: rgba(64, 112, 74, 1); }
.consent-overlay__btn--primary:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .consent-overlay__btn--primary:disabled:hover { background: rgba(74, 124, 84, 0.95); }

/* In-game concept test (ui/concept-test.js) — question/option styling layered
   on top of the reused .consent-overlay panel chrome. */
.ctest-panel { width: min(680px, 96vw); }
.ctest-body { font-size: 13px; }
.ctest-q {
  margin: 16px 0;
  padding: 14px 14px 8px;
  border: 1px solid rgba(26, 39, 31, 0.16);
  border-radius: 6px;
  background: rgba(250, 253, 244, 0.7);
}
.ctest-q__prompt { font-weight: 600; margin-bottom: 10px; color: rgba(30, 46, 34, 0.95); }
.ctest-q__num { color: rgba(74, 124, 84, 0.95); font-weight: 700; }
.ctest-q__req { color: #b23b3b; font-weight: 700; }
.ctest-fig {
  margin: 4px 0 12px;
  padding: 8px;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid rgba(26, 39, 31, 0.12);
  text-align: center;
}
.ctest-fig svg { width: 100%; max-width: 360px; height: auto; }
/* Real image files (the form's hand-drawn figures) get more room than the
   inline SVGs — they are photos of drawings, and the A/B/C/D labels stop being
   readable much below this. */
.ctest-fig img { width: 100%; max-width: 480px; height: auto; display: inline-block; }
.ctest-opts { display: flex; flex-direction: column; gap: 6px; }
.ctest-opt {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 7px 9px;
  border-radius: 5px;
  cursor: var(--cursor-pointer);
  transition: background 0.12s ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .ctest-opt:hover { background: rgba(74, 124, 84, 0.1); }
.ctest-opt input { margin-top: 2px; flex: 0 0 auto; }
.ctest-progress { font-size: 12px; color: rgba(34, 52, 38, 0.7); margin-right: auto; }
.ctest-footer { align-items: center; }
.ctest-score { font-size: 14px; }
/* Feedback-survey free-response boxes (ui/feedback-survey.js). */
.fb-text {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  padding: 8px 10px;
  border: 1px solid rgba(58, 74, 62, 0.45);
  border-radius: 6px;
  background: #fdfcf7;
  color: inherit;
  resize: vertical;
}
.fb-text:focus { outline: 2px solid rgba(74, 124, 84, 0.5); }

.title-screen__panel {
  width: min(680px, 100%);
  padding: 32px 34px 28px;
  border: 1px solid rgba(26, 39, 31, 0.16);
  background: linear-gradient(180deg, rgba(224, 233, 220, 0.9), rgba(207, 219, 203, 0.92));
  box-shadow: 0 22px 54px rgba(20, 33, 25, 0.12), 0 0 40px var(--screen-glow);
  text-align: center;
  backdrop-filter: blur(8px);
}

.title-screen__eyebrow {
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(26, 39, 31, 0.58);
  text-shadow: 0 0 10px rgba(152, 195, 166, 0.08);
}

.title-screen__title {
  margin: 12px 0 10px;
  font-size: clamp(38px, 8vw, 72px);
  line-height: 0.95;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--ink);
  text-shadow: 0 0 14px rgba(152, 195, 166, 0.12);
}

.title-screen__subtitle {
  max-width: 480px;
  margin: 0 auto;
  font-size: 18px;
  line-height: 1.4;
  color: rgba(26, 39, 31, 0.74);
}

.title-screen__offline {
  margin: 18px auto 0;
  width: min(520px, 100%);
  padding: 16px 18px 14px;
  border: 2px solid rgba(131, 85, 26, 0.28);
  border-radius: 22px;
  background: linear-gradient(180deg, rgba(251, 246, 223, 0.98), rgba(236, 225, 186, 0.98));
  box-shadow: 0 14px 28px rgba(89, 60, 19, 0.12), inset 0 2px 0 rgba(255, 245, 209, 0.9);
  text-align: left;
}

.title-screen__offline.hidden {
  display: none;
}

.title-screen__offline-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(88, 57, 16, 0.92);
}

.title-screen__offline-body {
  margin-top: 6px;
  font-size: 15px;
  line-height: 1.4;
  color: rgba(53, 39, 18, 0.86);
}

.title-screen__offline-facts {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.title-screen__offline-fact {
  padding: 10px 10px 8px;
  border-radius: 16px;
  background: rgba(255, 250, 233, 0.72);
  border: 1px solid rgba(131, 85, 26, 0.18);
}

.title-screen__offline-value {
  font-size: 20px;
  font-weight: 700;
  line-height: 1;
  color: rgba(64, 99, 40, 0.96);
}

.title-screen__offline-label {
  margin-top: 4px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(92, 66, 28, 0.76);
}

.offline-report {
  position: fixed;
  inset: 0;
  z-index: 85;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background: rgba(25, 34, 22, 0.32);
  backdrop-filter: blur(4px);
}

.offline-report.hidden {
  display: none;
}

.offline-report__panel {
  width: min(680px, 100%);
  padding: 26px 28px 24px;
  border: 3px solid rgba(131, 85, 26, 0.34);
  border-radius: 30px;
  background: linear-gradient(180deg, rgba(251, 246, 223, 0.99), rgba(236, 225, 186, 0.99));
  box-shadow: 0 26px 60px rgba(53, 39, 18, 0.24), inset 0 2px 0 rgba(255, 245, 209, 0.92);
  text-align: left;
}

.offline-report__eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(88, 57, 16, 0.92);
}

.offline-report__body {
  margin-top: 10px;
}

.offline-report__button {
  margin-top: 18px;
  min-width: 180px;
}

.milestone-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background: rgba(22, 21, 19, 0.42);
}

.milestone-overlay.hidden {
  display: none;
}

.milestone-overlay__panel {
  width: min(560px, 100%);
  padding: 30px 32px 28px;
  border: 3px solid rgba(53, 136, 77, 0.42);
  border-radius: 30px;
  background: linear-gradient(180deg, rgba(239, 247, 232, 0.99), rgba(222, 238, 205, 0.99));
  box-shadow: 0 26px 60px rgba(28, 56, 30, 0.32), inset 0 2px 0 rgba(255, 255, 240, 0.85);
  text-align: center;
  animation: milestoneOverlayPanelIn 260ms ease-out;
}

.milestone-overlay__eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(42, 102, 54, 0.92);
}

.milestone-overlay__body {
  margin-top: 14px;
  font-size: 16px;
  line-height: 1.4;
  color: rgba(22, 21, 19, 0.86);
}

.milestone-overlay__button {
  margin-top: 22px;
  min-width: 180px;
}

.milestone-overlay__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

.milestone-overlay__actions .milestone-overlay__button {
  margin-top: 22px;
}

.milestone-overlay__actions .milestone-overlay__button.hidden {
  display: none;
}

.milestone-overlay__button--secondary {
  background: linear-gradient(180deg, rgba(255, 232, 142, 0.98), rgba(232, 158, 36, 0.98));
  border: 1px solid rgba(155, 95, 12, 0.85);
  color: #4a2a04;
  text-shadow: 0 1px 0 rgba(255, 252, 222, 0.6);
  box-shadow: inset 0 1px 0 rgba(255, 252, 222, 0.85), 0 6px 14px rgba(180, 110, 20, 0.28);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .milestone-overlay__button--secondary:hover {
  filter: brightness(1.06);
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255, 252, 222, 0.95), 0 10px 20px rgba(180, 110, 20, 0.34);
}

/* End-of-game finale overlay — fullscreen "Congratulations!" with the
   summary stats. Higher z than the milestone overlay so it's always on
   top once the player wraps the last assignment. */
.finale-overlay {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background:
    radial-gradient(circle at 50% 40%, rgba(255, 240, 184, 0.18), transparent 60%),
    linear-gradient(180deg, rgba(20, 32, 48, 0.78), rgba(8, 18, 32, 0.86));
  animation: finaleOverlayFadeIn 360ms ease-out;
}
.finale-overlay.hidden { display: none; }
.finale-overlay__panel {
  width: min(620px, 100%);
  padding: 40px 44px 36px;
  border: 3px solid rgba(155, 95, 12, 0.55);
  border-radius: 32px;
  background: linear-gradient(180deg, rgba(252, 248, 226, 0.99), rgba(238, 222, 175, 0.99));
  box-shadow:
    0 36px 90px rgba(0, 0, 0, 0.42),
    0 0 60px rgba(255, 232, 142, 0.32),
    inset 0 2px 0 rgba(255, 251, 222, 0.92);
  text-align: center;
  animation: finaleOverlayPanelIn 520ms cubic-bezier(0.18, 0.95, 0.34, 1.04);
}
.finale-overlay__eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(155, 95, 12, 0.92);
}
.finale-overlay__title {
  margin: 12px 0 6px;
  font-family: "Fraunces", "Georgia", serif;
  font-size: clamp(34px, 6vw, 52px);
  font-weight: 600;
  line-height: 1.05;
  color: rgba(58, 32, 8, 0.96);
  text-shadow: 0 2px 0 rgba(255, 252, 222, 0.7);
}
.finale-overlay__subtitle {
  margin: 0 0 22px;
  font-size: 16px;
  color: rgba(58, 32, 8, 0.78);
}
.finale-overlay__stats {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px 18px;
  margin: 0 0 24px;
  padding: 18px 20px;
  border: 2px solid rgba(155, 95, 12, 0.28);
  border-radius: 18px;
  background: rgba(255, 247, 212, 0.55);
  font-size: 15px;
  text-align: left;
}
.finale-overlay__stats dt {
  color: rgba(58, 32, 8, 0.7);
  font-weight: 600;
  letter-spacing: 0.04em;
}
.finale-overlay__stats dd {
  margin: 0;
  color: rgba(58, 32, 8, 0.96);
  font-family: "JetBrains Mono", monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  text-align: right;
}
.finale-overlay__button {
  min-width: 220px;
  padding: 14px 28px;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
@keyframes finaleOverlayFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes finaleOverlayPanelIn {
  0%   { opacity: 0; transform: translateY(28px) scale(0.92); }
  60%  { opacity: 1; transform: translateY(-4px) scale(1.02); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

.intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 115;
  /* Short viewports: the card is a fixed stack (chapter tag + network art +
     title + button). Auto margins on the panel + overflow-y here mean a card
     taller than the window anchors at the top and scrolls, instead of flex
     centering clipping BOTH ends (which put GET TO WORK off-screen on small
     laptops). The max-height query below compacts the card so it normally
     fits without scrolling. */
  overflow-y: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background:
    radial-gradient(circle at 50% 40%, rgba(255, 240, 184, 0.18), transparent 60%),
    linear-gradient(180deg, rgba(20, 32, 48, 0.82), rgba(8, 18, 32, 0.9));
  animation: finaleOverlayFadeIn 320ms ease-out;
}
.intro-overlay.hidden { display: none; }

/* Boss-transition curtain (see beginBossTransition in app.js). Always
   mounted — opacity/pointer-events do the hiding, never display:none, so
   toggling .visible actually transitions instead of snapping. Sits above
   .intro-overlay (115) so it can cover a lingering modal too. */
.boss-transition-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s ease;
}
.boss-transition-overlay.visible { opacity: 1; }
.intro-overlay__panel {
  width: min(720px, 100%);
  margin: auto;
  flex-shrink: 0;
  padding: 36px 48px 32px;
  border: 3px solid rgba(155, 95, 12, 0.55);
  border-radius: 32px;
  background: linear-gradient(180deg, rgba(252, 248, 226, 0.99), rgba(238, 222, 175, 0.99));
  box-shadow:
    0 36px 90px rgba(0, 0, 0, 0.42),
    0 0 60px rgba(255, 232, 142, 0.32),
    inset 0 2px 0 rgba(255, 251, 222, 0.92);
  text-align: center;
  animation: finaleOverlayPanelIn 460ms cubic-bezier(0.18, 0.95, 0.34, 1.04);
}
.intro-overlay__network {
  width: min(440px, 100%);
  height: auto;
  margin: 18px auto 22px;
  display: block;
}
.intro-overlay__chapter-tag {
  font-family: "Fraunces", "Georgia", serif;
  font-size: clamp(48px, 7.5vw, 84px);
  font-weight: 800;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  line-height: 1;
  color: rgba(155, 95, 12, 0.98);
  text-shadow: 0 2px 0 rgba(255, 252, 222, 0.7);
  margin-bottom: 14px;
}
.intro-overlay__eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(155, 95, 12, 0.92);
  margin-bottom: 10px;
}
.intro-overlay__title {
  margin: 0 0 10px;
  font-family: "Fraunces", "Georgia", serif;
  font-size: clamp(22px, 3.4vw, 32px);
  font-weight: 500;
  line-height: 1.25;
  color: rgba(58, 32, 8, 0.96);
  text-shadow: 0 2px 0 rgba(255, 252, 222, 0.7);
}
.intro-overlay__subtitle {
  margin: 0;
  font-family: "Fraunces", "Georgia", serif;
  font-size: clamp(14px, 2vw, 18px);
  font-weight: 500;
  font-style: italic;
  line-height: 1.3;
  color: rgba(82, 50, 14, 0.78);
  text-shadow: 0 1px 0 rgba(255, 252, 222, 0.55);
}
.intro-overlay__button {
  min-width: 200px;
  padding: 14px 28px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
/* Short screens (13" laptops with browser chrome are ~600–700px tall): the
   full-size card is 660px, so shrink the chapter tag, the network art and the
   spacing until it fits down to ~560px. Same breakpoint the breakthrough
   strip uses. Text stays >= 16px. */
@media (max-height: 749px) {
  .intro-overlay {
    padding: 16px;
  }
  .intro-overlay__panel {
    padding: 22px 40px 22px;
    border-radius: 24px;
  }
  .intro-overlay__chapter-tag {
    font-size: clamp(40px, 8vh, 60px);
    margin-bottom: 8px;
  }
  .intro-overlay__eyebrow {
    margin-bottom: 6px;
  }
  .intro-overlay__title {
    font-size: clamp(20px, 2.6vw, 26px);
    margin-bottom: 8px;
  }
  .intro-overlay__network {
    /* 52vh wide keeps the 360:220 art at ~32vh tall without letterboxing. */
    width: min(320px, 100%, 52vh);
    margin: 10px auto 14px;
  }
  .intro-overlay__button {
    padding: 12px 24px;
  }
}
/* Group-intro variant has no network art, so the subtitle is the last
   element before the button — give it extra breathing room. */
.group-intro-overlay .intro-overlay__subtitle {
  margin-bottom: 32px;
}
/* Darker "boss level" treatment — opt-in via the --boss modifier (toggled
   in showGroupIntroOverlay, app.js, for group.intro.theme === "boss" only:
   today that's just Ship the Model). #group-intro-overlay is a single DOM
   element SHARED by every chapter group's intro card, so without the
   modifier this would darken every group's intro, not just the boss one. */
.group-intro-overlay--boss .intro-overlay__panel {
  border-color: rgba(255, 140, 60, 0.55);
  background: linear-gradient(180deg, rgba(30, 16, 10, 0.98), rgba(14, 8, 6, 0.99));
  box-shadow:
    0 36px 90px rgba(0, 0, 0, 0.6),
    0 0 60px rgba(255, 120, 40, 0.22),
    inset 0 2px 0 rgba(255, 200, 150, 0.12);
}
.group-intro-overlay--boss .intro-overlay__chapter-tag {
  color: rgba(255, 150, 70, 0.95);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
}
.group-intro-overlay--boss .intro-overlay__eyebrow {
  color: rgba(255, 170, 100, 0.85);
}
.group-intro-overlay--boss .intro-overlay__title {
  color: rgba(255, 238, 222, 0.96);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
}
.group-intro-overlay--boss .intro-overlay__subtitle {
  color: rgba(230, 205, 190, 0.78);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
}

/* "How this boss works" diagram (index.html, #group-intro-overlay-diagram):
   two icon steps + an arrow between them. Hidden by default; shown only for
   Ship the Model (see showGroupIntroOverlay). */
.intro-overlay__diagram {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: 4px 0 28px;
}
.intro-overlay__diagram.hidden {
  display: none;
}
.intro-overlay__diagram-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.intro-overlay__diagram-icon {
  width: 64px;
  height: 64px;
  flex: none;
}
.intro-overlay__diagram-arrow {
  width: 40px;
  height: 18px;
  flex: none;
}
.intro-overlay__diagram-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-align: center;
  color: rgba(255, 220, 190, 0.85);
  line-height: 1.4;
}
.intro-overlay__diagram-label small {
  display: block;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgba(255, 170, 100, 0.75);
}

@keyframes milestoneOverlayPanelIn {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.assignments-overlay__panel {
  width: min(860px, 96vw);
  max-height: min(86vh, 760px);
  display: flex;
  flex-direction: column;
  text-align: left;
  padding: 22px 24px 20px;
  border-color: rgba(131, 85, 26, 0.42);
  background: linear-gradient(180deg, rgba(251, 247, 225, 0.99), rgba(238, 229, 192, 0.99));
}

.assignments-overlay__panel .milestone-overlay__eyebrow {
  text-align: center;
  color: rgba(106, 67, 20, 0.92);
}

.assignments-overlay__time {
  margin-top: 6px;
  text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: rgba(82, 50, 12, 0.86);
}

.assignments-overlay__list {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  overflow-y: auto;
  display: grid;
  gap: 4px;
  flex: 1 1 auto;
  min-height: 0;
}

.assignments-overlay__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  border-radius: 12px;
  background: rgba(255, 250, 226, 0.55);
  border: 1px solid rgba(131, 85, 26, 0.16);
  font-size: 14px;
  font-weight: 600;
  color: rgba(58, 40, 14, 0.85);
  min-width: 0;
}

.assignments-overlay__group {
  list-style: none;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 12px;
  padding: 14px 6px 4px;
  border-bottom: 1px solid rgba(131, 85, 26, 0.22);
  margin-top: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.assignments-overlay__group:first-child {
  margin-top: 0;
}

.assignments-overlay__group--locked {
  opacity: 0.55;
}

/* Wraps an entire chapter group: header on top, then a two-column body with
   the chapter screenshot on the left and the assignment rows on the right.
   Each <li.chapter> sits directly inside the overlay's <ol>. */
.assignments-overlay__chapter {
  list-style: none;
  margin-top: 14px;
}

.assignments-overlay__chapter:first-child {
  margin-top: 0;
}

.assignments-overlay__chapter > .assignments-overlay__group {
  margin-top: 0;
  padding: 6px 6px 6px;
}

.assignments-overlay__chapter-body {
  display: grid;
  grid-template-columns: clamp(140px, 22%, 200px) 1fr;
  gap: 16px;
  align-items: start;
  padding: 8px 4px 4px;
}

.assignments-overlay__chapter-thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 14px;
  border: 1.5px solid rgba(131, 85, 26, 0.32);
  box-shadow: 0 2px 8px rgba(74, 48, 12, 0.18), inset 0 1px 0 rgba(255, 250, 220, 0.6);
  background: rgba(255, 250, 226, 0.5);
  position: sticky;
  top: 4px;
}

.assignments-overlay__chapter-thumb--locked {
  filter: grayscale(1) brightness(0.7) contrast(0.9);
  opacity: 0.6;
}

.assignments-overlay__chapter-thumb--current {
  border-color: rgba(167, 113, 28, 0.7);
  box-shadow: 0 4px 14px rgba(167, 113, 28, 0.32), inset 0 1px 0 rgba(255, 250, 220, 0.8), 0 0 0 2px rgba(255, 215, 138, 0.55);
}

.assignments-overlay__chapter-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
  min-width: 0;
}

/* Tighter screen widths: stack the thumbnail on top so rows stay readable.
   Bumped 720→860 because the row text (e.g. "Curves beat hand-built features")
   bunched against the badge in the side-by-side layout below ~860px. */
@media (max-width: 860px) {
  .assignments-overlay__chapter-body {
    grid-template-columns: 1fr;
  }
  .assignments-overlay__chapter-thumb {
    max-width: 240px;
    margin: 0 auto;
    position: static;
  }
}

.assignments-overlay__group-num {
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(106, 67, 20, 0.7);
}

.assignments-overlay__group-name {
  font-size: 17px;
  color: rgba(58, 40, 14, 0.95);
}

.assignments-overlay__group-subtitle {
  font-size: 12px;
  font-weight: 500;
  font-style: italic;
  color: rgba(106, 67, 20, 0.62);
  margin-left: auto;
}

.assignments-overlay__row--done {
  background: rgba(212, 234, 196, 0.7);
  border-color: rgba(53, 116, 60, 0.42);
  color: rgba(40, 76, 40, 0.92);
}

.assignments-overlay__row--current {
  background: linear-gradient(180deg, rgba(255, 235, 175, 0.95), rgba(247, 215, 138, 0.95));
  border-color: rgba(167, 113, 28, 0.62);
  color: rgba(82, 50, 12, 0.95);
  box-shadow: inset 0 1px 0 rgba(255, 252, 230, 0.9);
}

.assignments-overlay__row--locked {
  opacity: 0.5;
}

.assignments-overlay__row--locked.assignments-overlay__row--clickable {
  opacity: 1;
}

.assignments-overlay__row--clickable {
  cursor: var(--cursor-pointer);
  transition: transform 80ms ease, box-shadow 120ms ease, background 120ms ease;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .assignments-overlay__row--clickable:hover,
.assignments-overlay__row--clickable:focus-visible {
  transform: translateX(2px);
  box-shadow: 0 1px 0 rgba(82, 50, 12, 0.18), 0 6px 14px rgba(82, 50, 12, 0.14);
  outline: none;
  background: rgba(228, 244, 213, 0.85);
}

.assignments-overlay__row-num {
  min-width: 40px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: rgba(106, 67, 20, 0.7);
}

.assignments-overlay__row-title {
  flex: 1;
  min-width: 0;
}

.assignments-overlay__row-badge {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 2px 8px;
  border-radius: 999px;
}

.assignments-overlay__row--done .assignments-overlay__row-badge {
  background: rgba(53, 116, 60, 0.86);
  color: #f4f9ec;
}

.assignments-overlay__row--current .assignments-overlay__row-badge {
  background: rgba(141, 87, 18, 0.88);
  color: #fff8e1;
}

.assignments-overlay__row--locked .assignments-overlay__row-badge {
  background: rgba(60, 50, 30, 0.18);
  color: rgba(60, 50, 30, 0.6);
}

.assignments-overlay__row-unlocks {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-right: 4px;
}

.assignments-overlay__unlock {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  background: rgba(180, 145, 70, 0.28);
  color: rgba(82, 50, 12, 0.92);
  border: 1px solid rgba(131, 85, 26, 0.55);
}

.assignments-overlay__unlock--owned {
  background: rgba(86, 156, 88, 0.32);
  border-color: rgba(40, 95, 45, 0.7);
  color: rgba(28, 64, 30, 0.96);
}

.assignments-overlay__unlock-icon {
  width: 13px;
  height: 13px;
  flex: 0 0 13px;
}

.assignments-overlay__unlock-label {
  white-space: nowrap;
}

.assignments-overlay__panel .milestone-overlay__button {
  align-self: center;
  margin-top: 16px;
}

.breakthrough-strip__headline {
  cursor: var(--cursor-pointer);
  transition: filter 120ms ease, transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .breakthrough-strip__headline:hover {
  filter: brightness(1.06);
  transform: translateY(-1px);
  border-color: rgba(131, 85, 26, 0.55) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 209, 0.95),
    0 8px 16px rgba(131, 85, 26, 0.22) !important;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .breakthrough-strip__headline:hover .breakthrough-strip__open-hint {
  background: rgba(131, 85, 26, 0.24);
  border-color: rgba(131, 85, 26, 0.55);
  color: rgba(78, 48, 10, 0.98);
}

.breakthrough-strip__headline:focus {
  outline: none;
}

.breakthrough-strip__headline:focus-visible {
  outline: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 209, 0.9),
    0 0 0 3px rgba(167, 113, 28, 0.45);
}

.breakthrough-strip__open-hint {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  padding: 4px 9px;
  border-radius: 14px;
  background: rgba(131, 85, 26, 0.12);
  border: 1px solid rgba(131, 85, 26, 0.32);
  color: rgba(106, 67, 20, 0.9);
  font-family: "Trebuchet MS", "Avenir Next Condensed", "Gill Sans", sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.15;
  flex: 0 1 auto;
  min-width: 0;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}

.breakthrough-strip__open-hint-icon {
  width: 12px;
  height: 12px;
  flex: 0 0 auto;
}

.breakthrough-strip__open-hint-text {
  white-space: normal;
  text-wrap: balance;
  overflow-wrap: anywhere;
}

/* Narrow viewports (phone widths) AND short viewports (1024×600 Chromebooks,
   1366×768 with browser chrome) hide the "All assignments" label so the
   chapter dock stops squeezing it into a vertical letter-stack — at small
   sizes the wrap turned the open-hint into a ~120 px tall column of single
   letters and pushed the dock to 60% of viewport height. The icon and click
   target remain. */
@media (max-width: 700px), (max-height: 749px) {
  .breakthrough-strip__open-hint-text {
    display: none;
  }
  .breakthrough-strip__open-hint {
    padding: 4px;
  }
}


.title-screen__actions {
  margin-top: 26px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

/* Primary CTA wears the canonical "unlockable" sky-blue gradient so the
   intended action (Continue if there's a save, else New Run) reads as the
   one to click. */
.title-screen__start {
  border: 1px solid rgba(16, 74, 153, 0.92);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
  color: rgba(247, 252, 255, 0.98);
  text-shadow: 0 1px 0 rgba(17, 61, 124, 0.46);
  padding: 14px 24px;
  min-width: 170px;
  font-size: 16px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: var(--cursor-pointer);
  transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
  box-shadow:
    inset 0 1px 0 rgba(167, 231, 255, 0.88),
    0 10px 24px rgba(22, 92, 188, 0.32),
    0 0 0 2px rgba(78, 187, 255, 0.35);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .title-screen__start:hover,
.title-screen__start:focus-visible {
  transform: translateY(-1px);
  filter: brightness(1.05);
  box-shadow:
    inset 0 1px 0 rgba(184, 239, 255, 0.95),
    0 14px 28px rgba(22, 92, 188, 0.45),
    0 0 0 3px rgba(78, 187, 255, 0.55);
}

.title-screen__secondary {
  border: 1px solid rgba(22, 21, 19, 0.16);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(230, 222, 208, 0.94));
  color: var(--ink);
  padding: 14px 22px;
  min-width: 150px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: var(--cursor-pointer);
  transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
  box-shadow: 0 10px 24px rgba(22, 21, 19, 0.08);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .title-screen__secondary:hover,
.title-screen__secondary:focus-visible {
  transform: translateY(-1px);
  border-color: rgba(22, 21, 19, 0.28);
  box-shadow: 0 14px 28px rgba(22, 21, 19, 0.12);
  outline: none;
}

.title-screen__credits {
  margin: 24px auto 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 22px;
  opacity: 0.92;
}

.title-screen__logo {
  width: auto;
  display: block;
}

.title-screen__logo--cornell {
  height: 72px;
}

.title-screen__logo--aimi {
  height: 56px;
}

.title-screen__credits-divider {
  width: 1px;
  height: 52px;
  background: rgba(26, 39, 31, 0.22);
}

.title-screen__attribution {
  margin: 14px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px 8px;
  font-size: 14px;
  letter-spacing: 0.02em;
  color: rgba(26, 39, 31, 0.62);
  font-style: italic;
}

.title-screen__attribution-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-style: normal;
  font-weight: 500;
  color: rgba(26, 39, 31, 0.82);
}

.title-screen__attribution-and {
  opacity: 0.7;
}

.title-screen__attribution-logo {
  width: 18px;
  height: 18px;
  display: block;
}

.title-screen__hint {
  margin-top: 16px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(26, 39, 31, 0.48);
}

.title-screen__version {
  margin: 10px 0 0;
  font-family: "JetBrains Mono", monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: rgba(26, 39, 31, 0.42);
}

.title-screen__version:empty {
  display: none;
}

/* NSF / Intel funding acknowledgment (AI-MI award DMR-2433348; wording from
   aimi.cornell.edu/acknowledgment-of-ai-mi-support-on-publications-and-other-outputs
   plus the NSF PAPPG disclaimer). Shared by every version, since the title
   screen is. Erik 2026-09-26. */
.title-screen__funding {
  margin: 12px auto 0;
  max-width: 460px;
  font-size: 12px;
  line-height: 1.45;
  color: rgba(26, 39, 31, 0.5);
}

.title-screen__consent {
  margin: 8px auto 0;
  max-width: 340px;
  font-size: 10px;
  line-height: 1.4;
  color: rgba(26, 39, 31, 0.38);
}

.left-pane-backdrop {
  position: fixed;
  left: 20px;
  top: 20px;
  z-index: 21;
  pointer-events: none;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.left-pane-backdrop::before {
  display: none;
}

.left-pane-section {
  position: absolute;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.left-pane-section::before {
  content: "";
  position: absolute;
  left: 8px;
  right: 8px;
  top: 0;
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(244, 195, 103, 0.08), rgba(255, 228, 172, 0.34), rgba(244, 195, 103, 0.08));
}

.left-pane-section--upgrades::before {
  display: none;
}

.left-pane-section--upgrades {
  border-radius: 18px;
  overflow: visible;
  background: transparent;
}

.left-pane-section--workbench {
  padding: 6px 12px 0;
  /* Section span is bigger than the card itself (it reserves vertical room for
     the side desks drawn on the canvas below). Without `pointer-events: none`
     here, this div swallows clicks aimed at the canvas ghost desk slot. */
  pointer-events: none;
  z-index: 21;
}

.workbench-card {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  background: linear-gradient(180deg, rgba(240, 227, 184, 0.85), rgba(216, 193, 130, 0.85));
  border: 1px solid rgba(106, 67, 20, 0.45);
  border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255, 245, 209, 0.7);
  /* Re-enable pointer events on the card itself so the pip buttons inside
     remain clickable while the rest of the section stays click-through. */
  pointer-events: auto;
}

.workbench-card__label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(82, 50, 12, 0.86);
  white-space: nowrap;
}

.workbench-card .model-roster__upgrades {
  display: flex;
  flex-wrap: nowrap;
  gap: 5px;
  margin: 0;
}

.workbench-card .model-roster__pip {
  width: 30px;
  height: 30px;
  font-size: 17px;
  border-radius: 7px;
  flex-shrink: 0;
}

.workbench-auto-toggle {
  margin-left: auto;
  flex-shrink: 0;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.05em;
  color: #f4faff;
  padding: 6px 12px;
  border-radius: 9px;
  border: 1px solid rgba(20, 60, 25, 0.78);
  background: linear-gradient(180deg, #5ab063, #286b33);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32), 0 1px 0 rgba(0, 0, 0, 0.18);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.45);
  cursor: var(--cursor-pointer);
  transition: filter 120ms ease, transform 120ms ease;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .workbench-auto-toggle:hover {
  filter: brightness(1.1);
}

.workbench-auto-toggle:active {
  transform: translateY(1px);
}

.workbench-auto-toggle--paused {
  border-color: rgba(30, 36, 50, 0.78);
  background: linear-gradient(180deg, #8896ae, #4c5570);
}

.left-pane-section::after {
  content: "";
  position: absolute;
  left: 8px;
  right: 8px;
  top: 6px;
  height: 1px;
  border-radius: 999px;
  background: rgba(54, 29, 5, 0.28);
}

.left-pane-section--upgrades::after {
  display: none;
}
/* The workbench section holds the side-desk cards (each with its own frame);
   the ::before/::after top divider read as a stray horizontal line above the
   desks (Erik 2026-08-24, annotated on karthik). Hide it like upgrades does. */
.left-pane-section--workbench::before,
.left-pane-section--workbench::after {
  display: none;
}


/* Roster has replaced the legacy upgrades-dock and the upgrades + info partitions
   of the left pane. Hide them from first paint so they don't flash on startup. */
#upgrades-dock,
#left-pane-upgrades-section,
#left-pane-info-section {
  display: none !important;
}

.model-roster {
  position: fixed;
  z-index: 21;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  pointer-events: auto;
  background: linear-gradient(180deg, #fdf6dd, #efe1ae);
  border: 2.5px solid rgba(131, 85, 26, 0.42);
  border-radius: 22px;
  box-shadow: 0 8px 18px rgba(74, 48, 12, 0.22), inset 0 1px 0 rgba(255, 248, 215, 0.95);
}
.model-roster.hidden { display: none; }
.model-roster__tabs {
  display: flex;
  gap: 0;
  padding: 6px 8px 0;
  border-bottom: 1px solid rgba(131, 85, 26, 0.32);
  background: rgba(232, 215, 168, 0.55);
}
.model-roster__tab {
  flex: 1;
  appearance: none;
  background: transparent;
  border: 0;
  padding: 6px 8px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(106, 67, 20, 0.6);
  cursor: var(--cursor-pointer);
  border-radius: 8px 8px 0 0;
  transition: color 80ms ease, background 80ms ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__tab:hover {
  color: rgba(82, 50, 12, 0.88);
}
.model-roster__tab.active {
  background: rgba(251, 247, 225, 0.96);
  color: rgba(82, 50, 12, 0.96);
  box-shadow: inset 0 1px 0 rgba(255, 245, 209, 0.9);
}
.model-roster__tab.disabled,
.model-roster__tab[disabled] {
  color: rgba(82, 50, 12, 0.32);
  cursor: not-allowed;
  background: transparent;
  box-shadow: none;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__tab.disabled:hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__tab[disabled]:hover {
  color: rgba(82, 50, 12, 0.32);
  background: transparent;
}
.model-roster__tab--blink {
  position: relative;
  animation: modelDetailsTabBlink 900ms ease-in-out infinite;
}
@keyframes modelDetailsTabBlink {
  0%, 100% {
    background: rgba(255, 232, 142, 0.95);
    color: rgba(82, 50, 12, 0.98);
    box-shadow: inset 0 1px 0 rgba(255, 251, 222, 1), 0 0 0 2px rgba(232, 158, 36, 0.7);
  }
  50% {
    background: rgba(255, 246, 196, 0.98);
    color: rgba(60, 32, 6, 1);
    box-shadow: inset 0 1px 0 rgba(255, 252, 230, 1), 0 0 0 4px rgba(255, 200, 60, 0.85), 0 0 14px rgba(255, 200, 60, 0.55);
  }
}

.model-roster__list {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 8px 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.model-roster__list--viz {
  padding: 12px;
  align-items: center;
}
.model-roster__viz-empty {
  font-size: 12px;
  color: rgba(82, 50, 12, 0.7);
  text-align: center;
  padding: 24px 8px;
  line-height: 1.4;
}
.knn-details__current {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 0 4px;
}
.knn-details__current-label {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(82, 50, 12, 0.7);
}
.knn-details__thumb-svg {
  border-radius: 6px;
  border: 1px solid rgba(82, 50, 12, 0.24);
  background: #fdf3d8;
  display: block;
}
.knn-details__empty {
  font-size: 12px;
  color: rgba(82, 50, 12, 0.7);
  text-align: center;
  padding: 14px 4px 4px;
  line-height: 1.4;
}
.tree-details__empty {
  font-size: 12px;
  color: rgba(82, 50, 12, 0.7);
  text-align: center;
  padding: 14px 4px 4px;
  line-height: 1.4;
}
.tree-details__tree {
  margin-top: 10px;
  font-size: 12px;
  color: rgba(56, 43, 27, 0.92);
}
.tree-details__node {
  padding: 2px 0;
}
.tree-details__node--leaf {
  padding-left: 4px;
}
.tree-details__split {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-weight: 600;
}
.tree-details__edge {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(120, 84, 32, 0.75);
}
.tree-details__rule {
  font-family: "JetBrains Mono", monospace;
  font-size: 12px;
  background: rgba(82, 50, 12, 0.08);
  padding: 1px 6px;
  border-radius: 4px;
}
.tree-details__children {
  margin-left: 14px;
  padding-left: 10px;
  border-left: 2px solid rgba(131, 85, 26, 0.28);
  margin-top: 3px;
}
.tree-details__leaf {
  font-family: "JetBrains Mono", monospace;
  font-size: 11px;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 4px;
}
.tree-details__leaf--zero {
  background: rgba(220, 38, 38, 0.15);
  color: rgba(160, 30, 30, 0.95);
}
.tree-details__leaf--one {
  background: rgba(29, 78, 216, 0.15);
  color: rgba(25, 70, 180, 0.95);
}
.knn-details__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.knn-details__bucket {
  background: rgba(255, 250, 226, 0.65);
  border: 1.5px solid rgba(131, 85, 26, 0.28);
  border-radius: 10px;
  padding: 7px 8px 9px;
  cursor: var(--cursor-pointer);
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: background 80ms ease, border-color 80ms ease, box-shadow 80ms ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .knn-details__bucket:hover {
  background: rgba(255, 250, 226, 0.95);
  border-color: rgba(131, 85, 26, 0.55);
  box-shadow: 0 2px 6px rgba(74, 48, 12, 0.15);
}
.knn-details__bucket--prediction {
  border-color: rgba(46, 123, 255, 0.65);
  background: rgba(214, 232, 250, 0.6);
}
.knn-details__bucket-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  color: rgba(31, 56, 88, 0.9);
}
.knn-details__bucket-name {
  font-size: 14px;
  letter-spacing: 0.04em;
}
.knn-details__bucket-count {
  font-size: 10px;
  color: rgba(82, 50, 12, 0.62);
  font-weight: 600;
}
.knn-details__slots {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.knn-details__slot {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.knn-details__slot--empty {
  background: rgba(82, 50, 12, 0.08);
  border: 1px dashed rgba(82, 50, 12, 0.28);
}
.knn-details__slot--filled {
  cursor: grab;
  border: 1px solid rgba(82, 50, 12, 0.32);
  background: transparent;
  padding: 0;
  transition: transform 80ms ease, border-color 80ms ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .knn-details__slot--filled:hover {
  transform: scale(1.05);
  border-color: rgba(219, 101, 36, 0.85);
}
.knn-details__slot--filled:active {
  cursor: grabbing;
}
.knn-details__current-handle {
  cursor: grab;
  display: inline-block;
  border-radius: 6px;
  transition: transform 80ms ease, box-shadow 80ms ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .knn-details__current-handle:hover {
  transform: scale(1.04);
  box-shadow: 0 2px 6px rgba(74, 48, 12, 0.18);
}
.knn-details__current-handle:active {
  cursor: grabbing;
}
.knn-details__dragging {
  opacity: 0.4;
}
.knn-details__bucket--drag-over {
  background: rgba(214, 232, 250, 0.95);
  border-color: rgba(46, 123, 255, 0.9);
  box-shadow: 0 0 0 2px rgba(46, 123, 255, 0.35);
}

.knn-details__trash {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 8px 10px;
  min-width: 64px;
  border: 1.5px dashed rgba(150, 40, 30, 0.55);
  border-radius: 10px;
  background: rgba(255, 240, 232, 0.55);
  color: rgba(132, 32, 22, 0.78);
  font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: default;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.knn-details__trash-icon {
  width: 24px;
  height: 24px;
}
.knn-details__trash-label {
  font-weight: 600;
  opacity: 0.85;
}
.knn-details__trash--drag-over {
  background: rgba(255, 214, 206, 0.95);
  border-color: rgba(190, 36, 22, 0.95);
  color: rgba(132, 22, 14, 0.96);
  box-shadow: 0 0 0 2px rgba(200, 60, 40, 0.4);
}

/* Clear-all is a click button instead of a drop zone — solid border, cursor
   pointer, hover state. Disabled while the memory bank is empty. */
.knn-details__clear-all {
  border-style: solid;
  cursor: pointer;
  font: inherit;
  font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .knn-details__clear-all:hover:not(:disabled) {
  background: rgba(255, 214, 206, 0.95);
  border-color: rgba(190, 36, 22, 0.95);
  color: rgba(132, 22, 14, 0.96);
}
.knn-details__clear-all:active:not(:disabled) {
  background: rgba(255, 196, 184, 0.98);
  box-shadow: inset 0 1px 3px rgba(120, 22, 14, 0.35);
}
.knn-details__clear-all:disabled,
.knn-details__clear-all--disabled {
  opacity: 0.45;
  cursor: default;
}

.knn-classify {
  margin-top: 10px;
  padding: 8px 10px 10px;
  border: 1px solid rgba(131, 85, 26, 0.32);
  border-radius: 10px;
  background: rgba(255, 250, 226, 0.55);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.knn-classify--empty {
  text-align: center;
  font-size: 12px;
  color: rgba(82, 50, 12, 0.7);
  font-style: italic;
}
.knn-classify__header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  font-weight: 700;
  color: rgba(31, 56, 88, 0.9);
}
.knn-classify__title {
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.knn-classify__k {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  color: rgba(82, 50, 12, 0.65);
}
.knn-classify__legend {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(82, 50, 12, 0.7);
  text-transform: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.knn-classify__legend-swatch {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  margin: 0 1px 0 4px;
  vertical-align: middle;
}
.knn-classify__legend-swatch--input { background: rgba(196, 60, 60, 0.85); }
.knn-classify__legend-swatch--proto { background: rgba(60, 130, 60, 0.85); }
.knn-classify__equals {
  font-size: 14px;
  font-weight: 700;
  color: rgba(82, 50, 12, 0.5);
  margin: 0 2px;
}
.knn-classify__body {
  display: flex;
  align-items: center;
  gap: 8px;
}
.knn-classify__input {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
}
.knn-classify__input-label {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(82, 50, 12, 0.65);
}
.knn-classify__arrow {
  font-size: 18px;
  color: rgba(82, 50, 12, 0.45);
  flex: 0 0 auto;
}
.knn-classify__neighbors {
  display: flex;
  gap: 6px;
  flex: 1 1 auto;
  flex-wrap: wrap;
}
.knn-classify__neighbor {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 5px 6px 6px;
  border: 1px solid rgba(131, 85, 26, 0.28);
  border-radius: 8px;
  background: rgba(255, 250, 226, 0.7);
  min-width: 0;
}
.knn-classify__neighbor--winner {
  border-color: rgba(46, 123, 255, 0.7);
  background: rgba(214, 232, 250, 0.7);
  box-shadow: 0 0 0 1px rgba(46, 123, 255, 0.25);
}
.knn-classify__neighbor-rank {
  font-size: 10px;
  font-weight: 700;
  color: rgba(31, 56, 88, 0.85);
  letter-spacing: 0.02em;
}
.knn-classify__neighbor-row {
  display: flex;
  align-items: center;
  gap: 4px;
}
.knn-classify__minus {
  font-size: 14px;
  color: rgba(82, 50, 12, 0.55);
  font-weight: 700;
}
.knn-classify__neighbor-dist {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  color: rgba(82, 50, 12, 0.7);
}
.knn-classify__vote {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding-top: 4px;
  border-top: 1px dashed rgba(131, 85, 26, 0.28);
  font-size: 12px;
}
.knn-classify__vote-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border: 1px solid rgba(131, 85, 26, 0.32);
  border-radius: 999px;
  background: rgba(255, 250, 226, 0.8);
  font-size: 11px;
  color: rgba(31, 56, 88, 0.85);
}
.knn-classify__vote-badge strong {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
}
.knn-classify__vote-badge--winner {
  border-color: rgba(46, 123, 255, 0.75);
  background: rgba(214, 232, 250, 0.85);
}
.knn-classify__vote-arrow {
  color: rgba(82, 50, 12, 0.45);
  font-size: 14px;
}
.knn-classify__vote-result {
  font-weight: 700;
  font-size: 13px;
  color: rgba(31, 56, 88, 0.95);
}

.model-roster__viz-card {
  width: 100%;
  background: rgba(255, 250, 226, 0.7);
  border: 1px solid rgba(131, 85, 26, 0.28);
  border-radius: 12px;
  padding: 6px 10px 8px;
  box-shadow: inset 0 1px 0 rgba(255, 245, 209, 0.9);
}
.model-roster__viz-svg {
  /* Fill the panel width. The viewBox is intentionally wide-and-short so
     filling width still leaves room for the weights table below without
     overflowing on the target ~1556×866 viewport. */
  width: 100%;
  height: auto;
  display: block;
  margin: 0 auto;
}
/* 8.5 BatchNorm unit inspector entry point — hidden-unit nodes become
   clickable when mlpUnitInspectorAvailable() (renderMlpNetworkSvg). */
.mlp-network-svg__unit {
  cursor: var(--cursor-pointer);
}
.mlp-network-svg__unit-ring {
  cursor: var(--cursor-pointer);
  opacity: 0.75;
  animation: mlpUnitRingSpin 6s linear infinite;
  transform-origin: center;
  transform-box: fill-box;
}
.mlp-network-svg__unit-ring:hover {
  opacity: 1;
  stroke-width: 2.6;
}
@keyframes mlpUnitRingSpin {
  from { stroke-dashoffset: 0; }
  to { stroke-dashoffset: -24; }
}
.mlp-details__unit-inspector-hint {
  margin-top: 4px;
  font-size: 11px;
  font-style: italic;
  color: rgba(46, 111, 219, 0.85);
  text-align: center;
}
.perceptron-details {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11.5px;
  color: rgba(58, 40, 14, 0.92);
}
.perceptron-details__formula {
  font-family: 'Fraunces', serif;
  font-size: 11px;
  line-height: 1.45;
  color: rgba(82, 50, 12, 0.78);
  margin-bottom: 8px;
  text-align: center;
}
.perceptron-details__formula code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  background: rgba(255, 245, 209, 0.6);
  padding: 1px 4px;
  border-radius: 4px;
}
.perceptron-details__table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 6px;
}
.perceptron-details__table thead th {
  text-align: left;
  font-family: 'Fraunces', serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(106, 67, 20, 0.78);
  padding: 6px 4px 2px 4px;
  border-bottom: 1px solid rgba(131, 85, 26, 0.18);
}
.perceptron-details__table tbody th {
  font-weight: 500;
  text-align: left;
  padding: 2px 4px;
  color: rgba(58, 40, 14, 0.86);
}
.perceptron-details__num {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  text-align: right;
  padding: 2px 6px;
  white-space: nowrap;
}
.perceptron-details__bar-cell {
  width: 60px;
  padding: 2px 4px;
}
.perceptron-details__bar {
  display: block;
  height: 6px;
  background: rgba(131, 85, 26, 0.12);
  border-radius: 3px;
  overflow: hidden;
  position: relative;
}
.perceptron-details__bar::after {
  content: "";
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: var(--bar-fill, 0%);
  background: var(--bar-color, #2e7bff);
  border-radius: 3px;
}
.perceptron-details__config {
  display: flex;
  justify-content: space-between;
  font-size: 10.5px;
  color: rgba(82, 50, 12, 0.72);
  padding: 4px 4px 0 4px;
  border-top: 1px solid rgba(131, 85, 26, 0.18);
}

/* 7.1 lr-dial breakthrough — preset buttons appear inline with the lr/iter
   label row. Six values from 0.001 to 2.5; the active one stands out. */
.mlp-lr-dial {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.mlp-lr-dial__preset {
  font: inherit;
  font-size: 10px;
  padding: 2px 5px;
  border: 1px solid rgba(131, 85, 26, 0.32);
  border-radius: 3px;
  background: rgba(255, 248, 232, 0.6);
  color: rgba(82, 50, 12, 0.85);
  cursor: pointer;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .mlp-lr-dial__preset:hover {
  background: rgba(255, 222, 145, 0.7);
}
.mlp-lr-dial__preset--active {
  background: rgba(46, 123, 255, 0.88);
  color: #fff;
  border-color: rgba(46, 123, 255, 0.92);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .mlp-lr-dial__preset--active:hover {
  background: rgba(46, 123, 255, 0.95);
}
/* Reset button — circular, slightly separated from the preset row so the
   player doesn't read it as another α value. Rust-orange tint hints "warning
   action" without yelling. */
.mlp-lr-dial__reset {
  font: inherit;
  font-size: 11px;
  line-height: 1;
  margin-left: 6px;
  padding: 2px 6px;
  border: 1px solid rgba(170, 88, 36, 0.55);
  border-radius: 999px;
  background: rgba(255, 232, 200, 0.7);
  color: rgba(170, 88, 36, 0.95);
  cursor: pointer;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .mlp-lr-dial__reset:hover {
  background: rgba(255, 199, 145, 0.85);
}

/* MLP weights table — inherits perceptron table styling but packs an inline
   magnitude bar under each number so 4 weights per hidden row stay compact. */
.mlp-details {
  font-size: 12px;
  color: rgba(58, 40, 14, 0.92);
  margin-top: 4px;
}
.mlp-details__table {
  margin-bottom: 0;
}
.mlp-details__table th,
.mlp-details__table td {
  padding: 3px 6px;
  text-align: center;
  font-size: 12px;
}
.mlp-details__table thead th {
  font-size: 11px;
  padding-top: 4px;
  padding-bottom: 2px;
}
.mlp-details__table tbody th {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
  font-size: 13px;
}
.mlp-details__num {
  font-variant-numeric: tabular-nums;
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
  white-space: nowrap;
}
.mlp-details__num .perceptron-details__bar {
  width: 100%;
  margin-top: 2px;
  height: 4px;
}
.mlp-details__num--dim {
  opacity: 0.55;
}
.mlp-details__num--locked {
  color: rgba(131, 85, 26, 0.45);
  font-weight: 500;
}
.mlp-details__output {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 2px;
  margin-top: 2px;
  border-top: 1px dashed rgba(131, 85, 26, 0.22);
}
.mlp-details__output-label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(106, 67, 20, 0.78);
}
.mlp-details__output .mlp-details__num {
  flex: 1;
  text-align: right;
  max-width: 140px;
  font-size: 13px;
}
.mlp-details .perceptron-details__config {
  padding: 3px 4px 0 4px;
  font-size: 10px;
  /* Let the weight-decay caption wrap onto its own full-width line. */
  flex-wrap: wrap;
}
/* 8.1 weight-decay dial — same control as the α dial, but named so the
   "Weight Decay" purchase visibly maps to the λ symbol. The caption line
   describes what the active λ is doing to the weights (mechanism only, no
   verdicts — finding the working band is the assignment's lesson). The
   column wrapper keeps the caption aligned under the λ buttons. */
.mlp-l2-dial-wrap {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.mlp-l2-dial__caption {
  font-size: 9.5px;
  font-style: italic;
  color: rgba(82, 50, 12, 0.6);
}
/* Model Details' pointer to Ship the Model's under-canvas dial box, shown in
   place of the λ/dropout/BatchNorm rows on those two chapters only (see
   shipModelHasOwnDialBox, ui/ship-model-dial-box.js). */
.mlp-details__moved-hint {
  margin-top: 4px;
  padding: 4px 6px;
  font-size: 10px;
  font-style: italic;
  color: rgba(82, 50, 12, 0.6);
}
/* "Network flattened — hit ↻ Reset" rescue hint (renderMlpCollapseHint).
   Red-leaning so it reads as a warning, not another caption. */
.mlp-collapse-hint {
  margin-top: 6px;
  padding: 4px 6px;
  font-size: 10px;
  font-style: italic;
  color: rgba(150, 42, 22, 0.92);
  background: rgba(196, 78, 48, 0.10);
  border: 1px solid rgba(196, 78, 48, 0.35);
  border-radius: 4px;
}
/* One-time attention flash for the few seconds after Weight Decay is bought —
   warm glow that decays, modeled on cheat-btn-flash. Finite on purpose. */
.mlp-lr-dial--flash {
  animation: l2-dial-flash 1.1s ease-out 3;
  border-radius: 4px;
}
@keyframes l2-dial-flash {
  0%   { background: rgba(244, 220, 120, 0.95); box-shadow: 0 0 0 0 rgba(244, 220, 120, 0.8); }
  45%  { background: rgba(244, 232, 168, 0.7); box-shadow: 0 0 0 7px rgba(244, 220, 120, 0); }
  100% { background: transparent; box-shadow: none; }
}

/* 8.4 early-stopping train/val accuracy sparkline (renderMlpValSparkline). */
.mlp-val-spark {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.mlp-val-spark__legend {
  font-size: 10px;
  letter-spacing: 0.02em;
}
.mlp-val-spark__empty {
  font-size: 10px;
  font-style: italic;
  color: rgba(82, 50, 12, 0.55);
}
.mlp-val-spark__svg {
  display: block;
}

/* Image MLP details: architecture stats + receptive-field thumbnail. */
.mlp-details__deep-arch {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 8px;
}
.mlp-details__deep-arch-diagram {
  padding: 6px 8px 0;
  display: flex;
  justify-content: center;
}
.mlp-details__deep-arch-svg {
  width: 100%;
  max-width: 420px;
  height: auto;
  display: block;
}
.mlp-details__deep-arch-row {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: rgba(34, 52, 38, 0.85);
  gap: 12px;
}
.mlp-details__deep-arch-row span:first-child {
  color: rgba(34, 52, 38, 0.6);
  letter-spacing: 0.04em;
}
.mlp-details__deep-arch-row span:last-child {
  font-family: "JetBrains Mono", monospace;
  font-variant-numeric: tabular-nums;
}
.mlp-details__deep-thumb {
  margin-top: 6px;
  padding: 8px;
  border-top: 1px dashed rgba(131, 85, 26, 0.22);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.mlp-details__deep-thumb-label {
  font-size: 11px;
  color: rgba(34, 52, 38, 0.7);
  text-align: center;
}
.mlp-details__deep-thumb-svg {
  background: rgba(245, 244, 235, 0.6);
  border: 1px solid rgba(131, 85, 26, 0.18);
}
.mlp-details__deep-thumb-hint {
  font-size: 10px;
  color: rgba(34, 52, 38, 0.5);
  font-style: italic;
}

/* Current-scan + prediction block: input thumbnail on the left, predicted
   class label + per-class softmax probability bars on the right. Surfaces
   "what is the network deciding RIGHT NOW" inline in the details panel. */
.mlp-details__deep-scan {
  margin-top: 8px;
  padding: 8px 10px;
  border-top: 1px dashed rgba(131, 85, 26, 0.22);
}
.mlp-details__deep-scan-header {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(34, 52, 38, 0.55);
  margin-bottom: 6px;
}
.mlp-details__deep-scan-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.mlp-details__deep-scan-thumb svg {
  border: 1px solid rgba(131, 85, 26, 0.32);
  background: rgba(245, 244, 235, 0.6);
}
.mlp-details__deep-scan-meta {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.mlp-details__deep-scan-pred {
  font-size: 12px;
  color: rgba(34, 52, 38, 0.92);
}
.mlp-details__deep-verdict {
  margin-left: 6px;
  font-size: 11px;
  font-weight: 700;
}
.mlp-details__deep-verdict--correct { color: rgb(60, 130, 60); }
.mlp-details__deep-verdict--wrong { color: rgb(190, 60, 50); }

.mlp-details__lang-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 6px 10px 4px;
  border-bottom: 1px solid rgba(82, 60, 28, 0.18);
  margin-bottom: 6px;
}
.mlp-details__lang-prompt {
  font-family: "JetBrains Mono", monospace;
  font-size: 13px;
  color: rgba(46, 24, 8, 0.95);
}
.mlp-details__lang-blank {
  color: rgba(82, 60, 28, 0.55);
  letter-spacing: 0.1em;
}
.mlp-details__lang-legend {
  display: flex;
  gap: 12px;
  font-size: 10px;
  color: rgba(82, 60, 28, 0.7);
  padding: 0 10px 4px;
  align-items: center;
  flex-wrap: wrap;
}
.mlp-details__lang-legend-hint {
  margin-left: auto;
  font-style: italic;
  opacity: 0.85;
}
.mlp-details__lang-swatch {
  display: inline-block;
  width: 10px;
  height: 3px;
  border-radius: 2px;
  margin-right: 4px;
  vertical-align: middle;
}
.mlp-details__lang-swatch--pos { background: rgba(60, 140, 70, 0.85); }
.mlp-details__lang-swatch--neg { background: rgba(196, 70, 70, 0.85); }
.mlp-details__lang-svg {
  width: 100%;
  height: auto;
  display: block;
  margin-top: 4px;
}

.mlp-details__deep-scan-probs {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.mlp-details__deep-prob-row {
  display: grid;
  grid-template-columns: 24px 1fr 36px;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: rgba(34, 52, 38, 0.75);
}
.mlp-details__deep-prob-row--winner {
  color: rgba(34, 52, 38, 0.96);
  font-weight: 700;
}
.mlp-details__deep-prob-class {
  font-family: "JetBrains Mono", monospace;
  text-align: right;
}
.mlp-details__deep-prob-bar {
  position: relative;
  height: 8px;
  background: rgba(131, 85, 26, 0.12);
  border-radius: 4px;
  overflow: hidden;
}
.mlp-details__deep-prob-bar > span {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: 4px;
  transition: width 180ms ease-out;
}
.mlp-details__deep-prob-pct {
  font-family: "JetBrains Mono", monospace;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Top hidden features grid: small weight heatmaps for the units that fired
   hardest on the current scan, with activation scores below. Replaces the
   old single "most active hidden unit" thumbnail with a row that ranks by
   real per-input activation when there's a current scan available. */
.mlp-details__deep-features {
  margin-top: 8px;
  padding: 8px 10px;
  border-top: 1px dashed rgba(131, 85, 26, 0.22);
}
.mlp-details__deep-features-header {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(34, 52, 38, 0.55);
  margin-bottom: 6px;
}
.mlp-details__deep-features-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 4px;
}
.mlp-details__deep-feature {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.mlp-details__deep-feature .mlp-details__deep-thumb-svg {
  background: rgba(245, 244, 235, 0.6);
  border: 1px solid rgba(131, 85, 26, 0.22);
}
.mlp-details__deep-feature-label {
  font-size: 10px;
  font-weight: 700;
  color: rgba(34, 52, 38, 0.7);
}
.mlp-details__deep-feature-score {
  font-family: "JetBrains Mono", monospace;
  font-size: 10px;
  color: rgba(34, 52, 38, 0.55);
}
.model-roster__group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.model-roster__group + .model-roster__group {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid rgba(131, 85, 26, 0.25);
}
.model-roster__group-header {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(106, 67, 20, 0.78);
  padding: 2px 4px;
  margin-bottom: 1px;
}
.model-roster__list {
  scrollbar-width: auto;
  scrollbar-color: rgba(131, 85, 26, 0.85) rgba(131, 85, 26, 0.16);
}
.model-roster__list::-webkit-scrollbar {
  width: 14px;
}
.model-roster__list::-webkit-scrollbar-track {
  background: rgba(131, 85, 26, 0.16);
  border-radius: 8px;
  margin: 6px 2px;
}
.model-roster__list::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, rgba(170, 118, 44, 0.96), rgba(106, 67, 20, 0.96));
  border: 2px solid rgba(251, 247, 225, 0.55);
  border-radius: 8px;
  min-height: 36px;
  box-shadow: inset 0 1px 0 rgba(255, 235, 180, 0.5);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__list::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, rgba(196, 140, 56, 1), rgba(120, 78, 28, 1));
}
.model-roster__card {
  display: grid;
  grid-template-columns: 64px 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 12px 14px;
  background: linear-gradient(180deg, #fffaeb, #f0e2b5);
  /* 2px solid — same border width as every other "unlockable" surface
     (upgrades-dock pill, Lv-up button, side-desk ghost). */
  border: 2px solid rgba(131, 85, 26, 0.32);
  border-radius: 14px;
  cursor: var(--cursor-pointer);
  position: relative;
  transition: transform 80ms ease;
  box-shadow: inset 0 1px 0 rgba(255, 250, 220, 0.85), 0 2px 4px rgba(74, 48, 12, 0.1);
  font-family: 'Fraunces', serif;
  color: rgba(58, 40, 14, 0.95);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__card:hover { transform: translateX(2px); }
.model-roster__card.active {
  /* Magenta backdrop so the active (currently-selected) card reads as
     the "chip" the chip-trace exits from. Green/red are reserved for ML
     classification outcomes; light blue (.ready) is reserved for
     "click on this to unlock" affordances. Magenta carves out a
     distinct visual channel for selection state and pops cleanly
     against the cream HUD and the pale playfield (cyan didn't have
     enough luminance contrast on either background). */
  background: linear-gradient(180deg, #f1e6f1, #ccaccc);
  border-color: rgba(106, 70, 106, 0.85);
  /* Inset 4px on the left edge as a vivid magenta accent stripe (matches
     the accentStripe on DO ML / AUTO ON / BACKPROP); outer ring is a
     thicker magenta halo (3px) that visually connects to the chip-trace
     stroke. */
  box-shadow:
    inset 4px 0 0 0 #7a4d7a,
    inset 0 1px 0 rgba(249, 240, 249, 0.95),
    0 4px 10px rgba(168, 116, 168, 0.4),
    0 0 0 3px #a874a8;
  transform: translateX(3px);
}
.model-roster__card.active .model-roster__name {
  font-weight: 700;
}
/* Gradient + Newton hand-off: whichever method is taking the current step gets
   a warm amber ring, so the two cards visibly trade off mid-descent. Amber is
   free here — magenta is selection, light blue is "buy me", green/red are
   classification outcomes — and it matches the Taylor parabola on the canvas.
   Sits alongside .active rather than replacing it: the SELECTED card is still
   Newton's Method the whole time; this only says who is acting right now. */
.model-roster__card.handoff {
  box-shadow:
    inset 4px 0 0 0 #b8781a,
    inset 0 1px 0 rgba(249, 240, 249, 0.95),
    0 4px 10px rgba(214, 158, 62, 0.45),
    0 0 0 3px #f0b542;
  transform: translateX(3px);
}
.model-roster__card.handoff .model-roster__name {
  font-weight: 700;
}
.model-roster__card.locked {
  background: linear-gradient(180deg, #3a2a18, #1f160c);
  border-color: rgba(14, 10, 4, 0.75);
  color: rgba(255, 245, 220, 0.55);
  box-shadow: inset 0 1px 0 rgba(255, 230, 180, 0.08), 0 2px 4px rgba(0, 0, 0, 0.32);
  cursor: not-allowed;
  opacity: 1;
}
.model-roster__card.locked .model-roster__portrait {
  filter: grayscale(1) brightness(0.45) contrast(0.85);
  border-color: rgba(14, 10, 4, 0.6);
}
.model-roster__card.locked .model-roster__name {
  color: rgba(255, 245, 220, 0.6);
}
/* Both gating states (needs-points → .unaffordable, needs-era / needs-prereq
   → .dim) render in the same color on locked cards. They are visually the
   same status — "you can't unlock this yet" — so the only differentiator
   should be the condition pill (e.g. "Assignment 4" vs the coin cost),
   never the font color. */
.model-roster__card.locked .model-roster__cost.dim,
.model-roster__card.locked .model-roster__cost.unaffordable {
  color: rgba(255, 245, 220, 0.55);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__card.locked:hover { transform: none; }

/* Padlock prefix on the cost-condition pill, e.g. "🔒 Assignment 11" or
   "🔒 needs Perceptron". Sized to match the surrounding label text and
   inherits its color so it reads as one unit with the gating condition. */
.model-roster__cost-cond-lock {
  width: 11px;
  height: 11px;
  margin-right: 3px;
  vertical-align: -1px;
  flex-shrink: 0;
}

/* INCOMPATIBLE — model is unlocked but can't be applied to the current batch
   type (e.g., perceptron on a digit batch). Greyed but still readable so the
   player can see the model exists; not clickable for selection. */
/* Uniform grey for ANY card that can't act on the current batch kind —
   masters the locked (dark) and ready (blue) treatments below so the
   roster reads as exactly two populations: "usable here" and "not". */
.model-roster__card.incompatible,
.model-roster__card.incompatible.locked,
.model-roster__card.incompatible.ready,
.model-roster__card.incompatible.just-ready {
  background: linear-gradient(180deg, #e7e0d1, #d6cebb);
  border-color: rgba(94, 86, 70, 0.4);
  color: rgba(82, 76, 64, 0.8);
  box-shadow: none;
  animation: none;
  filter: grayscale(0.85) opacity(0.62);
  transform: none;
  cursor: not-allowed;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__card.incompatible:hover { transform: none; }
.model-roster__card.incompatible.active {
  opacity: 1;
  filter: none;
}
/* Ready + Incompatible: the model is affordable but the current chapter
   can't actually use it. Suppress the bright-blue "ready" gradient and the
   pulse animation so the card reads as inert, not "click me!". */
/* (Card-level ready.incompatible styling superseded by the unified grey
   rule above; the descendant text rules below remain as harmless
   refinements under the grayscale filter.) */
.model-roster__card.ready.incompatible .model-roster__name { color: rgba(82, 50, 12, 0.86); }
.model-roster__card.ready.incompatible .model-roster__meta { color: rgba(82, 50, 12, 0.62); }
.model-roster__card.ready.incompatible .model-roster__cost.ready {
  color: rgba(82, 50, 12, 0.78);
}
.model-roster__card.ready.incompatible .model-roster__cost.ready .model-roster__cost-num {
  text-shadow: none;
}
.model-roster__card.ready.incompatible .model-roster__portrait {
  border-color: rgba(131, 85, 26, 0.32);
}

.model-roster__card--workbench {
  cursor: default;
  background: linear-gradient(180deg, #f0e3b8, #d8c182);
  border-color: rgba(106, 67, 20, 0.45);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__card--workbench:hover { transform: none; }
/* Unlockable "ready to buy" treatment — every "this can be unlocked right
   now" surface in the game (upgrades-dock pill, side-desk ghost, Lv-up
   button, model roster card) uses the same canonical blue gradient:
     rgba(78, 187, 255, 0.98) → rgba(38, 119, 231, 0.98). */
.model-roster__card.ready {
  background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
  border-color: rgba(16, 74, 153, 0.92);
  color: rgba(247, 252, 255, 0.98);
  text-shadow: 0 1px 0 rgba(17, 61, 124, 0.46);
  box-shadow: inset 0 1px 0 rgba(167, 231, 255, 0.88), 0 4px 10px rgba(22, 92, 188, 0.5), 0 0 0 2px rgba(78, 187, 255, 0.7);
  animation: rosterReadyPulse 2200ms ease-in-out infinite;
}
.model-roster__card.ready .model-roster__name { color: rgba(247, 252, 255, 0.98); }
.model-roster__card.ready .model-roster__meta { color: rgba(220, 240, 255, 0.88); }
.model-roster__card.ready .model-roster__cost.ready { color: rgba(247, 252, 255, 0.98); }
.model-roster__card.ready .model-roster__cost.ready .model-roster__cost-num { text-shadow: 0 0 6px rgba(255, 255, 255, 0.5); }
.model-roster__card.ready .model-roster__portrait { border-color: rgba(16, 74, 153, 0.5); }
/* Kind-incompatible text unification — placed AFTER the locked/ready text
   rules so source order wins at equal specificity: every string on a
   greyed card uses the same muted ink, no white-on-grey leftovers from
   the state the card would otherwise be in. */
.model-roster__card.incompatible .model-roster__name,
.model-roster__card.incompatible .model-roster__meta,
.model-roster__card.incompatible .model-roster__cost,
.model-roster__card.incompatible .model-roster__cost.ready,
.model-roster__card.incompatible .model-roster__cost.dim,
.model-roster__card.incompatible .model-roster__cost.unaffordable,
.model-roster__card.incompatible .model-roster__cost .model-roster__cost-num,
.model-roster__card.incompatible .model-roster__cost-cond {
  color: rgba(82, 76, 64, 0.85);
  text-shadow: none;
}
@keyframes rosterReadyPulse {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(167, 231, 255, 0.88), 0 4px 10px rgba(22, 92, 188, 0.5), 0 0 0 2px rgba(78, 187, 255, 0.7); }
  50%      { box-shadow: inset 0 1px 0 rgba(184, 239, 255, 0.95), 0 4px 16px rgba(22, 92, 188, 0.7), 0 0 0 3px rgba(78, 187, 255, 0.95); }
}
/* Attention blink the first time a model becomes available to unlock. Falls
   back to the gentle .ready pulse once the JS removes the .just-ready class. */
.model-roster__card.just-ready {
  animation: rosterJustReadyBlink 520ms steps(2, end) infinite;
}
@keyframes rosterJustReadyBlink {
  0%, 49% {
    background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
    box-shadow: inset 0 1px 0 rgba(167, 231, 255, 0.88), 0 4px 10px rgba(22, 92, 188, 0.5), 0 0 0 2px rgba(78, 187, 255, 0.7);
    transform: scale(1);
  }
  50%, 100% {
    background: linear-gradient(180deg, rgba(97, 201, 255, 1), rgba(47, 128, 240, 1));
    box-shadow: inset 0 1px 0 rgba(200, 240, 255, 1), 0 6px 22px rgba(22, 92, 188, 0.85), 0 0 0 4px rgba(110, 195, 255, 0.95);
    transform: scale(1.03);
  }
}
.model-roster__badge-ready--blink {
  animation: rosterBadgeBlink 520ms steps(2, end) infinite;
}
@keyframes rosterBadgeBlink {
  0%, 49% { background: #2677e7; }
  50%, 100% { background: #cfe8ff; color: #15396f; }
}

/* Random-unlock coachmark — appears on first chapter where Random Line
   Drawer becomes purchasable (status === "ready"). Positioned by JS to
   sit immediately to the right of the random card with a left-pointing
   arrow + tap-cursor over the card, same purple as the AUTO-OFF
   coachmark. JS sets `left/top/height` each frame from the card's
   bounding rect; CSS handles all the animation. */
.random-unlock-coachmark {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 12px;
  /* Container width is auto so the arrow + label drive its size. Height set
     inline by JS to match the card. */
}
.random-unlock-coachmark.hidden { display: none; }
.random-unlock-coachmark__arrow {
  width: 260px;
  height: 96px;
  /* Arrowhead sits at viewBox x=22, near the left edge. Translate so it
     overlaps the card's right edge by ~22px and reads as "stabbing into"
     the card, not just "pointing nearby." */
  margin-left: -28px;
  order: 1;
  animation: randomCoachArrowBob 720ms ease-in-out infinite;
  filter: drop-shadow(0 4px 10px rgba(194, 65, 12, 0.55));
  /* Promote to a compositor layer so the bob animation runs on the GPU
     thread instead of being repainted with the rest of the page each
     frame — the arrow's a 260×96 SVG with a drop-shadow filter, which
     is expensive to repaint repeatedly. */
  will-change: transform, opacity;
}
@keyframes randomCoachArrowBob {
  0%, 100% { transform: translateX(10px); opacity: 0.9; }
  50%      { transform: translateX(-6px); opacity: 1; }
}
.random-unlock-coachmark__cursor {
  position: absolute;
  /* Sit OVER the card. JS sets the coachmark's `left` to the card's right
     edge, so to reach into the card the cursor needs a negative offset
     proportional to card width — fixed -140px lands inside a typical
     200–300px-wide roster card. */
  left: -140px;
  top: 50%;
  width: 44px;
  height: 44px;
  transform-origin: 30% 30%;
  animation: randomCoachCursorTap 1400ms ease-in-out infinite;
  filter: drop-shadow(0 3px 7px rgba(46, 24, 8, 0.7));
  /* GPU layer — tap animation does translate+scale, so the compositor
     can interpolate without repainting the cursor SVG every frame. */
  will-change: transform;
}
/* Matches the DO ML coachmark's tap pattern (drawDoMlCoachmark in app.js):
   cursor rests above-offset from the click point, dips DOWN into the card,
   presses (brief scale-down) at ~42% of the cycle, then returns up. Period
   1400ms identical to the canvas coachmark so the two animations read as
   the same gesture. */
@keyframes randomCoachCursorTap {
  0%, 25% { transform: translate(-4px, calc(-50% - 22px)) scale(1); }
  35%     { transform: translate(0, -50%) scale(1); }
  42%     { transform: translate(0, -50%) scale(0.9); }
  50%     { transform: translate(0, -50%) scale(1); }
  70%     { transform: translate(-4px, calc(-50% - 22px)) scale(1); }
  100%    { transform: translate(-4px, calc(-50% - 22px)) scale(1); }
}
/* Ripple rings emanate from the press point in sync with the cursor's
   pressing window (matches drawDoMlCoachmark's ring loop in app.js). Two
   pseudo-elements give a leading + trailing ring just like the canvas
   coachmark. Orange to match the arrow and the DO ML coachmark's rings. */
.random-unlock-coachmark::before,
.random-unlock-coachmark::after {
  content: "";
  position: absolute;
  left: -140px;
  top: 50%;
  width: 28px;
  height: 28px;
  margin-left: -14px;
  margin-top: -14px;
  border: 2.5px solid rgba(194, 65, 12, 0.6);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  animation: randomCoachRipple 1400ms ease-out infinite;
}
.random-unlock-coachmark::after {
  border-width: 2px;
  border-color: rgba(194, 65, 12, 0.42);
  animation-delay: 220ms;
}
@keyframes randomCoachRipple {
  0%, 30% { transform: scale(0.5); opacity: 0; }
  35%     { transform: scale(0.55); opacity: 0.85; }
  60%     { transform: scale(1.6); opacity: 0.35; }
  85%     { transform: scale(2.4); opacity: 0; }
  100%    { transform: scale(2.4); opacity: 0; }
}
/* Pip variant (the chapter-reminder coachmark): the base cursor/ripple
   offset of -140px is tuned to reach back into a 200-300px-wide CARD.
   Against a 32px upgrade pip that lands the click-here tap ~108px to
   the left of the target. Re-anchor on the pip's center (16px left of
   the container's left edge, which JS pins to the pip's right edge). */
.random-unlock-coachmark--pip .random-unlock-coachmark__cursor {
  left: -22px;
}
.random-unlock-coachmark--pip::before,
.random-unlock-coachmark--pip::after {
  left: -16px;
}
/* Not-yet-affordable: keep the arrow (this is what you're saving for) but
   drop the click-here cursor + ripples — prompting a click the game would
   reject reads as broken. JS toggles this from updateReminderUpgradeCoachmark. */
.random-unlock-coachmark--no-click .random-unlock-coachmark__cursor {
  display: none;
}
.random-unlock-coachmark--no-click::before,
.random-unlock-coachmark--no-click::after {
  display: none;
}
/* Save-up bubble: sits to the right of the arrow (the container is a flex
   row; arrow is order:1), visible only in the not-affordable state. Same
   orange family as the arrow so the two read as one coachmark. */
.reminder-upgrade-coachmark__msg {
  display: none;
  order: 2;
  max-width: 170px;
  background: rgba(255, 247, 222, 0.97);
  border: 2.5px solid rgba(194, 65, 12, 0.82);
  border-radius: 12px;
  padding: 7px 12px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.3;
  color: #8a3d0d;
  text-shadow: 0 1px 0 rgba(255, 252, 235, 0.8);
  box-shadow: 0 6px 14px rgba(89, 60, 19, 0.28), inset 0 1px 0 rgba(255, 252, 235, 0.9);
}
.random-unlock-coachmark--no-click .reminder-upgrade-coachmark__msg {
  display: block;
}

/* Coachmark pointing UP at the Model Details tab. Shown when a model that
   teaches via the details panel (k-NN, MLP) has just been unlocked — same
   visual vocabulary as the random-unlock-coachmark, but rotated so the
   arrow rises from below the tab. JS sets `left/top` from the tab's
   bounding rect each frame. */
.model-details-coachmark {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.model-details-coachmark.hidden { display: none; }
.model-details-coachmark__arrow {
  width: 56px;
  height: 84px;
  /* Arrowhead is at viewBox y≈10. Translate up so the tip overlaps the
     bottom edge of the tab and reads as "stabbing into" the button. */
  margin-top: -10px;
  animation: modelDetailsCoachArrowBob 720ms ease-in-out infinite;
  filter: drop-shadow(0 4px 10px rgba(194, 65, 12, 0.55));
  will-change: transform, opacity;
}
@keyframes modelDetailsCoachArrowBob {
  0%, 100% { transform: translateY(8px); opacity: 0.9; }
  50%      { transform: translateY(-4px); opacity: 1; }
}
.model-details-coachmark__cursor {
  position: absolute;
  /* Cursor sits OVER the tab (the coachmark's top edge is anchored just
     beneath the tab, so -36px reaches back up onto the tab face). */
  left: 50%;
  top: -8px;
  margin-left: -22px;
  width: 44px;
  height: 44px;
  transform-origin: 30% 30%;
  animation: modelDetailsCoachCursorTap 1400ms ease-in-out infinite;
  filter: drop-shadow(0 3px 7px rgba(46, 24, 8, 0.7));
  will-change: transform;
}
@keyframes modelDetailsCoachCursorTap {
  0%, 25% { transform: translate(10px, -24px) scale(1); }
  35%     { transform: translate(0, -8px) scale(1); }
  42%     { transform: translate(0, -8px) scale(0.9); }
  50%     { transform: translate(0, -8px) scale(1); }
  70%     { transform: translate(10px, -24px) scale(1); }
  100%    { transform: translate(10px, -24px) scale(1); }
}
.model-details-coachmark::before,
.model-details-coachmark::after {
  content: "";
  position: absolute;
  left: 50%;
  top: -8px;
  width: 28px;
  height: 28px;
  margin-left: -14px;
  margin-top: -14px;
  border: 2.5px solid rgba(194, 65, 12, 0.6);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  animation: modelDetailsCoachRipple 1400ms ease-out infinite;
}
.model-details-coachmark::after {
  border-width: 2px;
  border-color: rgba(194, 65, 12, 0.42);
  animation-delay: 220ms;
}
@keyframes modelDetailsCoachRipple {
  0%, 30% { transform: scale(0.5); opacity: 0; }
  35%     { transform: scale(0.55); opacity: 0.85; }
  60%     { transform: scale(1.6); opacity: 0.35; }
  85%     { transform: scale(2.4); opacity: 0; }
  100%    { transform: scale(2.4); opacity: 0; }
}
.model-roster__badge-ready {
  position: absolute;
  top: -7px;
  right: 8px;
  background: #2677e7;
  color: #f4faff;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  box-shadow: 0 2px 5px rgba(22, 75, 152, 0.4);
  font-family: 'Fraunces', serif;
}
.model-roster__portrait {
  width: 64px; height: 64px;
  border-radius: 10px;
  background: #fff;
  border: 1.5px solid rgba(131, 85, 26, 0.4);
  overflow: hidden;
}
.model-roster__portrait svg { display: block; width: 100%; height: 100%; }
.model-roster__info { min-width: 0; }
.model-roster__name {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.1;
}
.model-roster__meta {
  margin-top: 3px;
  font-size: 11.5px;
  color: rgba(58, 40, 14, 0.7);
  font-family: 'JetBrains Mono', monospace;
}
.model-roster__cost {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  padding: 2px 4px 2px 6px;
  border-left: 1px solid rgba(131, 85, 26, 0.18);
  min-width: 48px;
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
  line-height: 1;
}
.model-roster__cost-row {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.model-roster__cost-num {
  font-size: 17px;
  letter-spacing: 0.02em;
}
.model-roster__cost-icon {
  width: 16px;
  height: 16px;
  filter: drop-shadow(0 1px 0 rgba(255, 247, 214, 0.85));
}
.model-roster__cost-cond {
  margin-top: 4px;
  font-family: 'Fraunces', serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  font-style: italic;
  opacity: 0.85;
  text-align: right;
  white-space: nowrap;
  max-width: 140px;
}
.model-roster__cost.ready { color: #1a4f8e; }
.model-roster__cost.unaffordable { color: rgba(106, 67, 20, 0.95); }
.model-roster__cost.ready .model-roster__cost-num { text-shadow: 0 0 6px rgba(78, 187, 255, 0.55); }
.model-roster__cost.dim { color: rgba(58, 40, 14, 0.45); }

.model-roster__upgrades {
  margin-top: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.model-roster__pip {
  width: 32px;
  height: 32px;
  border-radius: 7px;
  border: 1.5px solid rgba(58, 40, 14, 0.5);
  background: rgba(58, 40, 14, 0.18);
  padding: 0;
  cursor: var(--cursor-pointer);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-family: 'Helvetica Neue', 'Helvetica', Arial, sans-serif;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
  transition: transform 80ms ease, box-shadow 80ms ease;
}
.model-roster__pip { position: relative; }
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__pip:hover { transform: scale(1.18); }
.model-roster__pip[disabled] { cursor: default; }
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__pip[disabled]:hover { transform: none; }

/* CHAPTER REMINDER — the current assignment specifically needs this
   upgrade (REMINDER_UPGRADE_BY_ASSIGNMENT in app.js). Fast amber blink so
   the pip is unmissable; overrides the resting/unaffordable look. */
.model-roster__pip.model-roster__pip--reminder {
  animation: rosterPipReminderBlink 520ms steps(2, end) infinite;
}
/* NO transform in these keyframes: the reminder coachmark (orange arrow)
   anchors to this pip's bounding rect every frame, and a scale pulse
   changes that rect 4x/sec — forcing style.left/top re-writes on the
   coachmark that interrupt its GPU-composited bob animation (the arrow
   visibly freezes in a busy tab). Shadow-only pulse keeps the rect fixed. */
@keyframes rosterPipReminderBlink {
  0%, 49% {
    box-shadow: 0 0 0 2px rgba(255, 200, 60, 0.85), 0 0 10px rgba(255, 170, 30, 0.55);
  }
  50%, 100% {
    box-shadow: 0 0 0 5px rgba(255, 220, 80, 1), 0 0 20px rgba(255, 190, 50, 0.95);
  }
}

/* OWNED — bright, tier-colored, with a green check badge */
.model-roster__pip.owned.tier-blue   { background: #4f86c6; border-color: #1f4d85; box-shadow: 0 0 4px rgba(79, 134, 198, 0.7); }
.model-roster__pip.owned.tier-purple { background: #9067c6; border-color: #4d2f7e; box-shadow: 0 0 4px rgba(144, 103, 198, 0.7); }
.model-roster__pip.owned::after {
  content: "✓";
  position: absolute;
  top: -5px;
  right: -5px;
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background: #2fbf63;
  border: 1.5px solid #f6f1d8;
  color: #ffffff;
  font-size: 10px;
  font-weight: 800;
  line-height: 11px;
  text-align: center;
  text-shadow: none;
  box-shadow: 0 0 4px rgba(38, 144, 71, 0.5);
}

/* READY — bright, tier-colored, pulsing */
.model-roster__pip.ready.tier-blue   { background: #b8d8ff; border-color: #4f86c6; box-shadow: 0 0 5px rgba(79, 134, 198, 0.6); animation: rosterPipPulse 1600ms ease-in-out infinite; }
.model-roster__pip.ready.tier-purple { background: #d8c8f4; border-color: #9067c6; box-shadow: 0 0 5px rgba(144, 103, 198, 0.6); animation: rosterPipPulse 1600ms ease-in-out infinite; }

/* NOT CLICKABLE — unified dark for hidden-era, locked, and unaffordable */
.model-roster__pip.unaffordable,
.model-roster__pip.locked,
.model-roster__pip.hidden-era {
  background: rgba(14, 10, 4, 0.88);
  border-color: rgba(14, 10, 4, 0.95);
  color: rgba(255, 255, 255, 0.18);
  text-shadow: none;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  opacity: 1;
  animation: none;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__pip.unaffordable:hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__pip.locked:hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__pip.hidden-era:hover { transform: none; }
.model-roster__pip-icon { display: inline-flex; align-items: center; justify-content: center; }
/* Color emoji (⚡ ⏩ 🎲) ignore the parent CSS `color`, so on a locked pip
   the lightning bolt and fast-forward arrows still read as bright/yellow.
   Desaturate + dim only the icon span (not the pip background) so the
   "locked" state actually reads as locked. */
.model-roster__pip.unaffordable .model-roster__pip-icon,
.model-roster__pip.locked .model-roster__pip-icon,
.model-roster__pip.hidden-era .model-roster__pip-icon {
  filter: grayscale(1) brightness(0.45);
  opacity: 0.55;
}
@keyframes rosterPipPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.12); }
}
/* Greyed-out (kind-incompatible) card: rosterUpgradePipsHtml renders its
   pips inert-disabled, so the "buy me" scale pulse would be a tutorial cue
   pointing at something unbuyable. Same suppression the card-level
   `.ready.incompatible` rule applies to the card's own pulse. */
.model-roster__card.incompatible .model-roster__pip.ready {
  animation: none;
}
.model-roster__badge-active {
  position: absolute;
  top: -8px;
  right: 8px;
  background: #7a4d7a;
  color: #f9f0f9;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 3px 10px;
  border-radius: 999px;
  box-shadow: 0 2px 5px rgba(58, 32, 58, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.32);
  font-family: 'Fraunces', serif;
}
.model-roster__lock {
  position: absolute;
  top: 6px; right: 6px;
  font-size: 12px;
  opacity: 0.7;
}
/* Big right-side Lv-up button — anchors the third grid column of the card
   (Clicker-Heroes style: portrait left, name middle, big action button right).
   Card uses `grid-template-columns: 64px 1fr auto`, so the button sizes itself
   via `min-width` + padding and `align-self: stretch` pins it vertically.
   Affordance follows the game-wide "unlockable" palette:
     affordable    → light blue (upgrades-dock ready blue)
     unaffordable  → dark-brown (model-roster .locked) */
.model-roster__level {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  gap: 4px;
  padding: 8px 12px;
  min-width: 104px;
  background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
  /* 2px solid matches the upgrades-dock unlock pill and the side-desk ghost
     so every "unlockable" surface in the game shares one border width. */
  border: 2px solid rgba(16, 74, 153, 0.92);
  border-radius: 12px;
  box-shadow:
    0 2px 0 rgba(22, 92, 188, 0.6),
    inset 0 1px 0 rgba(184, 232, 255, 0.9);
  font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
  color: #f4faff;
  text-shadow: 0 1px 0 rgba(17, 61, 124, 0.5);
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.12s ease, background 0.12s ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__level:hover {
  transform: translateY(-1px);
  background: linear-gradient(180deg, rgba(97, 201, 255, 1), rgba(47, 128, 240, 1));
  box-shadow:
    0 3px 0 rgba(22, 92, 188, 0.6),
    inset 0 1px 0 rgba(200, 240, 255, 0.95);
}
.model-roster__level:active {
  transform: translateY(1px);
  box-shadow:
    0 1px 0 rgba(22, 92, 188, 0.6),
    inset 0 1px 0 rgba(184, 232, 255, 0.9);
}
.model-roster__level-text {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: 0.08em;
  line-height: 1.05;
}
.model-roster__level-up {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  opacity: 0.95;
}
.model-roster__level-up .point-icon { width: 13px; height: 13px; }
.model-roster__level--unaffordable {
  background: linear-gradient(180deg, #3a2a18, #1f160c);
  border-color: rgba(14, 10, 4, 0.75);
  color: rgba(255, 245, 220, 0.55);
  text-shadow: none;
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.32),
    inset 0 1px 0 rgba(255, 230, 180, 0.08);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__level--unaffordable:hover {
  /* No lift on unaffordable — keeps the affordance change readable */
  transform: none;
  background: linear-gradient(180deg, #3a2a18, #1f160c);
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.32),
    inset 0 1px 0 rgba(255, 230, 180, 0.08);
}
.model-roster__level--unaffordable .model-roster__level-up { opacity: 0.7; }

.model-roster__level--max {
  background: linear-gradient(180deg, #b88a2a, #6f4d10);
  border-color: rgba(60, 38, 8, 0.85);
  color: rgba(255, 247, 226, 0.96);
  text-shadow: 0 1px 0 rgba(40, 22, 4, 0.6);
  cursor: default;
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 230, 180, 0.32);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__level--max:hover {
  /* No lift, no color change — the button is no longer interactive. */
  transform: none;
  background: linear-gradient(180deg, #b88a2a, #6f4d10);
  box-shadow:
    0 2px 0 rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 230, 180, 0.32);
}
.model-roster__level--max .model-roster__level-up {
  font-weight: 700;
  letter-spacing: 0.06em;
  opacity: 0.95;
}

/* Speed dial: the badge is a passive frame; the notch strip and the ↑ buy
   row inside it are the actual click targets. */
.model-roster__level--dial {
  cursor: default;
}
/* When the card carries the level-up action (not at max speed), the WHOLE
   card is clickable to upgrade — so give it the pointer + hover lift that
   signals "click me", the same affordance as the other level buttons. */
.model-roster__level--dial[data-action="level-up"] {
  cursor: pointer;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__level--dial:hover,
.model-roster__level--dial:active {
  transform: none;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__level--dial[data-action="level-up"]:hover {
  transform: translateY(-1px);
}
.model-roster__speed-strip {
  display: inline-flex;
  gap: 2px;
  align-items: center;
}
.model-roster__speed-notch {
  width: 7px;
  height: 12px;
  padding: 0;
  border: 1px solid rgba(10, 40, 90, 0.65);
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.14);
  cursor: pointer;
  transition: background 0.08s ease, transform 0.08s ease;
}
.model-roster__speed-notch--on {
  background: #fdf6d8;
  border-color: rgba(10, 40, 90, 0.85);
  box-shadow: inset 0 -2px 0 rgba(214, 178, 84, 0.85);
}
.model-roster__speed-notch--paid {
  background: rgba(255, 255, 255, 0.28);
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__speed-notch--paid:hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .model-roster__speed-notch--on:hover {
  transform: translateY(-1px);
  background: #fffdf0;
}
.model-roster__speed-notch--locked {
  background: rgba(0, 0, 0, 0.28);
  border-color: rgba(0, 0, 0, 0.4);
  cursor: default;
}
.model-roster__level--unaffordable .model-roster__speed-notch--on {
  /* Dark badge (next tier unaffordable) still shows the lit notches clearly */
  background: #f2e2ae;
}
/* The whole dial card upgrades on click now (data-action="level-up" on the
   panel), so the ↑ cost is just a readout, not its own inset button (Erik
   2026-08-24: "remove the inset, keep the coin info"). Drop the box chrome —
   border, background, box padding — and let the "↑ 87 {coin}" text sit plainly
   in the card. The inline layout comes from .model-roster__level-up.
   pointer-events:none on the inner spans keeps clicks resolving to the card's
   level-up action. */
.model-roster__speed-buy {
  border: 0;
  background: none;
  padding: 0;
  margin: 0;
  color: inherit;
  font-family: inherit;
  text-shadow: inherit;
  cursor: pointer;
}
.model-roster__speed-buy:disabled {
  cursor: default;
  opacity: 0.55;
}
.model-roster__speed-buy * { pointer-events: none; }

.upgrades-dock {
  position: fixed;
  top: 18px;
  left: 20px;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: none;
  height: 100%;
  overflow: hidden;
  padding: 8px 18px 16px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  pointer-events: none;
}

.dock-summary {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 240, 202, 0.84);
  padding: 2px 6px 0;
  overflow-wrap: anywhere;
  text-shadow: 0 1px 0 rgba(72, 33, 4, 0.48);
}

.utility-dock {
  position: fixed;
  right: 20px;
  top: 20px;
  z-index: 120;
  width: auto;
  pointer-events: none;
}

.utility-dock__stack {
  display: flex;
  flex-direction: row;
  gap: 8px;
  justify-content: flex-start;
}

.utility-dock__stack--debug {
  padding: 8px 2px 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  align-items: center;
}

.utility-dock__stack--topright {
  padding: 0;
  justify-content: flex-end;
}

.utility-dock__stack--floating-right {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 120;
  padding: 4px 8px;
  gap: 6px;
  align-items: center;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(101, 74, 34, 0.96), rgba(69, 49, 20, 0.98));
  border: 1px solid rgba(179, 137, 72, 0.46);
  box-shadow: 0 14px 28px rgba(33, 22, 10, 0.28), inset 0 1px 0 rgba(255, 229, 176, 0.16);
}

.utility-dock__stack--floating-right .utility-dock__button--icon {
  width: 26px;
  min-width: 26px;
  height: 26px;
  min-height: 26px;
  font-size: 11px;
}

.utility-dock__button {
  min-width: 0;
}

.utility-dock__label {
  font: 11px/1.2 monospace;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(143, 187, 164, 0.82);
}

.utility-dock__meta {
  min-width: 112px;
  padding: 6px 10px;
  border-radius: 16px;
  background: linear-gradient(180deg, rgba(20, 34, 27, 0.96), rgba(12, 19, 15, 0.98));
  border: 1px solid rgba(95, 140, 121, 0.24);
  color: rgba(194, 220, 205, 0.78);
  font: 10px/1.2 monospace;
  text-align: left;
  box-shadow: 0 10px 22px rgba(7, 12, 9, 0.2);
}

.utility-dock__meta--compact {
  min-width: 0;
  padding: 0 4px;
  border: 0;
  background: transparent;
  box-shadow: none;
  align-self: center;
  color: rgba(251, 239, 210, 0.9);
  font-size: 9px;
  letter-spacing: 0.08em;
  white-space: nowrap;
  text-shadow: 0 1px 0 rgba(42, 25, 8, 0.28);
}

.chapter-dock {
  position: fixed;
  top: 44px;
  left: 20px;
  transform: none;
  z-index: 20;
  width: 340px;
  height: auto;
  max-height: min(360px, calc(100vh - 88px));
  overflow: visible;
  pointer-events: none;
}

.console-dock {
  position: fixed;
  right: 20px;
  top: 20px;
  bottom: auto;
  z-index: 20;
  width: 268px;
  max-width: 268px;
  padding: 0;
  background: transparent;
  box-shadow: none;
  pointer-events: none;
}

.topbar-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: flex-end;
  width: 100%;
}

.topbar-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 3px solid rgba(146, 93, 18, 0.58);
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(252, 247, 219, 0.98), rgba(235, 222, 171, 0.98));
  color: #4b3314;
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: var(--cursor-pointer);
  min-height: 38px;
  white-space: normal;
  text-align: center;
  overflow-wrap: anywhere;
  box-shadow: inset 0 2px 0 rgba(255, 247, 214, 0.95), 0 8px 0 rgba(152, 96, 18, 0.55), 0 12px 22px rgba(113, 76, 24, 0.16);
  transition: background 140ms ease, transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease, color 140ms ease;
}

.topbar-select {
  border: 3px solid rgba(146, 93, 18, 0.5);
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(252, 248, 226, 0.98), rgba(238, 228, 188, 0.98));
  color: #4b3314;
  padding: 10px 16px;
  cursor: var(--cursor-pointer);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  min-height: 44px;
  min-width: 260px;
  box-shadow: inset 0 2px 0 rgba(255, 247, 214, 0.92), 0 8px 0 rgba(152, 96, 18, 0.44), 0 10px 18px rgba(113, 76, 24, 0.12);
}

.console-card {
  width: 100%;
  min-width: 0;
  min-height: 188px;
  padding: 12px 14px 14px;
  border: 4px solid rgba(122, 69, 11, 0.72);
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(101, 55, 11, 0.96), rgba(72, 38, 8, 0.98) 18%, rgba(244, 233, 195, 0.98) 18.5%, rgba(238, 228, 189, 0.97) 100%);
  box-shadow: 0 18px 30px rgba(89, 60, 19, 0.22), inset 0 2px 0 rgba(245, 189, 103, 0.32);
  color: rgba(52, 36, 14, 0.88);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.console-card__label {
  margin-bottom: 8px;
  padding: 0;
  color: rgba(255, 232, 191, 0.78);
  text-shadow: 0 1px 0 rgba(54, 26, 4, 0.5);
}

.console-card--embedded {
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  color: rgba(255, 238, 202, 0.88);
}

.console-card__toprow {
  display: grid;
  grid-template-columns: minmax(260px, 1fr) auto auto;
  gap: 8px;
  align-items: center;
  margin-bottom: 10px;
}

.console-card__toprow .topbar-select {
  height: 44px;
  min-height: 44px;
}

.console-card__status {
  display: grid;
  gap: 6px;
  padding: 8px 0 0;
  border-radius: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
}

.console-card--embedded .console-line {
  color: rgba(255, 238, 202, 0.9);
  text-shadow: 0 1px 0 rgba(53, 26, 4, 0.45);
}

.console-card--embedded .topbar-select {
  color: #4b3314;
}

.utility-dock__button--icon {
  width: 38px;
  height: 38px;
  min-height: 38px;
  padding: 0;
  border-radius: 999px;
  font-size: 13px;
  letter-spacing: 0.08em;
}

.breakthrough-strip {
  width: 100%;
  padding: 12px 16px 14px;
  border: 3px solid rgb(131, 85, 26);
  border-radius: 28px;
  /* Fully opaque (rgb, not rgba) so the cream parchment can never blend with
     whatever sits behind the chapter dock. The previous 0.98-alpha gradient
     could end up reading bluish in some browser/compositor combinations when
     the body's sky gradient bled through; pinning the channels to 100%
     keeps the strip visually distinct on every layout/viewport. */
  background: linear-gradient(180deg, rgb(251, 247, 225), rgb(238, 229, 192));
  box-shadow: 0 18px 30px rgba(89, 60, 19, 0.22), inset 0 2px 0 rgb(255, 245, 209);
  color: rgb(58, 40, 14);
  pointer-events: auto;
  /* `isolation: isolate` creates a new stacking context so the fx-overlay
     canvas (z-index 75, sits above the chapter dock at z-index 20) cannot
     accidentally composite into the strip's pixels during animation passes
     — particles and point bursts now layer over the strip, never blend
     into its background. */
  isolation: isolate;
}

.breakthrough-strip__header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: 8px;
}

.breakthrough-strip__utility {
  grid-column: 3;
  justify-self: end;
  display: inline-flex;
  gap: 4px;
}

.breakthrough-strip__utility .utility-dock__button--icon {
  width: 32px;
  height: 32px;
  min-height: 32px;
  font-size: 11px;
}

.breakthrough-strip__utility .utility-dock__button--icon .utility-dock__icon-svg {
  display: block;
  width: 18px;
  height: 18px;
}

.breakthrough-strip__headline {
  grid-column: 2;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 9px 14px 10px;
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(247, 238, 206, 0.92), rgba(235, 223, 179, 0.96));
  border: 1px solid rgba(131, 85, 26, 0.2);
  box-shadow: inset 0 1px 0 rgba(255, 245, 209, 0.9);
  max-width: 100%;
  min-width: 0;
}

.breakthrough-strip__chapter {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border-radius: 999px;
  background: transparent;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: none;
  color: rgba(106, 67, 20, 0.96);
  flex: 0 0 auto;
}

.breakthrough-strip__title {
  min-width: 0;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: none;
  white-space: normal;
  text-wrap: balance;
}

.breakthrough-strip__metrics {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 14px;
  padding-left: 14px;
  border-left: 1px solid rgba(131, 85, 26, 0.32);
  font-family: "Trebuchet MS", "Avenir Next Condensed", "Gill Sans", sans-serif;
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  color: rgba(120, 76, 18, 0.95);
}

.breakthrough-strip__metric-points,
.breakthrough-strip__metric-acc {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.breakthrough-strip__metric-icon {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  filter: drop-shadow(0 1px 0 rgba(255, 247, 214, 0.85));
}

.breakthrough-strip__metric-sep {
  opacity: 0.55;
  font-weight: 400;
}

.breakthrough-strip__metric-acc {
  color: rgba(60, 92, 52, 0.95);
}

.breakthrough-strip__metric--placeholder {
  visibility: hidden;
}

.breakthrough-strip__checklist {
  margin-top: 6px;
  display: grid;
  gap: 5px;
}

.breakthrough-strip__checkitem {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  gap: 8px;
  align-items: start;
  font-size: 10px;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(78, 58, 24, 0.78);
}

.breakthrough-strip__checkitem--done {
  color: rgba(42, 102, 54, 0.88);
}

.breakthrough-strip__checkitem--active {
  color: rgba(111, 67, 32, 0.92);
  font-weight: 700;
}

.breakthrough-strip__checkmark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 14px;
  font-weight: 700;
}

.breakthrough-strip__checktext {
  min-width: 0;
  overflow-wrap: anywhere;
}

.breakthrough-strip__progress-row {
  margin-top: 8px;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch;
  gap: 8px;
}

.breakthrough-strip__progress-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.breakthrough-strip__progress-card {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-height: 26px;
}

.breakthrough-strip__progress-label {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  gap: 8px;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  opacity: 0.72;
  align-items: baseline;
}

.breakthrough-strip__progress-card--gate .breakthrough-strip__progress-label {
  opacity: 0.58;
}

.breakthrough-strip__progress-kicker {
  white-space: nowrap;
}

.breakthrough-strip__progress-value {
  min-width: 0;
  text-align: right;
  overflow-wrap: anywhere;
  line-height: 1.15;
}

.breakthrough-strip__progress {
  position: relative;
  height: 12px;
  background: rgba(106, 79, 34, 0.12);
  border: 2px solid rgba(124, 74, 18, 0.18);
  overflow: hidden;
  border-radius: 999px;
}

.breakthrough-strip__progress-fill {
  height: 100%;
  background: linear-gradient(90deg, rgba(227, 180, 70, 0.78), rgba(182, 112, 31, 0.98));
  transition: width 220ms ease, background 180ms ease;
}

.breakthrough-strip__progress--ready {
  background: rgba(59, 118, 72, 0.08);
  border-color: rgba(59, 118, 72, 0.22);
}

.breakthrough-strip__progress--ready .breakthrough-strip__progress-fill {
  background: linear-gradient(90deg, rgba(79, 149, 92, 0.42), rgba(79, 149, 92, 0.9));
}

.breakthrough-strip__progress--gate {
  height: 14px;
  background: rgba(78, 120, 143, 0.12);
  border-color: rgba(61, 95, 124, 0.28);
  transition: background 180ms ease, border-color 180ms ease;
  overflow: visible;
}

.breakthrough-strip__progress-threshold {
  position: absolute;
  top: -6px;
  bottom: -6px;
  width: 3px;
  background: rgba(22, 41, 60, 0.96);
  transform: translateX(-1.5px);
  z-index: 2;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
}

.breakthrough-strip__progress-threshold::before {
  content: "";
  position: absolute;
  top: -6px;
  left: -7px;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 7px solid rgba(22, 41, 60, 0.96);
}

/* Phone-only inline restatement of the gate — shown solely by the
   .mobile-tuned phone block at the end of this file. */
.breakthrough-strip__progress-inline-threshold {
  display: none;
}

.breakthrough-strip__progress-threshold-label {
  position: absolute;
  top: -22px;
  transform: translateX(-50%);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: rgba(22, 41, 60, 0.96);
  background: rgba(255, 255, 255, 0.92);
  padding: 1px 5px;
  border-radius: 4px;
  border: 1px solid rgba(22, 41, 60, 0.32);
  white-space: nowrap;
  z-index: 3;
}

.breakthrough-strip__progress-passzone {
  position: absolute;
  top: 0;
  bottom: 0;
  background: repeating-linear-gradient(
    135deg,
    rgba(79, 149, 92, 0.10),
    rgba(79, 149, 92, 0.10) 5px,
    rgba(79, 149, 92, 0.18) 5px,
    rgba(79, 149, 92, 0.18) 10px
  );
  z-index: 0;
  pointer-events: none;
}

.breakthrough-strip__progress-fill--gate {
  position: relative;
  z-index: 0;
  background: linear-gradient(90deg, rgba(79, 162, 196, 0.62), rgba(55, 116, 176, 0.96));
}

.breakthrough-strip__progress--gate-low {
  background: rgba(137, 44, 44, 0.08);
  border-color: rgba(137, 44, 44, 0.22);
}

.breakthrough-strip__progress--gate-low .breakthrough-strip__progress-fill--gate {
  background: linear-gradient(90deg, rgba(176, 76, 76, 0.42), rgba(176, 76, 76, 0.9));
}

.breakthrough-strip__progress--gate-ready {
  background: rgba(59, 118, 72, 0.08);
  border-color: rgba(59, 118, 72, 0.22);
}

.breakthrough-strip__progress--gate-ready .breakthrough-strip__progress-fill--gate {
  background: linear-gradient(90deg, rgba(79, 149, 92, 0.42), rgba(79, 149, 92, 0.9));
}

/* Timed chapters (open-training/"Final Study") — a big plain numeral, NOT a
   progress bar: players read this strip's bars as a correctness/pass-fail
   signal (the accuracy gate above), and a draining bar here read as "you're
   failing" when there's nothing to fail. Neutral violet tone, distinct from
   the gate's red/green, until the last 10s go "urgent" red. Re-rendered
   fresh every second (state.ui.chapterSignature includes the live second),
   so the little scale-pop below plays on every tick for free — no JS timer
   loop needed here beyond the render itself. */
.breakthrough-strip__timer-big {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  min-height: 26px;
  padding: 6px 0;
}

.breakthrough-strip__timer-big-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(96, 64, 132, 0.75);
}

.breakthrough-strip__timer-big-value {
  font-size: 34px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: rgba(76, 48, 110, 0.96);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
  animation: breakthroughTimerBigTick 220ms ease-out;
}

.breakthrough-strip__timer-big-unit {
  font-size: 16px;
  font-weight: 700;
  margin-left: 1px;
}

@keyframes breakthroughTimerBigTick {
  from { transform: scale(1.22); }
  to   { transform: scale(1); }
}

.breakthrough-strip__timer-big--urgent .breakthrough-strip__timer-big-label,
.breakthrough-strip__timer-big--urgent .breakthrough-strip__timer-big-value {
  color: rgba(150, 34, 30, 0.98);
}

.breakthrough-strip__timer-big--urgent .breakthrough-strip__timer-big-value {
  animation: breakthroughTimerBigTick 220ms ease-out, breakthroughTimerBigUrgentPulse 0.6s ease-in-out infinite;
}

@keyframes breakthroughTimerBigUrgentPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* Animated "delta flash" overlay used when a progress fill jumps. A colored
   segment covers the change region in a brighter tone and fades; a tick line
   marks the previous position and fades alongside it. */
@keyframes breakthroughDeltaFlash {
  0% { opacity: 0; }
  18% { opacity: 1; }
  55% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes breakthroughDeltaTick {
  0% { opacity: 0; }
  18% { opacity: 1; }
  70% { opacity: 0.85; }
  100% { opacity: 0; }
}
.breakthrough-strip__progress-delta {
  position: absolute;
  top: 0;
  bottom: 0;
  pointer-events: none;
  z-index: 1;
  background: linear-gradient(90deg, rgba(255, 232, 152, 0.95), rgba(255, 196, 96, 0.92));
  box-shadow: 0 0 6px rgba(255, 220, 140, 0.7);
  animation: breakthroughDeltaFlash 900ms ease-out forwards;
}
.breakthrough-strip__progress-delta--gate {
  background: linear-gradient(90deg, rgba(190, 240, 220, 0.95), rgba(120, 210, 180, 0.92));
  box-shadow: 0 0 6px rgba(160, 230, 190, 0.7);
}
.breakthrough-strip__progress-delta--loss {
  background: linear-gradient(90deg, rgba(220, 140, 130, 0.92), rgba(200, 90, 80, 0.92));
  box-shadow: 0 0 6px rgba(220, 130, 110, 0.6);
}
.breakthrough-strip__progress-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: rgba(245, 240, 220, 0.95);
  box-shadow: 0 0 4px rgba(245, 240, 220, 0.8);
  pointer-events: none;
  z-index: 2;
  animation: breakthroughDeltaTick 900ms ease-out forwards;
}

.breakthrough-strip__advance,
.breakthrough-strip__select {
  width: 100%;
  max-width: none;
  min-width: 260px;
  align-self: stretch;
}

.breakthrough-strip__advance {
  min-height: 44px;
  height: 44px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Real World Simulation's "go back and train more" escape hatch — amber
   warning treatment, distinct from the blue .ready Proceed button, so it
   reads as "step back" rather than "move forward". */
/* Real World Simulation (locked-evaluation)'s "go back and train more"
   escape hatch — a standalone floating corner button, NOT part of the
   breakthrough-strip/chapter-dock (deliberately: it's an emergency exit for
   a frozen net, not another dock control, so it lives in the bottom-right
   corner of the screen on its own). See locked-eval-back-button in
   index.html and its wiring in ui/chapter-dock.js. Amber "step back"
   treatment, distinct from the dock's blue .ready Proceed button. */
.locked-eval-back-button {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 120;
  max-width: 240px;
  background: linear-gradient(180deg, rgba(255, 190, 110, 0.98), rgba(214, 120, 30, 0.98));
  border-color: rgba(140, 72, 10, 0.9);
  color: #3a2306;
  text-shadow: 0 1px 0 rgba(255, 228, 180, 0.55);
  box-shadow:
    inset 0 2px 0 rgba(255, 226, 180, 0.85),
    0 10px 0 rgba(140, 72, 10, 0.5),
    0 18px 28px rgba(150, 80, 15, 0.28);
  animation: lockedEvalBackButtonIn 220ms ease-out;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .locked-eval-back-button:hover,
.locked-eval-back-button:focus-visible {
  filter: saturate(1.08) brightness(1.04);
}
.locked-eval-back-button.hidden {
  display: none;
}
@keyframes lockedEvalBackButtonIn {
  from { opacity: 0; transform: translateY(10px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 700px) {
  .locked-eval-back-button {
    right: 12px;
    bottom: 12px;
    max-width: 180px;
    font-size: 12px;
    padding: 8px 12px;
  }
}

/* Ship the Model's regularization control box — position/left/top/width AND
   height come from layoutHudLanes (app.js), a FIXED height matching
   SHIP_MODEL_DIAL_BOX_HEIGHT (layout.js), which playfield() also reserves
   below the field on those two chapters. Fixed, not content-measured, on
   both ends — the box can never overlap the playfield above it (the field
   itself was shrunk to leave room) and never clips its own content (every
   size below is tuned to fit inside that exact reserve). Captions moved to
   each preset button's title tooltip (ui/ship-model-dial-box.js) instead of
   a permanent caption line — the biggest single space cost of the earlier,
   taller draft. */
.ship-model-dial-box {
  position: fixed;
  z-index: 15;
  overflow: hidden;
}
.ship-model-dial-box.hidden {
  display: none;
}
.ship-model-dial-box__panel {
  box-sizing: border-box;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, rgba(255, 251, 240, 0.97), rgba(240, 226, 190, 0.97));
  border: 2px solid rgba(146, 93, 18, 0.48);
  border-radius: 12px;
  padding: 6px 8px 5px;
  box-shadow: inset 0 2px 0 rgba(255, 247, 214, 0.8), 0 8px 18px rgba(90, 60, 20, 0.2);
}
.ship-model-dial-box__title {
  flex: none;
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(96, 64, 20, 0.8);
  text-align: center;
  margin-bottom: 3px;
}
.ship-model-dial-box__row {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: 5px;
  justify-content: center;
}
.ship-model-dial-box__dial {
  /* flex-basis 0 (not a px value) so the three columns always divide the row
     evenly and stay side by side — a min-width flex-basis was wrapping the
     third column onto its own row (and off the bottom of the box) on
     narrower playfields instead of just shrinking all three together. */
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 3px 2px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.28);
  overflow: hidden;
}
.ship-model-dial-box__dial-label {
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: rgba(70, 45, 12, 0.85);
  white-space: nowrap;
}
.ship-model-dial-box__dial-label--l2::before { content: "λ "; }
.ship-model-dial-box__dial-label--dropout::before { content: "◌ "; }
.ship-model-dial-box__dial-label--bn::before { content: "⇕ "; }
.ship-model-dial-box__buttons {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  justify-content: center;
}
/* λ / dropout's 5 presets: a FIXED 3-then-2 grid instead of flex-wrap's
   width-dependent packing — see the grid3 comment in
   ui/ship-model-dial-box.js for why that matters for this box's fixed
   height. */
.ship-model-dial-box__buttons--grid3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
  width: 100%;
}
.ship-model-dial-box__btn {
  font: inherit;
  font-size: 9px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 2px 3px;
  border-radius: 6px;
  border: 1.5px solid rgba(146, 93, 18, 0.32);
  background: linear-gradient(180deg, rgba(255, 251, 240, 0.9), rgba(238, 222, 182, 0.9));
  color: rgba(70, 45, 12, 0.85);
  cursor: var(--cursor-pointer);
  box-shadow: 0 2px 0 rgba(146, 93, 18, 0.22);
  transition: transform 120ms ease, box-shadow 120ms ease, background 140ms ease, color 140ms ease, border-color 140ms ease;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .ship-model-dial-box__btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 3px 0 rgba(146, 93, 18, 0.28);
}
.ship-model-dial-box__btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 0 rgba(146, 93, 18, 0.28);
}
/* Active state gets a distinct jewel tone per dial — blue/amber/teal — so
   the three columns stay easy to tell apart at a glance, not just a single
   generic "selected" color repeated three times. */
.ship-model-dial-box__btn--l2.ship-model-dial-box__btn--active {
  background: linear-gradient(180deg, #7fa8ff, #2e5fdb);
  border-color: #23449e;
  color: #fff;
  box-shadow: 0 2px 0 #1d3a8a;
}
.ship-model-dial-box__btn--dropout.ship-model-dial-box__btn--active {
  background: linear-gradient(180deg, #ffc172, #dc8720);
  border-color: #a3630f;
  color: #3a2306;
  box-shadow: 0 2px 0 #8a5511;
}
.ship-model-dial-box__btn--bn.ship-model-dial-box__btn--active {
  background: linear-gradient(180deg, #6fdab7, #24a274);
  border-color: #197c56;
  color: #06291c;
  box-shadow: 0 2px 0 #145e40;
}
/* Locked-value badge (8.3/8.4/8.5 force λ/dropout regardless of ownership) —
   same footprint as .ship-model-dial-box__btn so the column doesn't jump
   size, but non-interactive: no hover lift, no pointer cursor. */
.ship-model-dial-box__locked {
  font-size: 9px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 2px 6px;
  border-radius: 6px;
  border: 1.5px dashed rgba(146, 93, 18, 0.4);
  background: rgba(146, 93, 18, 0.1);
  color: rgba(70, 45, 12, 0.7);
  cursor: help;
}
@media (max-width: 900px) {
  .ship-model-dial-box__panel {
    padding: 4px 6px 4px;
  }
  .ship-model-dial-box__title {
    font-size: 9px;
    margin-bottom: 2px;
  }
  .ship-model-dial-box__btn {
    padding: 2px 4px;
    font-size: 9px;
  }
}

/* 8.5 BatchNorm unit inspector — clicking a hidden unit in the Model
   Details network diagram opens this popup (ui/mlp-unit-inspector.js).
   Same fixed-backdrop + centered-panel shape as .milestone-overlay, but its
   own panel styling (wider, chart-heavy, same parchment palette as
   .ship-model-dial-box). */
.mlp-unit-inspector {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(22, 21, 19, 0.5);
}
.mlp-unit-inspector.hidden {
  display: none;
}
/* Circular, matching the reference screenshot — a big tan "porthole" rather
   than a rounded rectangle. width==height (aspect-ratio) + border-radius 50%
   makes the true circle; the inner __content column is narrower than the
   panel and padded generously top/bottom so it clears the curve at the
   corners instead of poking past the circle's silhouette. */
.mlp-unit-inspector__panel {
  width: min(660px, 92vw, 86vh);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  border: 3px solid rgba(146, 93, 18, 0.48);
  background: linear-gradient(180deg, rgba(255, 251, 240, 0.99), rgba(240, 226, 190, 0.99));
  box-shadow: 0 26px 60px rgba(60, 40, 10, 0.32), inset 0 2px 0 rgba(255, 247, 214, 0.8);
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mlp-unit-inspector__content {
  width: 66%;
  max-width: 380px;
}
.mlp-unit-inspector__title {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: rgba(58, 40, 14, 0.92);
}
.mlp-unit-inspector__subtitle {
  margin-top: 2px;
  font-size: 11px;
  color: rgba(70, 50, 20, 0.7);
}
.mlp-unit-inspector__svg {
  width: 100%;
  height: auto;
  margin-top: 8px;
}
.mlp-unit-inspector__axis-label {
  margin-top: 2px;
  font-size: 10px;
  color: rgba(70, 50, 20, 0.6);
}
.mlp-unit-inspector__row {
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.mlp-unit-inspector__row-label {
  font-size: 12px;
  font-weight: 800;
  color: rgba(58, 40, 14, 0.85);
}
.mlp-unit-inspector__bnbtn {
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 14px;
  border-radius: 8px;
  border: 1.5px solid rgba(146, 93, 18, 0.32);
  background: linear-gradient(180deg, rgba(255, 251, 240, 0.9), rgba(238, 222, 182, 0.9));
  color: rgba(70, 45, 12, 0.85);
  cursor: var(--cursor-pointer);
}
.mlp-unit-inspector__bnbtn--active {
  background: linear-gradient(180deg, #6fdab7, #24a274);
  border-color: #197c56;
  color: #06291c;
}
.mlp-unit-inspector__meter {
  margin-top: 7px;
  text-align: left;
}
.mlp-unit-inspector__meter-label {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(58, 40, 14, 0.75);
}
.mlp-unit-inspector__meter-bar {
  margin-top: 3px;
  height: 8px;
  border-radius: 5px;
  background: rgba(146, 93, 18, 0.16);
  overflow: hidden;
}
.mlp-unit-inspector__meter-fill {
  height: 100%;
  background: linear-gradient(90deg, #5ec873, #268c3c);
  transition: width 200ms ease;
}
.mlp-unit-inspector__meter-value {
  margin-top: 2px;
  font-size: 10px;
  color: rgba(70, 50, 20, 0.65);
}
.mlp-unit-inspector__caption {
  margin-top: 9px;
  font-size: 11px;
  line-height: 1.35;
  color: rgba(58, 40, 14, 0.85);
}
.mlp-unit-inspector__close {
  margin-top: 10px;
}

@media (max-width: 900px) {
  .chapter-dock {
    width: min(620px, calc(100vw - 32px));
  }

  .breakthrough-strip__progress-row {
    align-items: stretch;
    flex-direction: column;
  }

  .breakthrough-strip__advance,
  .breakthrough-strip__select {
    max-width: none;
    width: 100%;
  }

  .title-screen {
    padding: 18px;
  }

  .title-screen__panel {
    padding: 24px 20px 22px;
  }

  .title-screen__subtitle {
    font-size: 16px;
  }
}

.console-title {
  margin-bottom: 8px;
}

.console-line + .console-line {
  margin-top: 6px;
}

.console-line {
  color: rgba(79, 55, 18, 0.9);
}

.console-insight {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(22, 21, 19, 0.12);
}

.console-insight__title {
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(22, 21, 19, 0.78);
}

.console-insight__blurb {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.35;
  letter-spacing: 0.02em;
  text-transform: none;
  color: rgba(22, 21, 19, 0.68);
  overflow-wrap: anywhere;
}

.console-insight__facts {
  margin-top: 7px;
  display: grid;
  gap: 5px;
}

.console-insight__fact {
  font-size: 10px;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(22, 21, 19, 0.78);
  overflow-wrap: anywhere;
}

.console-actions {
  margin-top: 10px;
}

.topbar-button:disabled {
  opacity: 0.4;
  cursor: var(--cursor-default);
  box-shadow: none;
}


:where(body:not(.mobile-tuned):not(.landscape-phone)) .topbar-button:not(:disabled):hover {
  background: linear-gradient(180deg, rgba(255, 249, 229, 1), rgba(243, 232, 188, 1));
  border-color: rgba(131, 85, 26, 0.46);
  transform: translateY(-2px);
  box-shadow: inset 0 2px 0 rgba(255, 247, 214, 0.98), 0 10px 0 rgba(152, 96, 18, 0.52), 0 14px 24px rgba(113, 76, 24, 0.2);
}

.topbar-button:focus-visible,
.topbar-select:focus-visible {
  outline: none;
  border-color: rgba(94, 156, 255, 0.72);
  box-shadow: 0 0 0 3px rgba(94, 156, 255, 0.16), 0 10px 22px rgba(22, 21, 19, 0.08);
}

.topbar-button.ready {
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(135deg, rgba(255, 244, 126, 0.98), rgba(150, 245, 121, 0.98) 42%, rgba(102, 224, 255, 0.98) 100%);
  border-color: rgb(43, 111, 77);
  color: rgb(23, 53, 39);
  text-shadow: 0 1px 0 rgba(250, 255, 230, 0.72);
  box-shadow:
    inset 0 2px 0 rgba(255, 251, 222, 1),
    0 0 0 4px rgba(132, 244, 187, 0.46),
    0 18px 30px rgba(39, 132, 89, 0.26);
  animation: unlockPulse 1s ease-in-out infinite;
  will-change: transform, box-shadow, filter;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .topbar-button.ready:not(:disabled):hover,
.topbar-button.ready:not(:disabled):focus-visible {
  background:
    linear-gradient(135deg, rgba(255, 237, 112, 1), rgba(150, 252, 138, 1) 36%, rgba(106, 232, 255, 1) 72%, rgba(255, 159, 221, 0.98) 100%);
  border-color: rgb(31, 119, 115);
  color: rgb(18, 44, 46);
  transform: translateY(-5px) scale(1.05);
  filter: saturate(1.14) brightness(1.08);
  box-shadow:
    inset 0 2px 0 rgba(255, 252, 232, 1),
    0 0 0 6px rgba(150, 245, 232, 0.52),
    0 0 24px rgba(106, 232, 255, 0.34),
    0 24px 36px rgba(35, 126, 132, 0.34);
}

.topbar-button.ready:not(:disabled):active {
  transform: translateY(-2px) scale(1.02);
  filter: saturate(1.08) brightness(1.01);
  box-shadow:
    inset 0 3px 0 rgba(244, 250, 220, 0.96),
    0 0 0 5px rgba(144, 238, 209, 0.42),
    0 14px 22px rgba(35, 126, 132, 0.26);
}

/* Urgent-blink override for upgrades dock buttons the current chapter
   specifically needs — see REMINDER_UPGRADE_BY_ASSIGNMENT in app.js.
   Replaces the gentle .ready unlockPulse with a faster, higher-contrast
   amber blink so the player can't miss the recommended buy. */
.topbar-button.ready.topbar-button--blink {
  animation: upgradeBlinkUrgent 520ms steps(2, end) infinite;
}
@keyframes upgradeBlinkUrgent {
  0%, 49% {
    box-shadow:
      inset 0 2px 0 rgba(255, 251, 222, 1),
      0 0 0 4px rgba(255, 200, 60, 0.85),
      0 0 22px rgba(255, 170, 30, 0.55),
      0 18px 30px rgba(39, 132, 89, 0.26);
    transform: scale(1.02);
  }
  50%, 100% {
    box-shadow:
      inset 0 2px 0 rgba(255, 251, 222, 1),
      0 0 0 6px rgba(255, 220, 80, 1),
      0 0 38px rgba(255, 190, 50, 0.95),
      0 22px 40px rgba(255, 140, 30, 0.45);
    transform: scale(1.06);
  }
}

.topbar-button--active {
  background: linear-gradient(180deg, rgba(126, 186, 85, 0.98), rgba(72, 133, 43, 0.98));
  border-color: rgba(49, 93, 30, 0.58);
  color: #f6f7df;
  /* Three-layer shadow matches the base .topbar-button wood plank — inner
     highlight, 8px solid green underline, soft outer drop. Without the solid
     underline the active sound-toggle looked flat next to the wooden menu
     button sitting in the same row. */
  box-shadow: inset 0 2px 0 rgba(214, 244, 169, 0.72), 0 8px 0 rgba(49, 93, 30, 0.6), 0 12px 22px rgba(53, 101, 31, 0.24);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .topbar-button--active:not(:disabled):hover {
  background: linear-gradient(180deg, rgba(139, 200, 96, 1), rgba(77, 143, 46, 1));
  border-color: rgba(49, 93, 30, 0.68);
  box-shadow: inset 0 2px 0 rgba(225, 250, 178, 0.78), 0 10px 0 rgba(49, 93, 30, 0.62), 0 14px 24px rgba(53, 101, 31, 0.26);
}

.topbar-button--inactive {
  background: linear-gradient(180deg, rgba(245, 240, 223, 0.98), rgba(222, 213, 179, 0.98));
  border-color: rgba(131, 85, 26, 0.28);
  color: rgba(90, 66, 29, 0.86);
}

.topbar-button--mode {
  background: linear-gradient(180deg, rgba(244, 236, 206, 0.98), rgba(227, 214, 170, 0.98));
  border-color: rgba(131, 85, 26, 0.42);
  color: rgba(81, 58, 24, 0.94);
  text-shadow: 0 1px 0 rgba(255, 245, 214, 0.36);
  box-shadow: inset 0 2px 0 rgba(255, 247, 220, 0.9), 0 8px 0 rgba(152, 96, 18, 0.42), 0 12px 20px rgba(113, 76, 24, 0.14);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .topbar-button--mode:not(:disabled):hover {
  background: linear-gradient(180deg, rgba(249, 241, 214, 1), rgba(233, 220, 178, 1));
  border-color: rgba(131, 85, 26, 0.56);
  box-shadow: inset 0 2px 0 rgba(255, 249, 228, 0.96), 0 10px 0 rgba(152, 96, 18, 0.38), 0 14px 22px rgba(113, 76, 24, 0.16);
}



.topbar-button--browse {
  background: linear-gradient(180deg, rgba(97, 201, 255, 0.98), rgba(47, 128, 240, 0.98));
  border-color: rgba(16, 74, 153, 0.92);
  color: rgba(247, 252, 255, 0.98);
  text-shadow: 0 1px 0 rgba(17, 61, 124, 0.46);
  box-shadow: inset 0 2px 0 rgba(184, 239, 255, 0.9), 0 8px 0 rgba(22, 92, 188, 0.62), 0 16px 26px rgba(25, 96, 186, 0.18);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .topbar-button--browse:not(:disabled):hover {
  background: linear-gradient(180deg, rgba(118, 212, 255, 1), rgba(58, 138, 245, 1));
  border-color: rgba(16, 74, 153, 0.98);
  box-shadow: inset 0 2px 0 rgba(204, 244, 255, 0.94), 0 10px 0 rgba(22, 92, 188, 0.58), 0 18px 28px rgba(25, 96, 186, 0.22);
}

.upgrades-dock .topbar-button,
.upgrades-dock .topbar-select,
.console-dock .topbar-button,
.console-dock .topbar-select,
.utility-dock .topbar-button {
  width: 100%;
  pointer-events: auto;
}

.upgrades-dock__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
  align-items: start;
}

.upgrades-dock__toprow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: start;
  margin-bottom: 5px;
}


.upgrades-dock__browse-button {
  width: 48px !important;
  min-width: 48px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  justify-content: center !important;
  align-items: center;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: 0;
  white-space: nowrap;
  overflow-wrap: normal;
  word-break: keep-all;
  line-height: 1;
}

.upgrades-dock__grid--cols-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.upgrades-dock__grid--cols-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.upgrades-dock .topbar-button:not(.topbar-button--tree),
.console-dock .topbar-button,
.utility-dock .topbar-button:not(.utility-dock__button--icon) {
  justify-content: flex-start;
  padding-left: 16px;
}

.upgrades-dock__grid .topbar-button {
  width: 100%;
  min-width: 0;
  min-height: 26px;
  padding: 4px 10px;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  border-width: 2px;
  border-radius: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
  text-align: left;
  line-height: 1.15;
  box-shadow:
    inset 0 1px 0 rgba(255, 247, 214, 0.85),
    0 4px 0 rgba(152, 96, 18, 0.50),
    0 7px 12px rgba(113, 76, 24, 0.14);
}

.upgrades-dock__more {
  margin-top: 8px;
  width: 100%;
}

/* Numeric portion of a point cost — used in topbar pills, level-up buttons,
   and anywhere else HTML shows a `1,200⊙`-style cost. Mirrors the model-roster
   card cost so the digits read consistently across the UI. */
.cost-num {
  font-family: "JetBrains Mono", monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  text-transform: none;
}

/* Inline coin glyph that follows a cost number. Sized to sit next to a single
   line of button text without inflating the row height. */
.point-icon {
  display: inline-block;
  vertical-align: -0.18em;
  width: 1.05em;
  height: 1.05em;
  margin-left: 4px;
  filter: drop-shadow(0 1px 0 rgba(255, 247, 214, 0.85));
}
.point-icon--upgrade-dock {
  width: 13px;
  height: 13px;
}
.point-icon--offline {
  width: 18px;
  height: 18px;
  margin-left: 6px;
  vertical-align: -0.15em;
}
.point-icon--tooltip {
  width: 12px;
  height: 12px;
  margin-left: 2px;
  vertical-align: -0.12em;
}

.utility-dock .utility-dock__button--icon {
  width: 38px;
  min-width: 38px;
  padding-left: 0;
  justify-content: center;
}

.upgrades-dock .topbar-button.ready,
.console-dock .topbar-button--active,
.utility-dock .topbar-button--active {
  background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
  border-color: rgba(16, 74, 153, 0.92);
  color: rgba(247, 252, 255, 0.98);
  text-shadow: 0 1px 0 rgba(17, 61, 124, 0.46);
  box-shadow: inset 0 2px 0 rgba(167, 231, 255, 0.88), 0 8px 0 rgba(22, 92, 188, 0.66), 0 16px 26px rgba(25, 96, 186, 0.2);
}


:where(body:not(.mobile-tuned):not(.landscape-phone)) .upgrades-dock .topbar-button.ready:not(:disabled):hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .console-dock .topbar-button--active:not(:disabled):hover,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .utility-dock .topbar-button--active:not(:disabled):hover {
  background: linear-gradient(180deg, rgba(97, 201, 255, 1), rgba(47, 128, 240, 1));
  border-color: rgba(16, 74, 153, 0.98);
  box-shadow: inset 0 2px 0 rgba(184, 239, 255, 0.94), 0 10px 0 rgba(22, 92, 188, 0.62), 0 18px 28px rgba(25, 96, 186, 0.22);
}

.breakthrough-strip__advance.ready {
  /* Rainbow cascade (Erik 2026-09-26, "more exciting and obvious ... cascading
     through many colors not just blue"): a wide 7-stop spectrum slides across
     the button (background-position over a 400%-wide gradient) while the halo
     cycles through the same hues in step. Replaces the two-tone blue blink.
     Stops are kept in the 45-55% lightness band so white text stays legible
     over the yellow/green segment. Period ADVANCE_CASCADE_PERIOD (1.8s) is
     mirrored in ui/chapter-dock.js so re-renders keep the phase. */
  background-image: linear-gradient(90deg,
    #e8433a 0%, #f08a24 14%, #d9b800 28%, #2fb457 42%, #1fb6c9 56%,
    #3a7bea 70%, #9b57e6 84%, #e8433a 100%);
  background-size: 400% 100%;
  background-position: 0% 50%;
  border-color: rgba(40, 40, 60, 0.7);
  color: #ffffff;
  text-shadow: 0 1px 2px rgba(20, 20, 40, 0.75), 0 0 6px rgba(20, 20, 40, 0.35);
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, 0.45),
    0 0 0 4px rgba(232, 67, 58, 0.45),
    0 18px 30px rgba(40, 40, 60, 0.3);
  animation: advanceCascade 1.8s linear infinite, advanceHalo 1.8s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .breakthrough-strip__advance.ready {
    animation: none;
    background-position: 30% 50%;
  }
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .breakthrough-strip__advance.ready:not(:disabled):hover,
.breakthrough-strip__advance.ready:not(:disabled):focus-visible {
  /* Hover keeps the cascade running; just lift it and widen the halo. */
  border-color: rgba(40, 40, 60, 0.9);
  color: #ffffff;
  transform: translateY(-1px) scale(1.02);
}

.topbar-button--blink-warn {
  animation: deactivateWarnBlink 0.72s steps(2, end) infinite;
}

@keyframes deactivateWarnBlink {
  0% {
    background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
    border-color: rgba(16, 74, 153, 0.92);
    box-shadow: inset 0 2px 0 rgba(184, 239, 255, 0.9), 0 8px 0 rgba(22, 92, 188, 0.62), 0 0 0 0 rgba(220, 80, 70, 0);
  }
  50% {
    background: linear-gradient(180deg, rgba(255, 138, 122, 1), rgba(220, 70, 60, 1));
    border-color: rgba(150, 30, 22, 0.96);
    color: rgba(255, 246, 240, 0.98);
    box-shadow: inset 0 2px 0 rgba(255, 220, 210, 0.95), 0 8px 0 rgba(150, 30, 22, 0.62), 0 0 0 6px rgba(220, 80, 70, 0.42), 0 0 22px rgba(220, 80, 70, 0.5);
  }
  100% {
    background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
    border-color: rgba(16, 74, 153, 0.92);
    box-shadow: inset 0 2px 0 rgba(184, 239, 255, 0.9), 0 8px 0 rgba(22, 92, 188, 0.62), 0 0 0 0 rgba(220, 80, 70, 0);
  }
}

.tooltip {
  position: fixed;
  z-index: 1000;
  max-width: 260px;
  padding: 7px 10px;
  background: rgba(22, 21, 19, 0.88);
  color: #f4f0e8;
  font-size: 12px;
  font-family: "Avenir Next Condensed", "Gill Sans", sans-serif;
  letter-spacing: 0.03em;
  line-height: 1.4;
  white-space: pre-line;
  pointer-events: none;
  opacity: 0;
  transition: opacity 100ms ease;
}

.tooltip.visible {
  opacity: 1;
}

@keyframes unlockPulse {
  0% {
    box-shadow:
      inset 0 2px 0 rgba(255, 251, 222, 1),
      0 0 0 0 rgba(132, 244, 187, 0.2),
      0 14px 24px rgba(39, 132, 89, 0.2);
    filter: saturate(1) brightness(1);
  }
  50% {
    box-shadow:
      inset 0 2px 0 rgba(255, 252, 230, 1),
      0 0 0 7px rgba(161, 247, 214, 0.34),
      0 0 18px rgba(106, 232, 255, 0.2),
      0 20px 32px rgba(35, 126, 132, 0.28);
    filter: saturate(1.08) brightness(1.05);
  }
  100% {
    box-shadow:
      inset 0 2px 0 rgba(255, 251, 222, 1),
      0 0 0 0 rgba(132, 244, 187, 0.2),
      0 14px 24px rgba(39, 132, 89, 0.2);
    filter: saturate(1) brightness(1);
  }
}

@keyframes advanceCascade {
  0%   { background-position: 0% 50%; }
  100% { background-position: -300% 50%; }
}

/* The halo runs through the same seven hues as the sweep, one cycle per
   period, so the ring always agrees with the colour arriving at the button's
   left edge. Pulses wider on every hue change so it reads at booth distance. */
@keyframes advanceHalo {
  0%    { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 4px rgba(232, 67, 58, 0.5),   0 0 22px rgba(232, 67, 58, 0.55),  0 18px 30px rgba(40, 40, 60, 0.3); }
  14%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 9px rgba(240, 138, 36, 0.4),  0 0 26px rgba(240, 138, 36, 0.6),  0 18px 30px rgba(40, 40, 60, 0.3); }
  28%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 4px rgba(217, 184, 0, 0.5),   0 0 22px rgba(217, 184, 0, 0.55),  0 18px 30px rgba(40, 40, 60, 0.3); }
  42%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 9px rgba(47, 180, 87, 0.4),   0 0 26px rgba(47, 180, 87, 0.6),   0 18px 30px rgba(40, 40, 60, 0.3); }
  56%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 4px rgba(31, 182, 201, 0.5),  0 0 22px rgba(31, 182, 201, 0.55), 0 18px 30px rgba(40, 40, 60, 0.3); }
  70%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 9px rgba(58, 123, 234, 0.4),  0 0 26px rgba(58, 123, 234, 0.6),  0 18px 30px rgba(40, 40, 60, 0.3); }
  84%   { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 4px rgba(155, 87, 230, 0.5),  0 0 22px rgba(155, 87, 230, 0.55), 0 18px 30px rgba(40, 40, 60, 0.3); }
  100%  { box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.45), 0 0 0 4px rgba(232, 67, 58, 0.5),   0 0 22px rgba(232, 67, 58, 0.55),  0 18px 30px rgba(40, 40, 60, 0.3); }
}

.insight-cost {
  color: #5e9cff;
  opacity: 0.85;
}

.insight-display {
  color: rgba(255, 244, 212, 0.96);
  opacity: 1;
  text-shadow: 0 1px 0 rgba(101, 61, 17, 0.32);
}

.era-gate {
  opacity: 0.62;
  font-size: 12px;
}


.tree-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}

.tree-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid rgba(22, 21, 19, 0.12);
  background: rgba(255, 255, 255, 0.58);
}

.tree-legend-dot {
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: currentColor;
  flex: 0 0 auto;
}

.tree-legend-item--state-unlocked {
  color: #244e2f;
  background: rgba(223, 241, 226, 0.9);
}

.tree-legend-item--state-affordable {
  color: #2f67a4;
  background: rgba(224, 237, 255, 0.9);
}

.tree-legend-item--state-locked {
  color: #9a6c17;
  background: rgba(255, 241, 214, 0.9);
}

.tree-legend-item--state-prereq {
  color: #6f4438;
  background: rgba(247, 228, 221, 0.9);
}

.tree-route-banner {
  margin-bottom: 18px;
  padding: 12px 14px;
  border-radius: 20px;
  border: 1px solid rgba(52, 84, 56, 0.16);
  background: linear-gradient(180deg, rgba(241, 246, 240, 0.98), rgba(228, 236, 226, 0.95));
}

.tree-route-banner__label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(52, 84, 56, 0.76);
}

.tree-route-banner__chips {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.tree-route-chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(52, 84, 56, 0.1);
  border: 1px solid rgba(52, 84, 56, 0.18);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(52, 84, 56, 0.88);
}

.tree-spine {
  display: grid;
  grid-template-columns: repeat(7, minmax(150px, 1fr));
  gap: 14px;
  align-items: start;
}

.tree-spine-cell {
  min-width: 0;
}

.tree-spine-link {
  margin: 10px auto 0;
  height: 2px;
  width: calc(100% - 18px);
  background: rgba(22, 21, 19, 0.12);
  border-radius: 999px;
}

.tree-spine-link--route {
  background: linear-gradient(90deg, rgba(52, 84, 56, 0.34), rgba(52, 84, 56, 0.86));
}

.tree-branches {
  margin-top: 18px;
  display: grid;
  grid-template-columns: repeat(7, minmax(150px, 1fr));
  gap: 14px;
}

.tree-branch-slot {
  min-width: 0;
  justify-self: start;
}

.tree-branch-slot--random {
  grid-column: 2;
}

.tree-branch-slot--deep {
  grid-column: 5;
}

.tree-branch-link {
  width: 2px;
  height: 18px;
  margin: 0 auto 10px;
  background: rgba(22, 21, 19, 0.12);
  border-radius: 999px;
}

.tree-branch-link--route {
  background: linear-gradient(180deg, rgba(52, 84, 56, 0.34), rgba(52, 84, 56, 0.86));
}

.tree-column {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: fit-content;
  max-width: 100%;
}

.tree-column--branch .tree-model-node {
  box-shadow: 0 10px 22px rgba(22, 21, 19, 0.06);
}

.tree-column--branch .tree-model-node {
  width: 180px;
  min-width: 180px;
  min-height: 96px;
}

.tree-model-node {
  border: 1px solid rgba(22, 21, 19, 0.18);
  border-radius: 20px;
  padding: 13px 14px;
  font-size: 12px;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  width: 100%;
  min-height: 110px;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.44);
  box-shadow: 0 8px 18px rgba(22, 21, 19, 0.04);
  color: rgba(22, 21, 19, 0.9);
}

.tree-card--dim {
  opacity: 0.42;
  filter: saturate(0.7);
}

.tree-model-node[data-model-unlock],
.tree-upgrade-node[data-upgrade] {
  cursor: var(--cursor-pointer);
  transition: transform 140ms ease, background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-model-node[data-model-unlock]:hover,
.tree-model-node[data-model-unlock]:focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-upgrade-node[data-upgrade]:hover,
.tree-upgrade-node[data-upgrade]:focus-visible {
  transform: translateY(-2px) scale(1.01);
  outline: none;
}

.tree-model-node.state-unlocked {
  border-color: rgba(36, 78, 47, 0.42);
  background: linear-gradient(180deg, rgba(223, 241, 226, 0.98), rgba(205, 231, 211, 0.98));
  color: #244e2f;
  box-shadow: 0 14px 28px rgba(36, 78, 47, 0.14);
}

.tree-model-node.state-affordable {
  border-color: rgba(44, 125, 54, 0.88);
  color: #173f1d;
  background: linear-gradient(180deg, rgba(226, 255, 190, 1), rgba(143, 222, 95, 0.99));
  box-shadow: inset 0 2px 0 rgba(248, 255, 232, 0.9), 0 16px 28px rgba(67, 155, 66, 0.28);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-model-node.state-affordable:hover,
.tree-model-node.state-affordable:focus-visible {
  background: linear-gradient(180deg, rgba(244, 255, 230, 1), rgba(206, 242, 168, 1));
  border-color: rgba(59, 111, 35, 0.74);
  box-shadow: 0 18px 30px rgba(81, 146, 52, 0.24);
}

.tree-model-node.state-prereq {
  border-color: rgba(138, 76, 60, 0.72);
  background: linear-gradient(180deg, rgba(255, 218, 202, 0.99), rgba(223, 162, 141, 0.99));
  color: #6d2f1e;
  box-shadow: inset 0 1px 0 rgba(255, 239, 233, 0.82), 0 14px 24px rgba(128, 75, 54, 0.18);
}

.tree-model-node.state-locked {
  border-color: rgba(150, 113, 26, 0.54);
  background: linear-gradient(180deg, rgba(255, 249, 233, 0.98), rgba(241, 222, 169, 0.98));
  color: #8d5b00;
}

.tree-node-name {
  font-size: 12px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
}

.tree-card__eyebrow {
  margin-bottom: 8px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(52, 84, 56, 0.64);
}

.tree-node-status {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.tree-node-status__pill {
  display: inline-flex;
  align-self: flex-start;
  padding: 4px 8px;
  border-radius: 999px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.tree-node-status__detail {
  font-size: 10px;
  line-height: 1.3;
  letter-spacing: 0.02em;
  text-transform: none;
  opacity: 0.84;
}

.tree-node-status--state-unlocked .tree-node-status__pill {
  background: rgba(36, 78, 47, 0.14);
  color: #244e2f;
}

.tree-node-status--state-affordable .tree-node-status__pill {
  background: rgba(49, 134, 61, 0.18);
  color: #173f1d;
}

.tree-node-status--state-locked .tree-node-status__pill {
  background: rgba(182, 126, 14, 0.18);
  color: #8d5b00;
}

.tree-node-status--state-prereq .tree-node-status__pill {
  background: rgba(138, 76, 60, 0.16);
  color: #6d2f1e;
}

.tree-upgrades {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 6px;
  width: 100%;
  align-items: flex-start;
}

.tree-upgrade-node {
  border: 1px solid rgba(22, 21, 19, 0.14);
  border-radius: 14px;
  padding: 7px 9px;
  width: 132px;
  min-width: 132px;
  max-width: 132px;
  font-size: 11px;
  background: rgba(255, 255, 255, 0.34);
  color: rgba(22, 21, 19, 0.9);
  box-sizing: border-box;
}

.tree-upgrade-summary {
  padding: 6px 2px 0;
  flex: 0 0 100%;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(22, 21, 19, 0.5);
}

.tree-upgrade-node.state-unlocked {
  border-color: rgba(36, 78, 47, 0.42);
  background: linear-gradient(180deg, rgba(223, 241, 226, 0.96), rgba(205, 231, 211, 0.96));
  color: #244e2f;
  box-shadow: 0 12px 24px rgba(36, 78, 47, 0.12);
}

.tree-upgrade-node.state-affordable {
  border-color: rgba(44, 125, 54, 0.88);
  color: #173f1d;
  background: linear-gradient(180deg, rgba(226, 255, 190, 1), rgba(143, 222, 95, 0.99));
  box-shadow: inset 0 2px 0 rgba(248, 255, 232, 0.88), 0 14px 26px rgba(67, 155, 66, 0.24);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-upgrade-node.state-affordable:hover,
.tree-upgrade-node.state-affordable:focus-visible {
  background: linear-gradient(180deg, rgba(244, 255, 230, 1), rgba(206, 242, 168, 1));
  border-color: rgba(59, 111, 35, 0.74);
  box-shadow: 0 18px 30px rgba(81, 146, 52, 0.24);
}

.tree-upgrade-node.state-prereq {
  border-color: rgba(138, 76, 60, 0.72);
  background: linear-gradient(180deg, rgba(255, 218, 202, 0.98), rgba(223, 162, 141, 0.98));
  color: #6d2f1e;
}

.tree-upgrade-node.state-locked {
  border-color: rgba(150, 113, 26, 0.54);
  background: linear-gradient(180deg, rgba(255, 249, 233, 0.96), rgba(241, 222, 169, 0.97));
  color: #8d5b00;
}

.tree-upgrade-node.state-unlocked .tree-node-status__pill {
  background: rgba(36, 78, 47, 0.14);
  color: #244e2f;
}

.tree-upgrade-node.state-prereq .tree-node-status__pill {
  background: rgba(138, 76, 60, 0.16);
  color: #6d2f1e;
}

/* Fortnite-style rarity coloring on the affordable state of unlocks. */
.upgrades-dock .topbar-button.ready.unlock-tier--gold,
.tree-model-node.state-affordable.unlock-tier--gold,
.tree-upgrade-node.state-affordable.unlock-tier--gold {
  background: linear-gradient(180deg, rgba(255, 232, 142, 1), rgba(255, 184, 42, 1) 60%, rgba(220, 140, 12, 0.99));
  border-color: rgba(155, 95, 12, 0.85);
  color: #4a2a04;
  text-shadow: 0 1px 0 rgba(255, 247, 210, 0.7);
  box-shadow:
    inset 0 2px 0 rgba(255, 250, 210, 0.95),
    0 0 0 4px rgba(255, 200, 80, 0.45),
    0 14px 26px rgba(180, 110, 20, 0.32);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .upgrades-dock .topbar-button.ready.unlock-tier--gold:not(:disabled):hover,
.upgrades-dock .topbar-button.ready.unlock-tier--gold:not(:disabled):focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-model-node.state-affordable.unlock-tier--gold:hover,
.tree-model-node.state-affordable.unlock-tier--gold:focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-upgrade-node.state-affordable.unlock-tier--gold:hover,
.tree-upgrade-node.state-affordable.unlock-tier--gold:focus-visible {
  background: linear-gradient(180deg, rgba(255, 240, 168, 1), rgba(255, 196, 78, 1) 60%, rgba(232, 152, 24, 1));
  border-color: rgba(155, 95, 12, 1);
  box-shadow:
    inset 0 2px 0 rgba(255, 252, 224, 1),
    0 0 0 6px rgba(255, 210, 110, 0.55),
    0 18px 32px rgba(180, 110, 20, 0.4);
}

.upgrades-dock .topbar-button.ready.unlock-tier--purple,
.tree-model-node.state-affordable.unlock-tier--purple,
.tree-upgrade-node.state-affordable.unlock-tier--purple {
  background: linear-gradient(180deg, rgba(228, 196, 252, 1), rgba(178, 108, 232, 1) 55%, rgba(124, 60, 200, 0.99));
  border-color: rgba(96, 40, 160, 0.88);
  color: #2d0d57;
  text-shadow: 0 1px 0 rgba(245, 230, 255, 0.6);
  box-shadow:
    inset 0 2px 0 rgba(245, 230, 255, 0.92),
    0 0 0 4px rgba(190, 130, 240, 0.45),
    0 14px 26px rgba(110, 50, 175, 0.32);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .upgrades-dock .topbar-button.ready.unlock-tier--purple:not(:disabled):hover,
.upgrades-dock .topbar-button.ready.unlock-tier--purple:not(:disabled):focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-model-node.state-affordable.unlock-tier--purple:hover,
.tree-model-node.state-affordable.unlock-tier--purple:focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-upgrade-node.state-affordable.unlock-tier--purple:hover,
.tree-upgrade-node.state-affordable.unlock-tier--purple:focus-visible {
  background: linear-gradient(180deg, rgba(238, 212, 255, 1), rgba(192, 130, 240, 1) 55%, rgba(140, 76, 218, 1));
  border-color: rgba(96, 40, 160, 1);
  box-shadow:
    inset 0 2px 0 rgba(250, 238, 255, 1),
    0 0 0 6px rgba(200, 145, 245, 0.55),
    0 18px 32px rgba(110, 50, 175, 0.4);
}

/* Hue tuned to match the model-roster Lv-up button (which the player
   already reads as "this is the bright blue that means I can act now").
   Same body gradient + border; the inset highlight + outer halo are
   kept so the unlock buttons still feel chunkier than the Lv pill. */
.upgrades-dock .topbar-button.ready.unlock-tier--blue,
.tree-model-node.state-affordable.unlock-tier--blue,
.tree-upgrade-node.state-affordable.unlock-tier--blue {
  background: linear-gradient(180deg, rgba(78, 187, 255, 0.98), rgba(38, 119, 231, 0.98));
  border-color: rgba(16, 74, 153, 0.92);
  color: #051e54;
  text-shadow: 0 1px 0 rgba(220, 240, 255, 0.55);
  box-shadow:
    inset 0 2px 0 rgba(220, 240, 255, 0.94),
    0 0 0 4px rgba(120, 180, 255, 0.45),
    0 14px 26px rgba(22, 92, 188, 0.4);
}

:where(body:not(.mobile-tuned):not(.landscape-phone)) .upgrades-dock .topbar-button.ready.unlock-tier--blue:not(:disabled):hover,
.upgrades-dock .topbar-button.ready.unlock-tier--blue:not(:disabled):focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-model-node.state-affordable.unlock-tier--blue:hover,
.tree-model-node.state-affordable.unlock-tier--blue:focus-visible,
:where(body:not(.mobile-tuned):not(.landscape-phone)) .tree-upgrade-node.state-affordable.unlock-tier--blue:hover,
.tree-upgrade-node.state-affordable.unlock-tier--blue:focus-visible {
  background: linear-gradient(180deg, rgba(97, 201, 255, 1), rgba(47, 128, 240, 1));
  border-color: rgba(16, 74, 153, 1);
  box-shadow:
    inset 0 2px 0 rgba(232, 246, 255, 1),
    0 0 0 6px rgba(140, 192, 255, 0.55),
    0 18px 32px rgba(22, 92, 188, 0.45);
}

@media (max-width: 1180px) {
  .tree-spine,
  .tree-branches {
    grid-template-columns: repeat(3, minmax(160px, 1fr));
  }

  .tree-branch-slot--random {
    grid-column: 1;
  }

  .tree-branch-slot--deep {
    grid-column: 2;
  }
}

/* Short-viewport compact mode for the chapter dock (1024×600 Chromebooks,
   1366×768 with browser chrome) AND mobile portrait (phones at ≤ 700 px wide).
   Shrinks fonts, paddings, and progress bar so the whole dock fits in ~110 px
   instead of ~180 px, matching the canvas reservation in layout.js
   (CHAPTER_DOCK_RESERVED_HEIGHT_MOBILE). On mobile portrait the dock would
   otherwise wrap its title row vertically (narrow column = each metric on its
   own line, plus the wrapped headline) and bloat to 200+ px. Keeps the
   normal desktop sizing untouched at ≥ 750 px tall AND ≥ 701 px wide. Placed
   at the bottom of the file so it wins the cascade against the base
   definitions higher up. */
@media (max-height: 749px), (max-width: 700px) {
  .breakthrough-strip {
    padding: 6px 12px 8px;
    border-width: 2px;
    border-radius: 20px;
  }
  .breakthrough-strip__header {
    column-gap: 6px;
  }
  .breakthrough-strip__headline {
    padding: 5px 10px 6px;
    gap: 8px;
    border-radius: 12px;
  }
  .breakthrough-strip__chapter,
  .breakthrough-strip__title {
    font-size: 14px;
  }
  .breakthrough-strip__metrics {
    font-size: 13px;
    margin-left: 8px;
    padding-left: 8px;
    gap: 6px;
  }
  .breakthrough-strip__metric-icon {
    width: 14px;
    height: 14px;
  }
  .breakthrough-strip__progress-row {
    margin-top: 4px;
    gap: 5px;
  }
  .breakthrough-strip__progress-stack {
    gap: 4px;
  }
  .breakthrough-strip__progress-card {
    min-height: 20px;
    gap: 2px;
  }
  .breakthrough-strip__progress {
    height: 10px;
  }
  .breakthrough-strip__progress--gate {
    height: 12px;
  }
  .breakthrough-strip__progress-label {
    font-size: 10px;
  }
  .breakthrough-strip__progress-value {
    font-size: 11px;
  }
  .breakthrough-strip__timer-big {
    min-height: 20px;
    padding: 3px 0;
    gap: 6px;
  }
  .breakthrough-strip__timer-big-value {
    font-size: 26px;
  }
  .breakthrough-strip__timer-big-unit {
    font-size: 13px;
  }
  .breakthrough-strip__advance,
  .breakthrough-strip__select {
    min-height: 28px;
    padding: 4px 12px;
    font-size: 13px;
  }
  .breakthrough-strip__utility .utility-dock__button--icon {
    width: 26px;
    height: 26px;
    min-height: 26px;
  }
  .breakthrough-strip__utility .utility-dock__button--icon .utility-dock__icon-svg {
    width: 14px;
    height: 14px;
  }
}

/* Mobile portrait only — hide the inline points / accuracy chip row that
   otherwise forces the title to overlap the utility buttons at typical phone
   widths (the "1.2 Aut[icons]omation..." pile at 411 px). Points stay visible
   on the cheat panel + workbench; accuracy is already shown on the gate bar.
   Also clamp the title to a single line + ellipsis so long names like
   "Triangle Trap" don't wrap and push the dock taller. */
@media (max-width: 700px) {
  .breakthrough-strip__metrics {
    display: none;
  }
  .breakthrough-strip__title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ── PC Shop overlay ─────────────────────────────────────────────────────
   Dark "rig" theme, deliberately contrasting the green milestone overlays. */
.rig-shop__panel {
  width: min(680px, 100%);
  max-height: 88vh;
  overflow-y: auto;
  padding: 22px 24px 20px;
  border: 2px solid rgba(78, 187, 255, 0.5);
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(26, 30, 40, 0.99), rgba(16, 18, 26, 0.99));
  box-shadow: 0 26px 60px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(120, 160, 220, 0.18);
  text-align: left;
  color: rgba(228, 234, 244, 0.95);
}
.rig-shop__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.rig-shop__panel .milestone-overlay__eyebrow {
  color: rgba(120, 190, 255, 0.95);
}
.rig-shop__balance {
  font: 700 14px ui-monospace, monospace;
  color: #ffd24a;
}
.rig-shop__summary {
  margin: 12px 0 14px;
  padding: 10px 12px;
  border: 1px solid rgba(90, 110, 150, 0.35);
  border-radius: 10px;
  background: rgba(10, 14, 22, 0.6);
}
.rig-shop__stats {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  font: 12px ui-monospace, monospace;
  color: rgba(180, 200, 230, 0.9);
}
.rig-shop__stats b { color: #fff; }
.rig-shop__installed {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.rig-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(40, 119, 231, 0.25);
  border: 1px solid rgba(78, 187, 255, 0.4);
  font: 700 11px ui-monospace, monospace;
}
.rig-chip__x {
  border: none;
  background: none;
  color: rgba(255, 150, 150, 0.9);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  padding: 0 1px;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .rig-chip__x:hover { color: #ff5b5b; }
.rig-shop__tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 14px;
}
.rig-shop__tab {
  flex: 1;
  padding: 10px 8px;
  border: 1px solid rgba(80, 100, 140, 0.35);
  border-radius: 10px;
  background: rgba(20, 24, 34, 0.7);
  color: rgba(180, 195, 220, 0.85);
  font: 700 13px 'Avenir Next Condensed', sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 120ms, color 120ms, border-color 120ms;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .rig-shop__tab:hover { background: rgba(30, 38, 54, 0.9); color: #fff; }
.rig-shop__tab--active {
  background: linear-gradient(180deg, #4ebbff, #2677e7);
  border-color: rgba(78, 187, 255, 0.7);
  color: #fff;
}
.rig-over { color: #ff6a4a; }
.rig-shop__warn {
  margin: 8px 0 2px;
  padding: 7px 10px;
  border: 1px solid rgba(255, 140, 70, 0.5);
  border-radius: 8px;
  background: rgba(70, 30, 14, 0.6);
  color: #ffc08a;
  font-size: 12px;
  line-height: 1.35;
}
.rig-shop__col-title {
  font: 700 11px 'Avenir Next Condensed', sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(150, 175, 210, 0.85);
  margin-bottom: 8px;
}
.rig-shop__list { display: flex; flex-direction: column; gap: 8px; }
.rig-shop__item {
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 9px 10px;
  border: 1px solid rgba(80, 100, 140, 0.3);
  border-radius: 10px;
  background: rgba(20, 24, 34, 0.7);
}
.rig-shop__item.is-current {
  border-color: rgba(90, 220, 160, 0.6);
  background: rgba(20, 50, 40, 0.6);
}
.rig-shop__item.is-disabled { opacity: 0.55; }
.rig-shop__item-main { flex: 1; min-width: 0; }
.rig-shop__item-name {
  font: 700 13px 'Avenir Next Condensed', sans-serif;
  color: #fff;
}
.rig-owned-badge {
  font: 700 10px ui-monospace, monospace;
  color: #4ebbff;
}
.rig-shop__item-stats {
  font: 11px ui-monospace, monospace;
  color: rgba(150, 200, 255, 0.8);
  margin: 2px 0 3px;
}
.rig-shop__item-desc {
  font-size: 11px;
  line-height: 1.35;
  color: rgba(190, 200, 218, 0.72);
}
.rig-shop__buy {
  flex: 0 0 auto;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 78px;
  padding: 8px 10px;
  border: none;
  border-radius: 9px;
  background: linear-gradient(180deg, #4ebbff, #2677e7);
  color: #fff;
  cursor: pointer;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .rig-shop__buy:hover:not(:disabled) { filter: brightness(1.08); }
.rig-shop__buy:disabled {
  background: rgba(70, 80, 100, 0.5);
  color: rgba(220, 228, 240, 0.6);
  cursor: default;
}
.rig-shop__buy-cost { font: 700 13px ui-monospace, monospace; }
.rig-shop__buy-note {
  font: 600 9px 'Avenir Next Condensed', sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.9;
}
.rig-shop__panel .milestone-overlay__button {
  margin-top: 18px;
  width: 100%;
}

/* ── PC interior overlay (the "inside my PC" view) ───────────────────────── */
.rig-shop-overlay { z-index: 112; }
.rig-interior-overlay {
  position: fixed;
  inset: 0;
  z-index: 106;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(6, 8, 12, 0.86);
}
.rig-interior-overlay.hidden { display: none; }
.rig-interior__stage {
  position: relative;
  width: min(1220px, 100%);
  height: min(740px, 100%);
}
#rig-interior-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 16px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.65), inset 0 0 0 1px rgba(120, 150, 200, 0.18);
}
.rig-interior__controls {
  position: absolute;
  top: 14px;
  right: 14px;
  display: flex;
  gap: 8px;
}
.rig-interior__btn {
  padding: 8px 14px;
  border: none;
  border-radius: 9px;
  font: 700 13px 'Avenir Next Condensed', sans-serif;
  letter-spacing: 0.03em;
  cursor: pointer;
  background: linear-gradient(180deg, #4ebbff, #2677e7);
  color: #fff;
}
:where(body:not(.mobile-tuned):not(.landscape-phone)) .rig-interior__btn:hover { filter: brightness(1.08); }
.rig-interior__btn--close {
  background: rgba(40, 46, 60, 0.92);
  border: 1px solid rgba(120, 140, 170, 0.4);
}

/* Narrow-but-tall desktop windows (701–1180 px wide — a laptop browser
   sharing the screen with Slack, which is how Kilian plays; 2026-08-21 "the
   visuals don't align"). The right pane is ~420–560 px here, and the dock's
   one-line headline can't fit "3.10 · Boxed Rules · coins | accuracy · All
   assignments" plus the two utility buttons: the title wrapped into the coin
   total and the open-hint squeezed into a vertical letter stack. Let the
   headline take the full row and wrap onto two lines (title row, metrics
   row), drop the open-hint label to its icon, and keep the utility buttons
   in their own column. Phones (≤ 700) and short screens keep their own
   compact rules above. Placed at the end of the file to win the cascade. */
@media (min-width: 701px) and (max-width: 1180px) and (min-height: 750px) {
  .breakthrough-strip__header {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .breakthrough-strip__headline {
    grid-column: 1;
    justify-self: stretch;
    flex-wrap: wrap;
    row-gap: 6px;
    column-gap: 10px;
    padding: 8px 12px 9px;
  }
  .breakthrough-strip__utility {
    grid-column: 2;
  }
  .breakthrough-strip__title {
    flex: 1 1 auto;
    font-size: 16px;
    text-wrap: pretty;
  }
  .breakthrough-strip__metrics {
    flex: 1 1 100%;
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
    font-size: 15px;
  }
  .breakthrough-strip__open-hint-text {
    display: none;
  }
  .breakthrough-strip__open-hint {
    margin-left: auto;
    padding: 4px;
  }
}

/* Toggleable owned upgrades (knn-template / knn-k-3 / transformer switches):
   the owned pip is an on/off switch. Off = dimmed + grey, effect suspended. */
.model-roster__pip.owned.toggleable { cursor: pointer; }
.model-roster__pip.owned.toggleable.switched-off {
  opacity: 0.45;
  filter: grayscale(0.85);
  box-shadow: none;
}

/* ── Phone tuning, opt-in per version ────────────────────────────────────
   Erik 2026-08-25: better phone layout for the kilian/peter builds only.
   Everything here is double-scoped — `.mobile-tuned` (set from
   applyGameVersion when the version entry carries `mobileTuning`) AND a
   phone-width media query — so the running CS3780 study, which never sets
   the class, renders byte-identically at every width. */
@media (max-width: 700px) {
  /* 1. The legacy console card (model <select> + activate button) lives
     UNDER the model roster and is completely covered by it on desktop. On a
     phone the roster is narrower and the card's flex row wraps to ~156px
     tall, so the select and a squeezed "activate" button poke out and
     collide with the roster — the button renders one letter per line
     ("VATE"). The roster cards already do model selection, so on phones the
     card is pure duplication: hide it. */
  .mobile-tuned .console-dock {
    display: none;
  }

  /* 2. The gate marker's floating label ("85% over last 10 to pass") is
     pinned above the accuracy bar (top: -22px), which on a phone puts it in
     the header row on top of the ? / menu / audio buttons. Drop the pin and
     show the same text inline beside the ACCURACY kicker instead, where
     there is free width. */
  .mobile-tuned .breakthrough-strip__progress-threshold-label {
    display: none;
  }
  .mobile-tuned .breakthrough-strip__progress-inline-threshold {
    display: inline;
    margin-left: auto;
    padding-left: 10px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: rgba(22, 41, 60, 0.82);
    white-space: nowrap;
  }
  .mobile-tuned .breakthrough-strip__progress-label {
    display: flex;
    align-items: baseline;
    width: 100%;
  }
  /* The marker's pointer triangle sits above the bar and pokes into the new
     inline text row; the vertical pin line alone still shows where the gate
     falls, so drop the triangle at phone widths. */
  .mobile-tuned .breakthrough-strip__progress-threshold::before {
    display: none;
  }
}




/* ── Plan E: the coin balance in the strip's top-right ────────────────────
   The inline .breakthrough-strip__metrics row is display:none under 700px (it
   made the title overlap the utility icons), which left a phone with no coin
   balance anywhere. This chip puts it back in the top-right group — a sibling
   of the utility buttons in the header grid's third column, so it is placed by
   normal flow and cannot collide with them at any width. Rendered only when
   the build is tuned AND the layout is a phone (ui/chapter-dock.js), and
   scoped here the same way, so untuned builds never see it. */
@media (max-width: 700px) {
  .mobile-tuned .breakthrough-strip__coin {
    /* The header is a `1fr auto 1fr` grid whose FIRST column is empty (the
       title sits centred in column 2, the utility icons end-aligned in
       column 3). The chip goes in that empty column, start-aligned — top
       left, and placed by the grid, so it can never collide with either. */
    grid-column: 1;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 9px;
    border-radius: 999px;
    background: rgba(255, 248, 219, 0.78);
    border: 1px solid rgba(131, 85, 26, 0.34);
    font-family: "Trebuchet MS", "Avenir Next Condensed", "Gill Sans", sans-serif;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.02em;
    color: rgba(120, 76, 18, 0.95);
    white-space: nowrap;
  }
  .mobile-tuned .breakthrough-strip__coin .breakthrough-strip__metric-icon {
    width: 15px;
    height: 15px;
  }
}

/* ── Plan E: the model index-tab row ──────────────────────────────────────
   One tab per model along the bottom edge; the tab IS the model. Phone builds
   only, and double-scoped exactly like Erik's 2026-08-25 phone tuning:
   `.mobile-tuned` (set from applyGameVersion when the version entry carries
   `mobileTuning`) AND a phone-width media query. A build that sets neither —
   the CS3780 study, karthik — renders byte-identically at every width.

   Hidden by default everywhere, including on phones in untuned builds. */
.model-tabs { display: none; }

@media (max-width: 700px) {
  .mobile-tuned .model-tabs.is-on {
    position: fixed;
    /* 8px, not 0: the SCROLLPORT is the card's box. The row scrolls, so with
       a full-bleed scrollport a scrolled tab painted in the 8px strip either
       side of the card — a tab sticking out past the edge of the card it is
       supposed to be the head of. Inset, the row is clipped to exactly the
       card's width and a tab can never leave it. This also replaces the end
       margins that used to buy the same inset (see :first-child below). */
    left: 8px;
    right: 8px;
    /* The side-desk lane owns the bottom edge; the tabs sit on top of it. */
    bottom: var(--pe-desk, 0px);
    /* = MOBILE_TAB_STRIP_H in layout.js. 88 -> 78: the row, the chapter dock
       and the playfield frame are all competing for the same column of
       pixels, and at 390x844 the frame's outer edge was landing 1px off the
       dock's bottom shoulder. Ten pixels off the strip is ten pixels of air
       back, and the tab still carries a 38px portrait and its name. */
    height: 78px;
    z-index: 22;                  /* above .model-roster (21) */
    display: flex;
    align-items: stretch;
    gap: 8px;
    /* NO padding, and no end margins either. A horizontally scrolling flex
       container does not reliably include its trailing padding in scrollWidth
       (Safari and Chrome disagree), so padding would let the row scroll 8px
       too far and hang the last tab over the card's right edge. The inset is
       the ROW'S OWN left/right above, which is in neither scrollWidth nor
       clientWidth and so cannot desync from anything. */
    padding: 0;
    overflow-x: auto;
    overflow-y: hidden;
    /* No browser panning at all: the row's sideways scroll is driven by the
       same handler as the card pull (app.js, rowScrollBegin), so one gesture
       can only ever do one of the two. It used to be pan-x — sideways was the
       browser's, up-and-down ours — and a diagonal drag did both at once. */
    touch-action: none;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* No strip: the icons ARE the row. The card they belong to carries the
       surface; a bar behind them read as a second, competing panel. */
    background: none;
    border: 0;
    box-shadow: none;
    /* The icons ride the card's top edge — one variable drives both, so they
       cannot drift apart — including through a model switch, where the same
       variable is tweened to glide the pair to the new card's height. */
    transform: translateY(calc(-1 * var(--pe-pull, 0px)));
    transition: transform 180ms ease;
    will-change: transform;
    /* The ROW is not a target — only the tabs in it are. It has no background
       (the tabs are the row), but an element with no background still eats
       taps, and once the card is out this invisible 88px band rides up over
       the playfield and swallowed everything aimed through it, including the
       tap meant to dismiss the card. Events on the tabs still bubble to the
       row, so the drag handler bound here is unaffected. */
    pointer-events: none;
  }
  .mobile-tuned .model-tabs.is-on > .model-tab { pointer-events: auto; }
  /* ...except when the row actually OVERFLOWS, which is when it stops being a
     strip of buttons and becomes a scroller. iOS reaches an overflow scroller
     through the same hit test it uses for taps, so a scroller that takes no
     pointer events gets no pan gesture and the tabs simply cannot be swiped —
     which is what a phone showed. A row that overflows is wall-to-wall tabs by
     definition, so there is no transparent band left for `auto` to make
     greedy: all it costs is the 8px gaps between tabs, where a tap now does
     nothing instead of dismissing the card. The class is set by
     syncCardJoinCorners, which already runs on every scroll, render and
     width change. */
  .mobile-tuned .model-tabs.is-on.is-scrollable { pointer-events: auto; }
  /* No end margins any more: the row itself is inset to the card's box, so the
     first tab already starts on the card's left edge and the last one ends on
     its right edge. Keeping both would inset twice. */
  .mobile-tuned .model-tabs.is-on::-webkit-scrollbar { display: none; }
  /* Scroll affordance (syncModelTabPeek in app.js): when the row overflows,
     every tab takes the width that leaves part of the next tab showing, and
     the row snaps a whole tab at a time, so a resting row always shows a
     cut-off tab on the side there is more of. Snapping is off while a tab is
     being dragged to a new place — the row auto-scrolls under the finger then,
     and a mandatory snap would fight it. */
  .mobile-tuned .model-tabs.is-on.is-peek {
    scroll-snap-type: x mandatory;
  }
  .mobile-tuned .model-tabs.is-on.is-peek > .model-tab {
    flex: 0 0 var(--pe-tab-w, 108px);
    min-width: 0;
    scroll-snap-align: start;
  }
  .mobile-tuned .model-tabs.is-on.is-peek > .model-tab .model-tab__name {
    max-width: calc(var(--pe-tab-w, 108px) - 20px);
  }
  .mobile-tuned .model-tabs.is-on.model-tabs--reordering,
  .mobile-tuned .model-tabs.is-on.model-tabs--finger-scroll {
    scroll-snap-type: none;
  }
  /* A row that FITS at the shrink floor fits exactly: the tabs give up the few
     pixels a long name asks for instead of the row scrolling by 3px and
     leaving the last tab a sliver short — a cut edge that promises more tabs
     that are not there. Same 96px floor as syncModelTabPeek. */
  .mobile-tuned .model-tabs.is-on:not(.is-peek) > .model-tab {
    flex-shrink: 1;
    min-width: 96px;
  }

  /* Each tab is its own card edge — a separate object with its own border and
     shadow, not a cell in a shared bar. ~3x the previous footprint: the tabs
     are the primary model control on a phone, and a 44px portrait is what
     makes the model recognisable at a glance. */
  .mobile-tuned .model-tab {
    flex: 0 0 auto;
    min-width: 108px;
    min-height: 68px;
    margin-top: 8px;              /* an unpulled card sits lower in the deck */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 7px 10px 5px;
    appearance: none;
    /* Byte-matched to the card's frame — see .model-roster__list. */
    border: 3px solid rgba(118, 74, 18, 0.62);
    border-bottom: 0;
    border-radius: 14px 14px 0 0;
    background: linear-gradient(180deg, #e7d3ab, #cfb27f);
    color: rgba(82, 50, 12, 0.78);
    font-family: inherit;
    cursor: var(--cursor-pointer);
    /* pan-x, NOT manipulation. `manipulation` is "pan-x pan-y pinch-zoom", so
       the browser claims a VERTICAL drag the moment it starts one and fires
       pointercancel at us — and the vertical drag on a tab is the gesture that
       pulls its card out of the deck, which therefore died on its first pixel
       (measured: pointercancel on move 1 of 18, the card settling straight
       back to 0). pan-x hands the browser exactly the axis that is its job —
       scrolling the row sideways — and keeps the other one for us. */
    /* none, not pan-x: see .model-tabs.is-on — the sideways scroll is the
       app's own now, so that it and the upward pull can lock each other out.
       (manipulation would still be wrong for the reason above.) */
    touch-action: none;
    /* NO drop shadow. The tabs sit against the sky, and an upward shadow on
       each one smudged the gaps between them and read as a dark bar behind
       the row — the row is supposed to be nothing but the tabs. The card the
       tabs belong to carries the only shadow in this group; the tab's own
       relief comes from its border and the inset highlight below. */
    box-shadow: none;
  }
  /* The active tab is taller and lighter — the card already half out. */
  .mobile-tuned .model-tab--active {
    margin-top: 0;
    padding-top: 11px;
    /* Ends on EXACTLY the colour the card's surface starts on (#fdf6dd), so
       where the tab meets the card there is no step. It used to end on
       #efe1ae — the card's BOTTOM colour — which drew a visible ledge across
       the join and made them read as two stacked objects. */
    background: linear-gradient(180deg, #fffdf3, #fdf6dd);
    color: rgba(82, 50, 12, 0.96);
    /* Inset highlight only — same reason as .model-tab above. */
    box-shadow: inset 0 1px 0 rgba(255, 248, 215, 0.95);
  }
  /* Locked but ON OFFER: dimmed, yet clearly still a card you can pull out —
     that is how a model is read about and bought on a phone. INERT (cannot
     act on this batch kind at all) stays fully faded out. */
  /* Dim the tab's CONTENTS, not the tab. `opacity` on the button itself made
     its background translucent too, so the playfield, the accuracy bar and the
     chapter dock showed straight through a locked tab whenever the card was
     pulled up over them — which reads as a rendering fault rather than as
     "dimmed". The box stays opaque and goes a shade duller instead. */
  .mobile-tuned .model-tab--locked {
    background: linear-gradient(180deg, #ddcaa3, #c4a877);
  }
  .mobile-tuned .model-tab--locked > * { opacity: 0.68; }
  .mobile-tuned .model-tab--inert {
    background: linear-gradient(180deg, #d7cdb6, #bdb294);
  }
  .mobile-tuned .model-tab--inert > * { opacity: 0.45; }

  /* "NEW" flag: a model that came on offer since the player last looked. It
     sits on the tab because the row is the only place a new model announces
     itself on a phone. Cleared by pulling that model's page out and putting it
     back — see clearPlanETabNew in app.js. position:absolute so it costs the
     tab no layout and cannot shove the portrait around. */
  .mobile-tuned .model-tab { position: relative; }
  /* READY — this model is not owned yet but IS affordable right now.
     Deliberately the same blue and the same word as the desktop roster's
     .model-roster__badge-ready: the phone should not invent a second
     vocabulary for a state the game already names. Sits where NEW sits, and
     only one of the two is ever rendered (see tabFlagHtml in app.js). */
  .mobile-tuned .model-tab__ready {
    position: absolute;
    top: 3px;
    right: 5px;
    padding: 2px 5px;
    border-radius: 6px;
    background: linear-gradient(180deg, #4a90f0, #2263c9);
    border: 1px solid rgba(14, 44, 96, 0.75);
    color: #f4faff;
    font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 0.08em;
    line-height: 1;
    text-transform: uppercase;
    pointer-events: none;
  }
  /* REARRANGING THE ROW (hold a tab, then drag it; see the reorder gesture in
     app.js). A long press on iOS otherwise brings up the callout and text
     selection, and either one ends the touch, so the tabs refuse both. */
  .mobile-tuned .model-tab {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
  /* The neighbours step aside to show where the tab will land. Eased, because
     they move in whole-tab jumps as the lifted one crosses their midpoints. */
  .mobile-tuned .model-tabs--reordering .model-tab {
    transition: transform 160ms ease;
  }
  /* The lifted tab follows the finger exactly, so it is the one thing in the
     row with NO transition; it sits above the others, a little larger, with a
     shadow, the way a card picked up off a table sits above the rest. */
  .mobile-tuned .model-tabs--reordering .model-tab.model-tab--lifted {
    transition: none;
    transform: translateX(var(--lift-x, 0px)) scale(1.05);
    z-index: 3;
    box-shadow: 0 10px 22px rgba(60, 38, 8, 0.32), 0 2px 4px rgba(60, 38, 8, 0.25);
    opacity: 1;
  }
  .mobile-tuned .model-tabs--reordering .model-tab.model-tab--lifted > * { opacity: 1; }
  /* UPGRADE — you already own this model and something on its card is
     affordable right now. The same green as NEW: both flags mean "there is
     something here for you", and one colour for that keeps the row reading as
     one signal. An ARROW rather than a word — "↑ cost" is already how the
     card's own level button spells upgrade. NEW and UPGRADE are mutually
     exclusive by construction (unowned vs owned), so one slot still serves;
     see tabFlagHtml in app.js. */
  .mobile-tuned .model-tab__upgrade {
    position: absolute;
    top: 3px;
    right: 5px;
    min-width: 15px;
    padding: 2px 4px;
    border-radius: 6px;
    background: linear-gradient(180deg, #5ab063, #2f7f3c);
    border: 1px solid rgba(20, 60, 25, 0.75);
    box-shadow: 0 1px 2px rgba(20, 45, 20, 0.35);
    color: #f6fff2;
    font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
    text-align: center;
    pointer-events: none;
  }
  /* A locked tab is dimmed to 0.68 so it reads as "not yours". A READY one is
     still not yours, but it is one tap from being yours, so it sits between
     the two: clearly dimmer than an owned tab, clearly livelier than one the
     player cannot act on. */
  .mobile-tuned .model-tab--ready > * { opacity: 0.92; }
  .mobile-tuned .model-tab--ready {
    border-color: rgba(34, 99, 201, 0.62);
  }
  .mobile-tuned .model-tab__new {
    position: absolute;
    top: 3px;
    right: 5px;
    padding: 2px 5px;
    border-radius: 6px;
    background: linear-gradient(180deg, #5ab063, #2f7f3c);
    border: 1px solid rgba(20, 60, 25, 0.75);
    color: #f6fff2;
    font-family: "Avenir Next Condensed", "Trebuchet MS", sans-serif;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 0.08em;
    line-height: 1;
    text-transform: uppercase;
    box-shadow: 0 1px 2px rgba(20, 45, 20, 0.35);
    pointer-events: none;
  }
  /* A new tab is not dimmed even while locked — the whole point is that it
     catches the eye. Targets the contents, to match the rule above. */
  .mobile-tuned .model-tab--new.model-tab--locked > * { opacity: 1; }

  .mobile-tuned .model-tab__portrait {
    width: 38px;
    height: 38px;
    display: block;
    border-radius: 3px;
    overflow: hidden;
  }
  .mobile-tuned .model-tab__portrait svg { width: 100%; height: 100%; display: block; }
  .mobile-tuned .model-tab__name {
    max-width: 104px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ── Plan E: the roster as a bottom sheet ─────────────────────────────────
   Pulled up from the tab strip, anchored above it. An OVERLAY — opening it
   never resizes the playfield. Same double scoping as the tab row. */
@media (max-width: 700px) {
  .mobile-tuned .model-roster--sheet {
    position: fixed;
    left: 0;
    right: 0;
    /* Above the tab strip, which is itself above the side-desk lane:
       --pe-desk (the lane) + 56 (MOBILE_TAB_STRIP_H). */
    /* Down to the desk lane, not to the tab row: the tabs travel WITH the
       card, so stopping at their resting slot left an empty band under the
       card once they moved. Parked, the card is clipped by this window. */
    bottom: var(--pe-desk, 0px);
    width: auto;
    /* dvh, not vh: vh is the pre-collapse address-bar height on iOS, which
       would hang the bottom of the sheet below the fold. */
    height: auto;
    /* As tall as the screen allows while still leaving a strip of the chapter
       dock visible above it — that strip is the scrim, and tapping it is one
       of the three ways to put the sheet away. 116 = 56 tab strip + 60 of
       visible HUD. dvh, not vh: vh is the pre-collapse address-bar height on
       iOS and the bottom of the sheet would hang below the fold. */
    /* 150 of headroom: the tabs ride 88px above the card, so a card allowed to
       fill to the top would push them off the screen. This keeps the whole
       tab, plus a strip of the chapter dock to tap on, always visible. */
    max-height: calc(100dvh - 150px - var(--pe-desk, 0px));
    /* NO radius. This box is an invisible clipping window, and a radius on an
       overflow:hidden box clips its CONTENT to the rounded shape — so an 18px
       arc was biting the card's own top corners out from under it, leaving a
       4px break in the left border exactly where the tab hands over to the
       card. The visible rounding belongs to .model-card-surface. */
    border-radius: 0;
    /* THE CARD PULL.
       The sheet itself is a fixed WINDOW that never moves; the card inside it
       does. The card is parked below the window (translateY(100%)) and slides
       up out from behind the tab strip like a card drawn from a deck, then
       pushes back down into it. The card moves as a rigid body — it is not
       revealed in place — which is what makes it read as a card rather than a
       curtain.
       pointer-events:none on the closed window keeps the empty box from
       swallowing taps meant for the playfield behind it. */
    overflow: hidden;
    background: none;
    border: 0;
    box-shadow: none;
    pointer-events: none;
  }
  /* pointer-events stays OFF on the window even when open, and goes on the
     CARD instead. The window is as tall as the screen allows, and at stop 1
     (and for any card shorter than the window) most of it is empty air above
     the card — an open window that took taps swallowed every tap aimed at the
     playfield behind it, including the tap meant to put the card away.
     overflow:hidden clips hit-testing too, so the card can only be hit where
     it is actually visible. */
  .mobile-tuned .model-roster--sheet.model-roster--open > .model-roster__list,
  .mobile-tuned .model-roster--sheet.model-roster--dragging > .model-roster__list { pointer-events: auto; }

  /* THE CARD — one object, tab included.
     The tab row is this card's head: a card drawn part-way out of a deck, the
     selected one sitting on top of the stack. So the BORDER belongs to the
     whole moving unit, not to the scrolling page inside it — that is the
     difference between "a card whose contents scroll" and "a scrolling panel
     with a line above it".
     Putting the frame here (.model-roster__list, the thing that translates)
     and the scroll on the inner surface means the border NEVER MOVES and the
     page always stops short of it: scrolled content keeps a quiet band at the
     top instead of running straight into the tabs. That band is what makes
     the scroll read as happening inside the card. */
  .mobile-tuned .model-roster--sheet > .model-roster__list {
    margin: 0 8px 0;
    /* 3px, up from 2: the frame is now the only thing drawing the card, and
       at 2px against a busy playfield it disappeared. Same colour family as
       .model-tab, a shade deeper so the join carries. */
    border: 3px solid rgba(118, 74, 18, 0.62);
    border-bottom: 0;
    /* A corner with a tab sitting on it is SQUARE, so the tab's side border
       and the card's side border are collinear and read as one unbroken line
       — that is what makes them a single card rather than a card with a
       button balanced on top. A corner with nothing above it stays rounded.
       Which is which is measured, not assumed: syncCardJoinCorners (app.js)
       sets these per frame, because the row scrolls and the right-hand join
       comes and goes with it. */
    border-radius: var(--pe-card-tl, 0px) var(--pe-card-tr, 14px) 0 0;
    background: linear-gradient(180deg, #fdf6dd, #efe1ae) !important;
    box-shadow: inset 0 1px 0 rgba(255, 248, 215, 0.95) !important;
    /* The quiet band. Padding on the FRAME, not on the scroller, so it cannot
       scroll away — 10px of card is always visible between the tabs and
       whatever the page has scrolled to. */
    padding: 10px 0 0 !important;
    /* Clip the scroller to the rounded corners. */
    overflow: hidden;
  }
  .mobile-tuned .model-card-surface {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* No frame of its own any more — it is the page, not the card. */
    margin: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    /* 6px of top padding INSIDE the scroller, on top of the frame's own 10px
       band. The frame's band is what scrolled content cannot enter; this is
       what stops the first panel from starting on the scrollport's very first
       pixel, where its top corner radius and the glow above it were being
       shaved off by the clip. It scrolls away with the content, which is
       exactly right: it is breathing room at rest, not a second band. */
    padding: 6px 10px 10px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Until the card is all the way out its own scroller is OFF, and a swipe
     inside it PULLS THE CARD UP instead — 1:1 with the finger, settling like
     any other pull (see the roster's pointerdown handler in app.js).
     touch-action is the load-bearing half: once the browser has claimed a
     gesture for scrolling, nothing in JS can take it back, so the axis has to
     be refused before the finger moves. overflow-y:hidden is the belt to that
     braces — it stops a stray programmatic scroll sliding the page up behind
     the cut, where it cannot be scrolled back.

     This replaced a version that watched for a scroll and then jumped the card
     to its full stop, putting the scroll back to zero afterwards: two
     discontinuities in one gesture, and it read as a lurch. Growing with the
     finger has neither. */
  .mobile-tuned .model-roster--sheet .model-card-surface {
    touch-action: none;
    overflow-y: hidden;
  }
  /* The pull owns the card's empty space, not its controls. Giving the
     controls `touch-action: auto` back is the other half of isCardControl in
     app.js: without it the browser hands us the gesture before our handler
     ever gets to decline it. */
  .mobile-tuned .model-roster--sheet .model-card-surface button,
  .mobile-tuned .model-roster--sheet .model-card-surface a[href],
  .mobile-tuned .model-roster--sheet .model-card-surface input,
  .mobile-tuned .model-roster--sheet .model-card-surface select,
  .mobile-tuned .model-roster--sheet .model-card-surface textarea,
  .mobile-tuned .model-roster--sheet .model-card-surface [role="button"],
  .mobile-tuned .model-roster--sheet .model-card-surface [draggable="true"],
  .mobile-tuned .model-roster--sheet .model-card-surface [data-no-sheet-drag],
  .mobile-tuned .model-roster--sheet .model-card-surface [data-knn-drag],
  .mobile-tuned .model-roster--sheet .model-card-surface [data-knn-store],
  .mobile-tuned .model-roster--sheet .model-card-surface [data-knn-trash] {
    touch-action: auto;
  }
  /* And they have to be big enough for a thumb. The k-NN exemplars are the
     smallest thing a player is asked to DRAG anywhere in the game. */
  .mobile-tuned .knn-details__slot {
    min-width: 44px;
    min-height: 44px;
  }
  .mobile-tuned .knn-details__slot svg { width: 100%; height: 100%; }
  /* While an exemplar is being dragged by finger, nothing else may claim the
     gesture — not the card's pull, not the page. */
  .mobile-tuned.knn-touch-dragging,
  .mobile-tuned.knn-touch-dragging .model-card-surface { touch-action: none; }
  .mobile-tuned.knn-touch-dragging .knn-details__slot { cursor: grabbing; }
  /* A drag that starts on an exemplar is ours FROM THE FIRST TOUCH. Setting
     touch-action on the body once the drag is recognised (above) is too late:
     the browser decides pan-or-not at touchstart, so a mostly-vertical drag
     toward the trash (which sits BELOW the buckets) was taken as a scroll of
     the card, the browser cancelled the pointer, and the drop never
     happened — on a phone only a tap (remove) ever worked. The cost is that a
     swipe starting exactly on a 44px exemplar does not scroll the card. The
     native long-press drag and callout are off for the same reason: iOS
     would otherwise start its own drag preview and cancel ours. */
  .mobile-tuned [data-knn-drag],
  .mobile-tuned .model-roster--sheet .model-card-surface [data-knn-drag] {
    /* The second selector out-ranks the `touch-action: auto` the card's
       control list above hands every [data-knn-drag] — that `auto` is exactly
       what let the browser take the drag as a scroll. */
    touch-action: none;
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    user-select: none;
  }
  .mobile-tuned .knn-touch-ghost {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 200;
    pointer-events: none;
    opacity: 0.9;
    box-shadow: 0 8px 18px rgba(58, 40, 14, 0.35);
    border-radius: 8px;
    will-change: transform;
  }
  .mobile-tuned .knn-details__dragging { opacity: 0.35; }
  /* All the way out: the page is the thing being read now, so the scroller
     comes back. pan-y rather than auto — sideways in here belongs to nothing. */
  .mobile-tuned .model-roster--sheet.model-roster--full .model-card-surface {
    touch-action: pan-y;
    overflow-y: auto;
  }
  .mobile-tuned .model-roster--sheet > .model-roster__list {
    /* The card stays in flow so the window can size itself to it (cards
       differ in height per model). The window is overflow:hidden, and an
       overflow:hidden box is still PROGRAMMATICALLY scrollable — a
       scrollIntoView inside the roster was scrolling it by the card's height
       and silently cancelling the pull. keepModelSheetUnscrolled() is what
       holds it at zero; see app.js. */
    /* 100% = parked entirely below the window; subtracting the pull brings it
       out. At pull = card height the card sits flush at the window bottom,
       which is exactly where the icon row is. */
    transform: translateY(calc(100% - var(--pe-pull, 0px)));
    transition: transform 180ms ease;
    will-change: transform;
  }

  /* while a finger is down the variable is driven directly; no easing */
  .mobile-tuned.model-sheet-dragging .model-roster--sheet > .model-roster__list,
  .mobile-tuned.model-sheet-dragging .model-tabs.is-on { transition: none; }
  /* Same suppression, for the other case where easing is wrong: the card's
     content was just swapped, so its height changed in one frame and the two
     eased transforms would travel on different curves — see
     setModelSheetPullInstant in app.js. */
  .mobile-tuned.model-sheet-resyncing .model-roster--sheet > .model-roster__list,
  .mobile-tuned.model-sheet-resyncing .model-tabs.is-on { transition: none; }
  /* THE DOWNWARD GLIDE. A card shorter than the distance it has to fall has
     nowhere above itself to be drawn: its window is sized to it, and that
     window's top edge already IS the resting position. --pe-fill pads the card
     out, below its content, to exactly the height the travel needs; the window
     grows with it, and the card then glides down out of a window that was big
     enough all along. It is 0 at every other moment, set once per glide and
     never animated, and by the time the glide lands every padded pixel has
     passed below the lane and is clipped away — so dropping it at the end
     changes nothing on screen. See morphModelSheetPull in app.js.
     It rides on a custom property rather than an inline style so that a card
     rebuilt mid-glide still gets it, and because the layout pass in draw()
     clears the roster's inline geometry every frame. */
  .mobile-tuned.model-sheet-resyncing .model-roster--sheet > .model-roster__list {
    /* !important because the rule above pins this card's padding with one
       (it has to, to beat the desktop `padding: 8px 10px 12px`), and an
       ordinary declaration loses to !important however specific it is. This
       one is both more specific and later, so between the two !importants
       this wins — and it is 0 at every moment except mid-glide.
       Padding on the FRAME and not min-height on the card: .model-card-surface
       is `flex: 1 1 auto`, so a taller card would stretch the scroller and
       re-lay out the page inside it. Padding leaves the page alone and puts
       the blank below it, inside the card's own border. */
    padding-bottom: var(--pe-fill, 0px) !important;
  }

  /* the card's own tab: the same icon and name as the tab it was pulled from,
     so the thing on the card matches the thing you grabbed */
  .mobile-tuned .model-sheet-tab {
    flex: 0 0 auto;
    cursor: grab;
    touch-action: none;
    display: none;                /* the index icons are the card's tab now */
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 14px 6px;
    border-bottom: 1px solid rgba(131, 85, 26, 0.28);
    font-family: inherit;
    color: rgba(82, 50, 12, 0.96);
  }
  .mobile-tuned .model-sheet-tab__portrait { width: 26px; height: 26px; border-radius: 4px; overflow: hidden; }
  .mobile-tuned .model-sheet-tab__portrait svg { width: 100%; height: 100%; display: block; }
  .mobile-tuned .model-sheet-tab__name { font-size: 14px; font-weight: 700; }
  /* the grab bar — the affordance that says "pull me" */
  .mobile-tuned .model-sheet-tab__grip {
    margin-left: auto;
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: rgba(131, 85, 26, 0.38);
  }

  /* Plan E: the tab IS the model, so the MODELS / MODEL DETAILS strip is
     redundant. Hidden, NOT removed from the HTML — renderModelRoster reuses
     those two buttons across re-renders on purpose (a full innerHTML replace
     at 10-20Hz was eating clicks), and deleting them takes that path away. */
  .mobile-tuned .model-roster--sheet .model-roster__tabs { display: none; }

  /* the scrim: tapping it closes the sheet (handler is on window) */
  .mobile-tuned.model-sheet-open::after {
    content: "";
    position: fixed;
    /* the scrim stops above the desk lane — the lane is meant to stay
       readable, and dimming it would defeat the point. The tab strip is at
       z-index 22, above the scrim, so it stays live too. */
    inset: 0 0 var(--pe-desk, 0px) 0;
    z-index: 20;                  /* under the sheet (21) and the tabs (22) */
    background: rgba(28, 18, 4, 0.34);
  }
}
/* Double-scoped like every other .mobile-tuned rule: the width query is part
   of the contract, not decoration (tests/mobile-layout-isolation.spec.js). */
@media (max-width: 700px) and (prefers-reduced-motion: reduce) {
  .mobile-tuned .model-roster--sheet { transition: none; }
}

/* Plan E: the sheet holds ONE model's page — its card, then its details
   panel — rather than the roster list. */
@media (max-width: 700px) {
  .mobile-tuned .model-roster__list--page {
    display: flex;
    flex-direction: column;
  }
  /* The card is the page's header. Its desktop grid is
     `64px 1fr auto` — portrait | info | speed dial — which at 390px squeezes
     the info column until the upgrade pips wrap into a tall two-wide strip and
     the dial becomes a full-height blue slab. On a phone it stacks instead:
     portrait + name on one row, pips under them, dial full width at the
     bottom. */
  /* The card opens with its model stated once, large: portrait + name, then
     features, then upgrade, then details. */
  .mobile-tuned .model-card-surface > .model-roster__card > .model-roster__portrait {
    width: 72px;
    height: 72px;
  }
  .mobile-tuned .model-card-surface .model-roster__name {
    font-size: 21px;
    line-height: 1.15;
  }
  /* The tab already says which model is active — a badge on the card as well
     is the same claim twice, and it collides with the card's top edge. */
  .mobile-tuned .model-card-surface .model-roster__badge-active { display: none; }

  .mobile-tuned .model-card-surface > .model-roster__card {
    flex: 0 0 auto;
    grid-template-columns: 72px 1fr;
    grid-template-areas:
      "portrait info"
      "dial     dial";
    align-items: start;
    padding: 12px;
    gap: 10px 12px;
  }
  .mobile-tuned .model-card-surface > .model-roster__card > .model-roster__portrait {
    grid-area: portrait;
  }
  .mobile-tuned .model-sheet-tab { display: none; }
  .mobile-tuned .model-card-surface > .model-roster__card > .model-roster__info {
    grid-area: info;
    min-width: 0;
  }
  /* Feature icons: 32px on a desktop where they sit in a wide card and the
     pointer is precise. On a phone they are the card's main control surface
     and the finger is not — 46 is a comfortable tap and reads at arm's
     length. */
  .mobile-tuned .model-card-surface .model-roster__pip {
    width: 46px;
    height: 46px;
  }
  .mobile-tuned .model-card-surface .model-roster__upgrades {
    gap: 7px;
    margin-top: 8px;
  }
  .mobile-tuned .model-card-surface > .model-roster__card > .model-roster__level,
  .mobile-tuned .model-card-surface > .model-roster__card > .model-roster__cost {
    grid-area: dial;
    align-self: auto;
    width: 100%;
    min-width: 0;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    /* Trimmed from 10px 12px + an 18px label. It is the speed/level control,
       not the headline of the card — at the old size it filled the whole
       first stop and read as the only thing on the page. */
    padding: 6px 10px;
    border-radius: 10px;
  }
  .mobile-tuned .model-card-surface .model-roster__level-text {
    font-size: 15px;
    letter-spacing: 0.05em;
  }
  /* The speed dial's ↑ buy button measures 56x15 on a phone — the whole dial
     panel is also a level-up target, but the thing that LOOKS like the button
     has to be tappable on its own. Same for the notches. 44px floor, section 3. */
  .mobile-tuned .model-roster__list--page .model-roster__speed-buy,
  .mobile-tuned .model-roster__list--page .model-roster__level-up {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    /* Was width:100% — a full-bleed slab that read as the card's main event.
       It is an occasional purchase, so it sizes to its own label now and sits
       centred. Still 44 tall: shrinking the finger target was not the ask,
       and the whole dial panel behind it is a level-up target anyway. */
    width: auto;
    align-self: center;
    min-width: 132px;
    max-width: 100%;
    padding: 0 16px;
    min-height: 44px;
    margin-top: 6px;
    font-size: 13px;
    border-radius: 9px;
  }
  .mobile-tuned .model-roster__list--page .model-roster__speed-notch {
    min-width: 30px;
    min-height: 30px;
  }
  .mobile-tuned .model-roster__list--page .model-roster__speed-strip {
    gap: 6px;
    flex-wrap: wrap;
  }

  .mobile-tuned .model-roster__page-viz {
    flex: 1 1 auto;
    min-height: 0;
    border-top: 1px solid rgba(131, 85, 26, 0.28);
    padding-top: 10px;
  }
}

/* ── Plan E: phone touch targets in the assignments menu ──────────────────
   The level list is the one place a player navigates the whole game from, and
   on a phone its rows were ~32px tall — under the 44px Apple HIG floor, with
   4px between neighbours, so a thumb landed on the wrong level as often as
   the right one. Rows grow to the floor and the gaps grow with them.
   Same double scoping as every other Plan E rule: `.mobile-tuned` AND a phone
   media query, so a build with neither (the CS3780 study, karthik) renders
   byte-identically at every width. */
@media (max-width: 700px) {
  .mobile-tuned .assignments-overlay__panel {
    width: 96vw;
    max-height: 88dvh;
    padding: 16px 12px 14px;
  }
  .mobile-tuned .assignments-overlay__list,
  .mobile-tuned .assignments-overlay__chapter-rows {
    gap: 8px;
  }
  .mobile-tuned .assignments-overlay__row {
    /* 46, not 44: the border is inside the box, so a 44px min-height leaves a
       42px live area once the 1px rules are taken off. */
    min-height: 46px;
    padding: 10px 12px;
    gap: 12px;
    font-size: 15px;
    border-radius: 14px;
  }
  .mobile-tuned .assignments-overlay__row-num {
    min-width: 44px;
    font-size: 15px;
  }
  .mobile-tuned .assignments-overlay__row-badge {
    font-size: 11px;
    padding: 3px 9px;
  }
  /* The chapter header is the other tap target in this list (it scrolls the
     group into view), and it sat at ~28px. */
  .mobile-tuned .assignments-overlay__chapter > .assignments-overlay__group {
    padding: 10px 6px;
  }
  .mobile-tuned .assignments-overlay__group-name { font-size: 16px; }
  /* The thumbnail is decoration; at 240px it pushed the rows a full screen
     down and turned every chapter into its own page of scrolling. */
  .mobile-tuned .assignments-overlay__chapter-thumb {
    max-width: 148px;
  }
  /* Sticky :hover on touch used to leave a tapped row shifted 2px AND tinted
     pale green (the row's hover background), which read as "this level is
     done". That is now handled globally: every :hover rule in this file is
     prefixed `:where(body:not(.mobile-tuned):not(.landscape-phone))`, so hover paint simply does not
     exist while the phone channel is on. `:where()` adds no specificity, so
     the cascade is byte-identical when the channel is off. */
  /* The "unlocks k-NN (Scans)" chip is a flex item after the title, and the
     title is `flex: 1; min-width: 0` with visible overflow — so once the title
     wraps to two lines on a phone it spills sideways UNDER the chip and the
     two read as printed on top of each other. Give the chips a line of their
     own below the title instead of a share of the same one. `order` is
     explicit because every item defaults to 0 and source order alone would
     put the chips before the badge. */
  .mobile-tuned .assignments-overlay__row {
    flex-wrap: wrap;
    row-gap: 4px;
  }
  .mobile-tuned .assignments-overlay__row-num { order: 0; }
  .mobile-tuned .assignments-overlay__row-title {
    order: 1;
    overflow-wrap: anywhere;
  }
  .mobile-tuned .assignments-overlay__row-badge { order: 2; }
  .mobile-tuned .assignments-overlay__row-unlocks {
    order: 3;
    /* A full-width basis forces the wrap; the indent lines the chips up with
       the title rather than with the level number.
       The indent is PADDING, not margin: flex-basis resolves against the
       container's content box and a margin is added OUTSIDE it, so
       `flex: 1 0 100%` + `margin-left: 56px` overflowed every row by exactly
       the difference — 44px at every width — and that overflow is what let
       the whole assignments menu be dragged sideways. box-sizing keeps
       padding inside the basis, so this fits exactly. */
    flex: 1 0 100%;
    min-width: 0;
    box-sizing: border-box;
    margin-left: 0;
    margin-right: 0;
    padding-left: 56px;
    flex-wrap: wrap;
    gap: 6px;
  }

  /* THE LEVEL LIST SCROLLS UP AND DOWN, NEVER SIDEWAYS.
     One overflowing child is enough to make the whole menu draggable
     horizontally, and on a phone that reads as the menu being broken — you
     lose your place and there is nothing over there to see. Belt to the
     braces of the padding fix above.
     ONLY ON THE SCROLL CONTAINER ITSELF. This clamp was briefly also on the
     panel, on each chapter <li>, on the chapter body and on each row, and it
     emptied the menu completely: an element whose overflow is not `visible`
     has an automatic minimum size of ZERO instead of its content size, so the
     chapter items — grid items in the list — collapsed to 0px tall and the
     whole list rendered blank while still reporting 163 rows in the DOM.
     The list is the only element here that is genuinely a scroll container,
     and it is the only one that needs the clamp. */
  .mobile-tuned .assignments-overlay__list {
    overflow-x: hidden;
  }
  /* A long level name wraps instead of pushing the row wide. */
  .mobile-tuned .assignments-overlay__row-title {
    overflow-wrap: anywhere;
    word-break: break-word;
  }
}

/* ── Plan E: two small overflows the phone sweep turned up ────────────────
   Both are a couple of pixels, both only bite at phone widths, and both would
   otherwise let a panel be dragged sideways a fraction of an inch — which on
   a touch screen is worse than it sounds, because it steals the vertical
   scroll gesture. */
@media (max-width: 700px) {
  /* The deep-feature tiles are a fixed-size grid of thumbnails; at a phone's
     card width the last column runs 3-7px past its cell. Clip rather than
     rescale — the thumbnails are pixel art and a fractional scale muddies
     them, and the overflow is a rounding tail, not lost content. */
  .mobile-tuned .mlp-details__deep-features,
  .mobile-tuned .mlp-details__deep-features-grid,
  .mobile-tuned .mlp-details__deep-feature {
    overflow-x: hidden;
    min-width: 0;
  }
  .mobile-tuned .breakthrough-strip__open-hint {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
  }
  /* The progress row's label is a 62px + 1fr grid; on a 360px phone the
     right-hand cell's text is 9px wider than the cell. Narrow the fixed
     column and let the text wrap rather than clipping it — "NEED 12 MORE
     BATCHES" is the one string in the dock the player actually needs. */
  .mobile-tuned .breakthrough-strip__progress-row,
  .mobile-tuned .breakthrough-strip__progress-stack,
  .mobile-tuned .breakthrough-strip__progress-card {
    min-width: 0;
  }
  /* The label is a `62px + 1fr` grid built for a short kicker ("ACCURACY").
     The loss chapters use "Closeness to Bottom", which measures 132px with
     white-space:nowrap and blows straight through the 62px column — 360px
     phones ended up 7px wider than the dock. A wrapping flex row keeps BOTH
     strings whole (this one restates the pass condition, so truncating it is
     not an option) and simply takes a second line when they do not fit. */
  .mobile-tuned .breakthrough-strip__progress-label {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
    /* 10px, not 11: the pair fits one line at 360 with a point to spare, and
       the dock's reserved height (CHAPTER_DOCK_RESERVED_HEIGHT_MOBILE) has no
       room for a second line — wrapping it pushed the dock into the
       playfield's frame at 320 and 360. */
    font-size: 10px;
  }
  /* The PASS CONDITION never shrinks: it is the one string here the player
     has to be able to read. */
  .mobile-tuned .breakthrough-strip__progress-inline-threshold {
    flex: 0 0 auto;
    white-space: nowrap;
  }
  /* The metric's name gives way first, and only on a 320 where there is
     genuinely not room for both (the loss chapters call it "Closeness to
     Bottom", three times the length of "Accuracy"). */
  .mobile-tuned .breakthrough-strip__progress-kicker {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* An unlock chip shrinks and ellipsises rather than pushing its row wide —
     "Image MLP (Camera Scans)" is longer than a 320px phone's row once the
     56px indent is taken off. */
  .mobile-tuned .assignments-overlay__row-unlocks { overflow-x: hidden; }
  .mobile-tuned .assignments-overlay__unlock {
    max-width: 100%;
    min-width: 0;
  }
  .mobile-tuned .assignments-overlay__unlock-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* And the indent goes away entirely on the narrowest phones, where 56px is
     a quarter of the row. */
  @media (max-width: 360px) {
    .mobile-tuned .assignments-overlay__row-unlocks { padding-left: 0; }
  }
}

/* ── Plan E: no hover hints on a phone ────────────────────────────────────
   The hint box is a HOVER affordance: shown from mousemove, hidden from the
   document's mouseleave. A touch screen fires exactly one synthetic mousemove
   at the tap point and then never fires mouseleave, so the box appeared on
   the first tap and never went away again — a black slab parked over the
   thing the player had just tapped.
   app.js stops feeding it (tooltipsSuppressed); this makes sure no other path
   can paint it either. Double-scoped like every other Plan E rule, so a build
   that has not opted in keeps its hover hints at every width. */
@media (max-width: 700px) {
  .mobile-tuned .tooltip {
    display: none !important;
  }
}

/* Perceptron Model Details — the "last update" figure (ui/perceptron-scene.js) */
.puf { margin: 6px 0 10px; display: grid; justify-items: center; gap: 6px; }
.puf__title { font-family: 'Fraunces', serif; font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(106, 67, 20, 0.78); }
.puf__svg { width: 100%; max-width: 240px; height: auto; border-radius: 6px; }
.puf__math { width: 100%; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.5; color: rgba(58, 40, 14, 0.9); }
.puf__row { display: flex; justify-content: space-between; gap: 10px; padding: 1px 6px; border-radius: 4px; transition: background 160ms ease; }
.puf__row .puf__k { color: rgba(106, 67, 20, 0.85); white-space: nowrap; }
.puf__row .puf__v { text-align: right; white-space: nowrap; }
.puf__row--on { background: rgba(255, 232, 142, 0.55); }
.puf__row--result { border-top: 1px solid rgba(131, 85, 26, 0.25); margin-top: 2px; padding-top: 3px; font-weight: 600; }
.puf__row--done.puf__row--result { background: rgba(60, 180, 160, 0.18); }
.puf__hint { font-family: 'Fraunces', serif; font-size: 12px; color: rgba(82, 50, 12, 0.7); text-align: center; padding: 4px 6px 0; }

/* ═══ Resized-window tuning (the "pane" channel) ═══════════════════════════
   Everything below is scoped TWICE, same contract as .mobile-tuned: the
   .pane-tuned body class (set by paneTuningEnabled() in app.js — opt-in per
   curated version or ?pane=1, never on study builds, never in the phone
   layout) AND a min-width media query. Without the class, none of this
   applies and the desktop layout is exactly what it was.

   MODEL CARD RESTACK. The desktop card is one row: portrait | name + pips |
   speed-level slab. It needs ~450px to hold that row; in a narrower roster
   the columns fight — names wrap letter by letter, pips fold into a tall
   2-wide stack, and the slab stretches to the card's full height. So the
   card restacks by the width of the ROSTER it sits in (a container query,
   not a media query — the window width is the wrong input once the split
   ratio changes): portrait + name + pips on top, the speed slab as a full-
   width bar underneath. */
@media (min-width: 700px) {
  .pane-tuned .model-roster__list {
    container-type: inline-size;
    container-name: roster;
  }
}
@container roster (max-width: 460px) {
  .pane-tuned .model-roster__card {
    grid-template-columns: 48px minmax(0, 1fr);
    gap: 8px 12px;
    padding: 10px 12px;
  }
  .pane-tuned .model-roster__portrait {
    width: 48px;
    height: 48px;
    align-self: start;
  }
  .pane-tuned .model-roster__name {
    font-size: 15px;
  }
  .pane-tuned .model-roster__upgrades {
    margin-top: 5px;
  }
  .pane-tuned .model-roster__pip {
    width: 28px;
    height: 28px;
    font-size: 16px;
  }
  /* The speed slab and the unlock price become a full-width bar under the
     card's top row instead of a third column. */
  .pane-tuned .model-roster__card > .model-roster__level,
  .pane-tuned .model-roster__card > .model-roster__cost {
    grid-column: 1 / -1;
  }
  .pane-tuned .model-roster__card > .model-roster__level {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    column-gap: 10px;
    row-gap: 4px;
    min-width: 0;
    padding: 6px 12px;
  }
  .pane-tuned .model-roster__card > .model-roster__level .model-roster__level-text {
    font-size: 15px;
    letter-spacing: 0.06em;
  }
}

/* The legacy console dock (model <select> + activate) sits UNDER the roster
   (z 20 vs 21) and is fully covered by it at 50/50. Its toprow grid has a
   260px minimum, so in the narrower 40/60 roster its activate button poked
   out past the roster's right edge as a stray sliver. Clip it to its own box,
   which the layout already keeps inside the roster. */
@media (min-width: 700px) {
  .pane-tuned .console-dock {
    overflow: hidden;
  }
}

/* Roster tabs: at the narrowest desktop split (roster ~256px at 701 wide) the
   "MODEL DETAILS" label wrapped to two lines and pushed the whole list down.
   Keep both labels on one line by trimming the tracking there. */
@media (min-width: 700px) {
  .pane-tuned .model-roster {
    container-type: inline-size;
    container-name: rosterpane;
  }
  .pane-tuned .model-roster__tab {
    white-space: nowrap;
  }
}
@container rosterpane (max-width: 300px) {
  .pane-tuned .model-roster__tab {
    letter-spacing: 0.02em;
    padding: 6px 4px;
  }
}

/* CHAPTER DOCK. The dock's two-row headline (title row, metrics row, icon-only
   "All assignments") was keyed to the WINDOW (701-1180 wide). The dock is
   half the window, so between ~1180 and ~1500 it was too narrow for the one-
   row headline and the "All assignments" label broke into a letter stack
   ("ASSI / GNME / NTS" at 1280). Under the pane channel the same two-row
   rules key off the dock's OWN width instead, which also stays correct when
   the split ratio changes. Short windows keep their compact rules. */
@media (min-width: 700px) {
  .pane-tuned #chapter-dock {
    container-type: inline-size;
    container-name: chapterdock;
  }
}
/* Docks between 790 and 860px (laptop windows from ~1650 wide up) get the
   DESKTOP header untouched — one row, the pill centred in the dock with the
   full "All assignments" label, exactly as it was before the channel (Erik
   2026-09-25, three rounds: "2 rows when it should be 1", "centered weird",
   "still weird" — every departure from the desktop header at that size
   read as wrong). Swept 2026-09-25: short and mid-length titles ("Train the
   Perceptron") hold one row down to a 798px dock and wrap at 768; the
   longest ("Same Words, Different Order") wraps inside the pill below 868,
   as it always did on the desktop. */
/* Stacked (dock under 790px): two rows, EXACTLY two. The first stacked cut
   was a flex wrap with the metrics forced full-width, so the open-hint fell
   to a THIRD row on its own and the card stood ~90px tall — the dock grew
   from 168 to 213px and pushed the playfield down. As a grid the hint shares
   the title row and the metrics sit under it. */
@container chapterdock (max-width: 790px) {
  @media (min-height: 750px) {
    .pane-tuned .breakthrough-strip__header {
      grid-template-columns: minmax(0, 1fr) auto;
    }
    .pane-tuned .breakthrough-strip__utility {
      grid-column: 2;
    }
    .pane-tuned .breakthrough-strip__headline {
      grid-column: 1;
      justify-self: stretch;
      max-width: 100%;
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      grid-template-areas:
        "chapter title hint"
        "metrics metrics metrics";
      align-items: center;
      row-gap: 6px;
      column-gap: 10px;
      padding: 8px 12px 9px;
    }
    .pane-tuned .breakthrough-strip__chapter {
      grid-area: chapter;
    }
    .pane-tuned .breakthrough-strip__title {
      grid-area: title;
      font-size: 16px;
      white-space: normal;
      text-wrap: pretty;
    }
    .pane-tuned .breakthrough-strip__metrics {
      grid-area: metrics;
      margin-left: 0;
      padding-left: 0;
      border-left: 0;
      font-size: 15px;
    }
    .pane-tuned .breakthrough-strip__open-hint-text {
      display: none;
    }
    .pane-tuned .breakthrough-strip__open-hint {
      grid-area: hint;
      justify-self: end;
      margin-left: 0;
      padding: 4px;
    }
  }
}

/* The speed slab used to stretch to the card's full height, so a model with
   three rows of upgrade pips (MLP) got a 140px-tall blue slab. Centre it at
   the portrait's height instead — same look on one-row cards. */
@media (min-width: 700px) {
  .pane-tuned .model-roster__card > .model-roster__level {
    align-self: center;
    min-height: 64px;
    box-sizing: border-box;
  }
}
@container roster (max-width: 460px) {
  .pane-tuned .model-roster__card > .model-roster__level {
    min-height: 0;
  }
}

/* MODEL DETAILS in the narrowest roster (~250px list at 701 wide): the MLP
   weights table's OUT column ran past the details card's right edge. Tighten
   the cell padding and numerals there — same columns, same order. */
@container roster (max-width: 300px) {
  .pane-tuned .mlp-details__table th,
  .pane-tuned .mlp-details__table td {
    padding: 3px 3px;
    font-size: 11px;
  }
}

/* MODEL DETAILS (pane channel). Coco 2026-09-25: "some of the Model Details
   panels need rearranging too". Three things were wrong in a narrow roster:
   1. TYPE. About a dozen newer panels (transformer / LLM, SVM, CLIP, RL, NB,
      logistic, …) use class names that have no CSS at all, so their text fell
      back to the roster's 16px serif: next to the 12px mono/sans of the older
      panels it read as a different app. The details card now sets ONE base
      (12.5px sans); explicitly styled panels keep what they set.
   2. ROWS. The dial rows (.mlp-lr-dial-row, e.g. SVM's SLACK) were unstyled,
      so the label and three buttons wrapped wherever the line broke. The label
      now sits on its own line and the buttons share the next one evenly.
   3. DIAGRAMS. SVG diagrams are drawn for a wide panel and shrink with it —
      labels landed at 5-8px. That is fixed in JS (paneTuneDetailsSvgText in
      app.js), because the right size depends on how far each SVG shrank. */
@media (min-width: 700px) {
  .pane-tuned .model-roster__viz-card {
    font-family: 'Avenir Next', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-size: 12.5px;
    line-height: 1.4;
    color: rgba(58, 40, 14, 0.92);
  }
  /* section headings: same voice as "CURRENT SCAN" / "OPTION SOFTMAX" */
  .pane-tuned .model-roster__viz-card .ts-subhead,
  .pane-tuned .model-roster__viz-card .clip-subhead {
    margin: 10px 0 4px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.3;
    color: rgba(109, 88, 58, 0.82);
  }
  .pane-tuned .model-roster__viz-card .roster-viz-caption,
  .pane-tuned .model-roster__viz-card .ts-prose,
  .pane-tuned .model-roster__viz-card .model-roster__viz-note,
  .pane-tuned .model-roster__viz-card .roster-viz__hint {
    margin: 6px 0;
    font-size: 12.5px;
    line-height: 1.45;
  }
  /* dial rows: label line, then the choices sharing one line */
  .pane-tuned .model-roster__viz-card .mlp-lr-dial-row {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;   /* a choice whose label wraps keeps its row's top edge */
    gap: 6px;
    margin: 8px 0;
  }
  .pane-tuned .model-roster__viz-card .mlp-lr-dial-label {
    flex: 1 0 100%;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: rgba(109, 88, 58, 0.82);
  }
  .pane-tuned .model-roster__viz-card .mlp-lr-dial-btn {
    flex: 1 1 0;
    min-width: 0;
    min-height: 32px;
    padding: 4px 6px;
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    border: 1px solid rgba(131, 85, 26, 0.35);
    border-radius: 8px;
    background: rgba(255, 248, 232, 0.8);
    color: rgba(82, 50, 12, 0.9);
    cursor: pointer;
  }
  .pane-tuned .model-roster__viz-card .mlp-lr-dial-btn.active {
    background: rgba(46, 123, 255, 0.9);
    border-color: rgba(46, 123, 255, 0.95);
    color: #fff;
  }
}

/* ═══ Wide channel (big monitors, option C) ══════════════════════════════════
   Double-scoped like .pane-tuned: .model-roster--split is only set by
   renderModelRoster when wideSplitActive() (layout.js wideTier() >= 2 AND the
   current model has a details page), and the rules sit under a min-width
   query, so nothing here applies to a build that has not opted in. */
@media (min-width: 2300px) {
  .model-roster--split {
    display: grid;
    grid-template-columns: 560px minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }
  .model-roster--split .model-roster__tabs {
    display: none;
  }
  .model-roster--split .model-roster__list {
    grid-row: 2;
    min-height: 0;
  }
  .model-roster--split .model-roster__list--beside {
    border-left: 1px solid rgba(131, 85, 26, 0.25);
  }
}

/* ── Lively dots, CSS edition (ui/lively-dots.js) ─────────────────────────
   A layer over the field's plot area; every dot gets a halo <div> animated
   with transform/opacity only (compositor-driven, no repaint). Hidden during
   drags / result animations (.is-quiet off) so the canvas feedback rings
   read unchanged. Sits above the game canvas, below the HUD docks (21). */
.lively-dots {
  position: fixed;
  z-index: 20;
  pointer-events: none;
  overflow: hidden;
  border-radius: 14px;
  opacity: 0;
  transition: opacity 240ms ease;
  contain: strict;
}
.lively-dots.is-quiet { opacity: 1; }
/* One clipping box per board (the train/test split boards have two). */
.lively-dots__box {
  position: absolute;
  overflow: hidden;
  border-radius: 14px;
}
.lively-dots.hidden { display: none; }
.lively-dots__dot {
  position: absolute;
  border-radius: 50%;
  will-change: transform, opacity;
  animation: lively-dot-breathe 1.9s ease-in-out infinite;
}
.lively-dots__dot--pos {
  background: radial-gradient(circle, rgba(86, 112, 255, 0.6) 0%, rgba(86, 112, 255, 0.42) 26%, rgba(86, 112, 255, 0.16) 46%, rgba(86, 112, 255, 0) 66%);
}
.lively-dots__dot--neg {
  background: radial-gradient(circle, rgba(255, 190, 80, 0.7) 0%, rgba(255, 190, 80, 0.5) 26%, rgba(255, 190, 80, 0.2) 46%, rgba(255, 190, 80, 0) 66%);
}
/* Digit Silhouettes: black ink dots and cream paper dots. */
.lively-dots__dot--ink {
  background: radial-gradient(circle, rgba(40, 40, 52, 0.5) 0%, rgba(40, 40, 52, 0.34) 26%, rgba(40, 40, 52, 0.12) 46%, rgba(40, 40, 52, 0) 66%);
}
.lively-dots__dot--paper {
  background: radial-gradient(circle, rgba(255, 248, 225, 0.85) 0%, rgba(255, 248, 225, 0.6) 26%, rgba(255, 248, 225, 0.24) 46%, rgba(255, 248, 225, 0) 66%);
}
.lively-dots__glint {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 70%);
}
@keyframes lively-dot-breathe {
  0%, 100% { transform: scale(0.82); opacity: 0.55; }
  50% { transform: scale(1.1); opacity: 1; }
}
.lively-dots__sweep {
  position: absolute;
  top: -40%;
  left: 0;
  width: 16%;
  height: 180%;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.34) 50%, rgba(255, 255, 255, 0) 100%);
  transform: translateX(-120%) rotate(18deg);
  will-change: transform;
  animation: lively-dot-sweep 4.2s linear infinite;
}
@keyframes lively-dot-sweep {
  from { transform: translateX(-120%) rotate(18deg); }
  to { transform: translateX(760%) rotate(18deg); }
}

/* ── Landscape phone ──────────────────────────────────────────────────────
   A phone held sideways (layout.js landscapePhoneLayout): the chapter dock
   heads a narrow left column above the model roster and the side-desk lane,
   the playfield takes the full height in the middle, the canvas buttons
   stand on the right. body.landscape-phone is set only when the build opted
   into the mobile channel AND the device is a phone AND the screen is wider
   than tall and at most 500px tall (syncMobileTuningClass, app.js), so no
   other build or window can match these rules. */
/* The legacy console card (model <select> + activate) is covered by the
   roster on desktop; in this short column it pokes out below it. The roster
   cards already select models — same call as the portrait phone rule. */
body.landscape-phone .console-dock {
  display: none;
}
/* The roster lives in the drawer (app.js setLandscapeDrawer). */
body.landscape-phone #model-roster {
  display: none !important;
}
body.landscape-phone .breakthrough-strip {
  padding: 5px 10px 7px;
  border-radius: 20px;
}
/* One header row: headline (left, shrinks) + utility icons (right). */
body.landscape-phone .breakthrough-strip__header {
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 6px;
}
body.landscape-phone .breakthrough-strip__headline {
  grid-column: 1;
  justify-self: stretch;
  min-height: 36px;
  box-sizing: border-box;
  padding: 4px 10px;
  gap: 8px;
  min-width: 0;
}
body.landscape-phone .breakthrough-strip__chapter {
  font-size: 15px;
}
/* The title gets two lines inside the header's 36px instead of one
   ellipsised line — most assignment names are two or three words. */
body.landscape-phone .breakthrough-strip__title {
  flex: 1 1 auto;
  min-width: 48px;
  font-size: 13px;
  line-height: 1.1;
  white-space: normal;
  text-wrap: balance;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* The whole headline already opens the assignments list; its small ≡ hint
   is the first thing to go in a column this narrow. */
body.landscape-phone .breakthrough-strip__open-hint {
  display: none;
}
/* Accuracy is the bar right below; the header keeps only the coin balance. */
body.landscape-phone .breakthrough-strip__metric-acc {
  display: none;
}
body.landscape-phone .breakthrough-strip__metrics {
  margin-left: 2px;
  padding-left: 8px;
  font-size: 13px;
  flex: 0 0 auto;
}
body.landscape-phone .breakthrough-strip__utility {
  grid-column: 2;
}
body.landscape-phone .breakthrough-strip__utility {
  grid-column: 2;
  gap: 3px;
}
body.landscape-phone .breakthrough-strip__utility .utility-dock__button--icon {
  width: 34px;
  height: 34px;
  min-height: 34px;
}
/* Pass condition inline beside the kicker instead of pinned above the bar,
   where it would sit on the header row (same treatment as portrait). */
body.landscape-phone .breakthrough-strip__progress-threshold-label,
body.landscape-phone .breakthrough-strip__progress-threshold::before {
  display: none;
}
body.landscape-phone .breakthrough-strip__progress-inline-threshold {
  display: inline;
  margin-left: auto;
  padding-left: 8px;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  color: rgba(22, 41, 60, 0.82);
}
body.landscape-phone .breakthrough-strip__progress-label {
  display: flex;
  align-items: baseline;
  width: 100%;
  min-width: 0;
  font-size: 10px;
}
/* The advance button gets its own full-width row under the bar: its label
   can be long ("NEED 6 MORE BATCHES AND 22% MORE ACCURACY") and takes two
   lines at this width. The column has the height for it. */
body.landscape-phone .breakthrough-strip__progress-row {
  margin-top: 4px;
  grid-template-columns: minmax(0, 1fr);
  gap: 5px;
}
body.landscape-phone .breakthrough-strip__advance {
  min-height: 40px;
  height: 40px;
  padding: 2px 8px;
  font-size: 10.5px;
  line-height: 1.15;
  letter-spacing: 0.06em;
  white-space: normal;
  text-overflow: clip;
  width: 100%;
  min-width: 0;
  max-width: none;
}
/* The bar shares its row with the advance button, so the label above it is
   short: the pass condition only ("92% over last 10 to pass") — the metric's
   name is what the bar already shows. The pass condition never truncates. */
body.landscape-phone .breakthrough-strip__progress-kicker {
  display: none;
}
body.landscape-phone .breakthrough-strip__progress-inline-threshold {
  margin-left: 0;
  padding-left: 0;
  font-size: 10px;
  letter-spacing: 0.02em;
}
/* No hover on a phone: hint boxes are suppressed in JS (tooltipsSuppressed)
   and hidden here as well, same as the portrait rule. */
body.landscape-phone .tooltip {
  display: none !important;
}
/* Under 700px wide the dock's metrics are hidden for the PORTRAIT phone,
   which shows a coin chip instead. A landscape phone that narrow (iPhone SE,
   667) has no chip, so it keeps the inline coin balance. */
@media (max-width: 700px) {
  body.landscape-phone .breakthrough-strip__metrics {
    display: inline-flex;
  }
}

/* ── Landscape phone: active-model bar + roster drawer ─────────────────── */
:root {
  --landscape-drawer-w: min(440px, calc(100vw - 140px));
}
.landscape-summary,
.landscape-drawer-backdrop {
  display: none;
}
/* Active-model bar under the dock in the right-hand column: opens the drawer. */
body.landscape-phone .landscape-summary {
  display: flex;
  position: fixed;
  z-index: 21;
  box-sizing: border-box;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: 16px;
  border: 2px solid rgb(124, 72, 160);
  background: linear-gradient(180deg, #efe2f6, #e0cdef);
  box-shadow: 0 3px 0 rgba(90, 50, 120, 0.35);
  text-align: left;
  cursor: var(--cursor-pointer);
  -webkit-tap-highlight-color: transparent;
}
body.landscape-phone .landscape-summary__portrait,
body.landscape-phone .landscape-summary__portrait svg {
  flex: 0 0 auto;
  display: block;
  width: 34px;
  height: 34px;
}
body.landscape-phone .landscape-summary__text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
body.landscape-phone .landscape-summary__text b {
  font: 700 15px/1.1 Georgia, "Times New Roman", serif;
  color: rgb(58, 40, 14);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
body.landscape-phone .landscape-summary__text small {
  font: 700 9.5px/1 "Trebuchet MS", sans-serif;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: rgb(104, 62, 140);
}
body.landscape-phone .landscape-summary__chevron {
  margin-left: auto;
  font: 700 24px/1 sans-serif;
  color: rgb(104, 62, 140);
}

/* The drawer: the regular roster, full height, from the right edge. */
body.landscape-phone.landscape-drawer-open #model-roster {
  display: flex !important;
  top: 0 !important;
  left: auto !important;
  right: 0 !important;
  width: var(--landscape-drawer-w) !important;
  height: 100vh !important;
  height: 100dvh !important;
  z-index: 40 !important;
  border-radius: 22px 0 0 22px;
  box-shadow: -10px 0 30px rgba(20, 12, 2, 0.35);
  animation: landscape-drawer-in 180ms ease-out;
}
@keyframes landscape-drawer-in {
  from { transform: translateX(40px); opacity: 0.4; }
  to { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  body.landscape-phone.landscape-drawer-open #model-roster { animation: none; }
}
body.landscape-phone.landscape-drawer-open .landscape-drawer-backdrop {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 39;
  background: rgba(20, 14, 4, 0.45);
}
body.landscape-phone .landscape-summary__text small {
  white-space: nowrap;
  font-size: 9px;
  letter-spacing: 0.05em;
}
/* A buyable upgrade somewhere in the roster: a green dot on the bar, the
   same signal the portrait tabs use. */
body.landscape-phone .landscape-summary {
  position: fixed;
}
body.landscape-phone .landscape-summary--upgrade::after {
  content: "";
  position: absolute;
  top: 6px;
  right: 8px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgb(66, 170, 92);
  box-shadow: 0 0 0 2px #efe2f6;
}
/* The effects canvas (desk results flying out, "+18 points", batch hand-offs)
   normally paints above every panel (z 75). While the drawer is open it goes
   UNDER the dimmed backdrop, so nothing from the side desks shows through the
   roster. */
body.landscape-phone.landscape-drawer-open #fx-overlay {
  z-index: 38;
}
/* Assignments menu on a landscape phone: narrower, and the title and the
   timer share one row so the list gets the height (Keyi, 2026-10-02). */
body.landscape-phone .assignments-overlay__panel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 12px;
  width: min(540px, 90vw);
  max-height: calc(100dvh - 24px);
  padding: 12px 16px 12px;
  border-radius: 22px;
}
body.landscape-phone .assignments-overlay__panel .milestone-overlay__eyebrow {
  grid-column: 1;
  justify-self: end;
  text-align: right;
}
body.landscape-phone .assignments-overlay__time {
  grid-column: 2;
  justify-self: start;
  margin-top: 0;
  text-align: left;
  font-size: 16px;
}
body.landscape-phone .assignments-overlay__list {
  grid-column: 1 / -1;
  align-self: stretch;
  min-height: 0;
  margin-top: 8px;
}
body.landscape-phone .assignments-overlay__panel .milestone-overlay__button {
  grid-column: 1 / -1;
  justify-self: center;
  margin-top: 10px;
  min-height: 44px;
  padding-top: 8px;
  padding-bottom: 8px;
}
/* Short landscape screens (≤ 340 tall): a tighter dock so the model bar and
   the desk lane still fit under it. */
@media (max-height: 340px) {
  body.landscape-phone .breakthrough-strip {
    padding: 4px 8px 5px;
  }
  body.landscape-phone .breakthrough-strip__headline {
    min-height: 30px;
    padding: 2px 8px;
  }
  body.landscape-phone .breakthrough-strip__utility .utility-dock__button--icon {
    width: 30px;
    height: 30px;
    min-height: 30px;
  }
  body.landscape-phone .breakthrough-strip__progress-row {
    margin-top: 2px;
    gap: 3px;
  }
  body.landscape-phone .breakthrough-strip__advance {
    min-height: 32px;
    height: 32px;
    font-size: 9.5px;
  }
  body.landscape-phone .landscape-summary {
    padding: 3px 8px;
  }
  body.landscape-phone .landscape-summary__portrait,
  body.landscape-phone .landscape-summary__portrait svg {
    width: 28px;
    height: 28px;
  }
  body.landscape-phone .landscape-summary__text b {
    font-size: 13px;
  }
}

/* ── Phones: menus close by tapping outside, no Close button ─────────────
   The assignments menu already closes on a tap on its dimmed backdrop
   (ui/overlays.js); on a phone the button only costs a row of the list. */
@media (max-width: 700px) {
  .mobile-tuned #assignments-overlay-close {
    display: none;
  }
}
body.landscape-phone #assignments-overlay-close {
  display: none;
}

/* ── Title screen on a landscape phone ────────────────────────────────────
   The portrait-shaped panel (title, buttons, logos, credits, funding text
   stacked) is ~550px tall; a phone held sideways has 320-430. Two columns:
   title + buttons on the left, logos and credits on the right. */
body.landscape-phone .title-screen {
  padding: 10px;
}
body.landscape-phone .title-screen__panel {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  grid-template-rows: auto auto auto auto auto;
  column-gap: 28px;
  row-gap: 0;
  align-content: center;
  align-items: center;
  width: min(860px, 96vw);
  max-height: calc(100dvh - 20px);
  overflow-y: auto;
  padding: 14px 22px;
}
body.landscape-phone .title-screen__title {
  grid-column: 1;
  grid-row: 1 / 3;
  align-self: end;
  margin: 0 0 8px;
  font-size: clamp(26px, 9vh, 42px);
}
body.landscape-phone .title-screen__subtitle {
  grid-column: 1;
  grid-row: 3;
  font-size: 13px;
  line-height: 1.35;
}
body.landscape-phone .title-screen__offline {
  grid-column: 1;
  grid-row: 3;
}
body.landscape-phone .title-screen__actions {
  grid-column: 1;
  grid-row: 4 / 6;
  align-self: start;
  margin-top: 12px;
}
body.landscape-phone .title-screen__credits {
  grid-column: 2;
  grid-row: 1;
  margin: 0 auto;
  gap: 14px;
}
body.landscape-phone .title-screen__logo--cornell { height: 48px; }
body.landscape-phone .title-screen__logo--aimi { height: 38px; }
body.landscape-phone .title-screen__credits-divider { height: 36px; }
body.landscape-phone .title-screen__attribution {
  grid-column: 2;
  grid-row: 2;
  margin: 8px auto 0;
  font-size: 12px;
}
body.landscape-phone .title-screen__version {
  grid-column: 2;
  grid-row: 3;
  margin: 4px auto 0;
}
body.landscape-phone .title-screen__funding {
  grid-column: 2;
  grid-row: 4;
  margin: 6px auto 0;
  font-size: 9.5px;
  line-height: 1.35;
}
body.landscape-phone .title-screen__consent {
  grid-column: 2;
  grid-row: 5;
  margin: 4px auto 0;
  font-size: 8.5px;
}

/* ── Title screen on a portrait phone: a little tighter, so the title never
   rides up against the top edge on a short phone (iPhone SE, 667). */
@media (max-width: 700px) {
  .mobile-tuned .title-screen {
    padding: 14px;
  }
  .mobile-tuned .title-screen__panel {
    padding: 22px 18px 18px;
    max-height: calc(100dvh - 28px);
    overflow-y: auto;
  }
  .mobile-tuned .title-screen__title {
    margin: 4px 0 8px;
    font-size: clamp(30px, 9.5vw, 40px);
  }
  .mobile-tuned .title-screen__subtitle {
    font-size: 15px;
  }
  .mobile-tuned .title-screen__credits {
    margin-top: 16px;
  }
  .mobile-tuned .title-screen__logo--cornell { height: 56px; }
  .mobile-tuned .title-screen__logo--aimi { height: 44px; }
  .mobile-tuned .title-screen__credits-divider { height: 40px; }
  .mobile-tuned .title-screen__funding {
    font-size: 10.5px;
  }
}
