/* ==========================================================================
   Free Learning Academy — "Sticker Book" design system
   Signature: chunky press-down ledges. Interactive surfaces sit on a solid
   colored ledge and press into it on tap. Everything else stays quiet.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Type */
  --font-display: "Baloo 2", "Trebuchet MS", system-ui, sans-serif;
  --font-body: "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-code: ui-monospace, "SFMono-Regular", "Cascadia Code", monospace;

  /* Subject hues — every subject owns one */
  --sky: #3d6bff;
  --sky-deep: #2447c9;
  --grape: #7c4dff;
  --grape-deep: #5a2fd6;
  --bubble: #ff4d8d;
  --bubble-deep: #d62a6a;
  --sunny: #ffb020;
  --sunny-deep: #d98a00;
  --mint: #0fb984;
  --mint-deep: #0a8f66;
  --tangerine: #ff6b35;
  --tangerine-deep: #d94b18;
  --berry: #e0399b;
  --berry-deep: #b21f78;
  --ocean: #0ea5b7;
  --ocean-deep: #087e8c;

  /* Semantic */
  --primary: var(--sky);
  --primary-deep: var(--sky-deep);
  --success: var(--mint);
  --success-deep: var(--mint-deep);
  --warning: var(--sunny);
  --warning-deep: var(--sunny-deep);
  --danger: #f0453a;
  --danger-deep: #c4271d;

  /* Surfaces */
  --bg: #eef1ff;
  --bg-tint-a: rgba(124, 77, 255, 0.1);
  --bg-tint-b: rgba(15, 185, 132, 0.09);
  --bg-tint-c: rgba(255, 176, 32, 0.1);
  --card: #ffffff;
  --card-soft: #f7f8ff;
  --rail: #131a44;
  --rail-ink: #e6eaff;
  --rail-ink-soft: #a2acdd;

  /* Ink */
  --ink: #171b42;
  --ink-soft: #5c6494;
  --ink-faint: #8f97c2;

  /* Lines and depth */
  --line: #dce1f7;
  --line-strong: #c3cbec;
  --ledge: rgba(23, 27, 66, 0.16);
  --shadow-sm: 0 2px 8px rgba(23, 27, 66, 0.06);
  --shadow-md: 0 10px 26px rgba(23, 27, 66, 0.09);
  --shadow-lg: 0 22px 48px rgba(23, 27, 66, 0.14);

  /* Shape */
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 26px;
  --r-pill: 999px;

  /* Rhythm — deliberately tight so more fits per screen */
  --gap-xs: 0.35rem;
  --gap-sm: 0.55rem;
  --gap: 0.85rem;
  --gap-lg: 1.25rem;
  --pad-card: 1rem 1.1rem;

  --rail-w: 244px;
  --topbar-h: 60px;
  --ease-pop: cubic-bezier(0.34, 1.56, 0.64, 1);
}

[data-theme="dark"] {
  --bg: #0c1130;
  --bg-tint-a: rgba(124, 77, 255, 0.16);
  --bg-tint-b: rgba(15, 185, 132, 0.12);
  --bg-tint-c: rgba(255, 176, 32, 0.1);
  --card: #182052;
  --card-soft: #131a44;
  --rail: #080c24;
  --rail-ink: #e6eaff;
  --rail-ink-soft: #98a2d6;

  --ink: #edf0ff;
  --ink-soft: #a7b0e0;
  --ink-faint: #7b85b8;

  --line: #2a3470;
  --line-strong: #3a468d;
  --ledge: rgba(0, 0, 0, 0.5);
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 10px 26px rgba(0, 0, 0, 0.36);
  --shadow-lg: 0 22px 48px rgba(0, 0, 0, 0.45);

  --sky: #6b8dff;
  --grape: #9c78ff;
  --bubble: #ff77a9;
  --sunny: #ffc154;
  --mint: #2ed3a0;
  --tangerine: #ff8f63;
  --berry: #f766b8;
  --ocean: #2bc0d1;
  --primary: var(--sky);
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
  background-color: var(--bg);
  background-image:
    radial-gradient(circle at 8% -4%, var(--bg-tint-a), transparent 42%),
    radial-gradient(circle at 96% 4%, var(--bg-tint-b), transparent 38%),
    radial-gradient(circle at 50% 108%, var(--bg-tint-c), transparent 46%);
  background-attachment: fixed;
  transition: background-color 0.3s ease, color 0.3s ease;
}

img,
video {
  max-width: 100%;
  display: block;
}

h1,
h2,
h3,
h4,
h5 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin: 0 0 0.4rem;
  color: var(--ink);
}

h1 {
  font-size: clamp(1.6rem, 3.2vw, 2.3rem);
}
h2 {
  font-size: clamp(1.25rem, 2.2vw, 1.6rem);
}
h3 {
  font-size: 1.12rem;
}
h4 {
  font-size: 1rem;
}

p {
  margin: 0 0 0.6rem;
}

a {
  color: var(--primary);
  text-decoration: none;
}

small {
  font-size: 0.78rem;
  color: var(--ink-soft);
}

:focus-visible {
  outline: 3px solid var(--sunny);
  outline-offset: 2px;
  border-radius: 6px;
}

::selection {
  background: var(--sunny);
  color: #171b42;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.container {
  width: min(1280px, 100%);
  margin: 0 auto;
}

.section {
  padding: 0.55rem 0;
}

/* `.container` itself carries no horizontal padding (it relies on centring
   once the viewport is wider than its 1280px cap) — invisible for a white
   card on the page's own background, but a full-bleed colored block (a
   gradient bar or tile) touching the edge with no gutter reads as a bug.
   `.section-pad` gives just those sections the same side gutter `.hero`
   already carries inline, without touching `.container` for every other
   page that never needed it. */
.section-pad > .container {
  padding: 0 1rem;
}

@media (max-width: 640px) {
  .section-pad > .container {
    padding: 0 0.7rem;
  }
}

/* --------------------------------------------------------------------------
   3. App shell — sidebar, topbar, content
   -------------------------------------------------------------------------- */

.app-shell {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: var(--rail-w);
  flex: 0 0 var(--rail-w);
  background: var(--rail);
  color: var(--rail-ink);
  padding: 0.9rem 0.7rem 0.7rem;
  position: sticky;
  top: 0;
  height: 100vh;
  /* On a phone the browser chrome eats part of 100vh, which pushed the log out
     button under the address bar. dvh measures what is actually visible; the vh
     line above stays as the fallback for anything that does not know dvh. */
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  z-index: 60;
  overflow: hidden;
}

.sidebar::after {
  /* a thin candy stripe so the rail is not a flat slab */
  content: "";
  position: absolute;
  inset: 0 0 0 auto;
  width: 3px;
  background: linear-gradient(180deg, var(--bubble), var(--sunny), var(--mint), var(--sky));
  opacity: 0.85;
}

.sidebar-brand {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.2rem 0.4rem 0.75rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

.sidebar-brand strong {
  display: block;
  font-family: var(--font-display);
  font-size: 0.98rem;
  color: #fff;
  line-height: 1.1;
}

.sidebar-brand small {
  color: var(--rail-ink-soft);
  font-size: 0.7rem;
}

.brand-mark {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 13px;
  font-size: 1.15rem;
  background: linear-gradient(140deg, var(--sunny), var(--tangerine));
  box-shadow: 0 4px 0 var(--tangerine-deep);
  transform: rotate(-6deg);
}

/* The rail is a column of three: brand, links, footer. Only the middle one is
   allowed to grow and only the middle one scrolls, so the wordmark stays at the
   top and the streak and log out button stay reachable at the bottom however
   many links a role happens to have.

   ``min-height: 0`` is the load-bearing line. A flex child defaults to
   min-height: auto, which means it refuses to shrink below its content — so
   ``overflow-y: auto`` never engaged, the nav grew to its natural height and
   pushed the footer off the bottom of the screen. */
.sidebar-nav {
  display: grid;
  gap: 0.2rem;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
  /* room for the scrollbar so it never sits on top of a link */
  padding-right: 0.15rem;
}

.sidebar-nav::-webkit-scrollbar {
  width: 6px;
}

.sidebar-nav::-webkit-scrollbar-track {
  background: transparent;
}

.sidebar-nav::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.2);
  border-radius: var(--r-pill);
}

.sidebar-nav:hover::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.34);
}

/* A hint that there is more below the fold, drawn only while the list really
   does overflow (app.js sets the flag after measuring). */
.sidebar-nav.is-scrollable {
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 1.6rem), transparent 100%);
  mask-image: linear-gradient(180deg, #000 calc(100% - 1.6rem), transparent 100%);
}

.sidebar-nav.is-scrollable.at-end {
  -webkit-mask-image: none;
  mask-image: none;
}

.sidebar-nav a {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 0.7rem;
  border-radius: var(--r-md);
  color: var(--rail-ink-soft);
  font-weight: 700;
  font-size: 0.88rem;
  transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease;
}

/* A group is a <details>, so the heading is its <summary>: a real control that
   opens and closes with a click or the space bar and needs no script to do it.
   Styled as a quiet label rather than a button, because it is a signpost first
   and a lever second. */
.nav-group {
  margin: 0.55rem 0 0.15rem;
}

.sidebar-nav > .nav-group:first-child {
  margin-top: 0;
}

.nav-group > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  padding: 0.3rem 0.7rem;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--rail-ink-soft);
  opacity: 0.7;
  list-style: none;
  transition: opacity 0.15s ease, background 0.15s ease;
}

/* Safari still draws its own triangle without this. */
.nav-group > summary::-webkit-details-marker {
  display: none;
}

.nav-group > summary::after {
  content: "▾";
  font-size: 0.7rem;
  transition: transform 0.18s ease;
}

.nav-group:not([open]) > summary::after {
  transform: rotate(-90deg);
}

.nav-group > summary:hover {
  opacity: 1;
  background: rgba(255, 255, 255, 0.06);
}

.nav-group > summary:focus-visible {
  outline: 2px solid var(--rail-ink);
  outline-offset: 2px;
  opacity: 1;
}

/* A closed group still says how much is inside, so nothing feels lost. It is
   only shown while closed — open, you can count the links yourself. */
.nav-count {
  margin-left: auto;
  padding: 0.05rem 0.35rem;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.12);
  font-size: 0.62rem;
  opacity: 0;
  transition: opacity 0.15s ease;
}

.nav-group:not([open]) .nav-count {
  opacity: 0.85;
}

.nav-links {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.sidebar-nav a .nav-ico {
  font-size: 1.05rem;
  width: 1.4rem;
  text-align: center;
  flex: 0 0 1.4rem;
}

.sidebar-nav a:hover {
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  transform: translateX(3px);
}

.sidebar-nav a.is-active {
  background: linear-gradient(100deg, var(--sky), var(--grape));
  color: #fff;
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3);
}

.sidebar-footer {
  flex: 0 0 auto;
  margin-top: auto;
  padding-top: 0.7rem;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  display: grid;
  gap: 0.5rem;
}

.rail-streak {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.65rem;
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.07);
  color: var(--rail-ink);
  font-size: 0.78rem;
  font-weight: 700;
}

.main-shell {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  min-height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem 1rem;
  background: color-mix(in srgb, var(--card) 82%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.topbar-title {
  min-width: 0;
  flex: 1;
}

.topbar-title h2 {
  margin: 0;
  font-size: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-shrink: 0;
}

.icon-btn {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  border-radius: 12px;
  font-size: 1.05rem;
  cursor: pointer;
  transition: transform 0.15s var(--ease-pop), background 0.18s ease;
}

.icon-btn:hover {
  background: var(--card-soft);
  transform: translateY(-2px);
}

.icon-btn:active {
  transform: translateY(0);
}

/* Both are off by default and each is switched on by exactly one breakpoint:
   the hamburger below 860 where the rail is a drawer, the rail toggle between
   861 and 1024 where the rail is a column of icons. */
.mobile-menu-toggle,
.rail-toggle {
  display: none;
}

.user-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.7rem 0.3rem 0.3rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink);
  font-weight: 800;
  font-size: 0.78rem;
  max-width: 190px;
}

.user-badge .avatar {
  width: 26px;
  height: 26px;
  flex: 0 0 26px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(140deg, var(--grape), var(--bubble));
  color: #fff;
  font-family: var(--font-display);
  font-size: 0.8rem;
}

.user-badge span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.content-area {
  padding: 0.75rem 1rem 1.25rem;
  flex: 1;
  /* A flex item will not shrink below its own min-content width unless it is
     told to. Without this, one wide thing anywhere on the page — a long
     question, a table, a fieldset — sets a floor for the whole column, and the
     shell grows wider than the window instead of the content wrapping. That is
     what makes a page "go off the side of the screen": the page is genuinely
     wider than the screen, not merely clipped. */
  min-width: 0;
}

/* Mobile bottom tab bar */
.tabbar {
  display: none;
  position: fixed;
  inset: auto 0 0 0;
  z-index: 70;
  padding: 0.3rem 0.4rem calc(0.3rem + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--card) 94%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
}

.tabbar-inner {
  display: flex;
  justify-content: space-around;
  align-items: stretch;
  gap: 0.2rem;
}

.tabbar a {
  flex: 1;
  display: grid;
  justify-items: center;
  gap: 1px;
  padding: 0.32rem 0.2rem;
  border-radius: var(--r-md);
  color: var(--ink-soft);
  font-size: 0.63rem;
  font-weight: 800;
  line-height: 1.2;
}

.tabbar a .nav-ico {
  font-size: 1.15rem;
}

.tabbar a.is-active {
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 12%, transparent);
}

.scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(10, 14, 40, 0.5);
  backdrop-filter: blur(2px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.22s ease, visibility 0.22s ease;
}

.scrim.is-open {
  opacity: 1;
  visibility: visible;
}

/* --------------------------------------------------------------------------
   4. Public site chrome
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--card) 84%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.nav-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  padding: 0.55rem 1rem;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.02rem;
  color: var(--ink);
}

.nav {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.nav a {
  padding: 0.42rem 0.7rem;
  border-radius: var(--r-md);
  color: var(--ink-soft);
  font-weight: 700;
  font-size: 0.88rem;
  transition: background 0.18s ease, color 0.18s ease;
}

.nav a:hover {
  background: var(--card-soft);
  color: var(--ink);
}

.nav a.is-active {
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 12%, transparent);
}

.nav a.btn {
  color: #fff;
}

/* The public header has no rail to fall back on like the signed-in app
   shell — below 720px the link row would either overflow or wrap onto a
   second line, so it becomes a slide-in panel instead, mirroring the
   signed-in sidebar's mobile-open pattern but scoped to its own toggle. */
.public-nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--line);
  background: var(--card-soft);
  border-radius: var(--r-md);
  font-size: 1.05rem;
  color: var(--ink);
}

@media (max-width: 720px) {
  .public-nav-toggle {
    display: inline-flex;
  }

  .nav {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 60;
    height: 100vh;
    width: min(78vw, 300px);
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
    background: var(--card);
    box-shadow: var(--shadow-lg);
    padding: 4.2rem 1.1rem 1.5rem;
    transform: translateX(100%);
    transition: transform 0.22s ease;
    overflow-y: auto;
  }

  .nav.is-open {
    transform: translateX(0);
  }

  .nav a,
  .nav .icon-btn,
  .nav a.btn {
    width: 100%;
    text-align: left;
  }
}

.site-footer {
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--card) 70%, transparent);
  padding: 1rem;
  margin-top: 1.5rem;
}

.footer-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  color: var(--ink-soft);
  font-size: 0.85rem;
}

.footer-wrap p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   5. Buttons — the press-down ledge signature
   -------------------------------------------------------------------------- */

.btn {
  --btn-bg: var(--card);
  --btn-ledge: var(--line-strong);
  --btn-ink: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.55rem 1rem;
  border: none;
  border-radius: var(--r-md);
  background: var(--btn-bg);
  color: var(--btn-ink);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 0.86rem;
  line-height: 1.3;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--btn-ledge);
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.18s ease;
}

.btn:hover {
  filter: brightness(1.05);
}

.btn:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 var(--btn-ledge);
}

.btn-primary {
  --btn-bg: linear-gradient(140deg, var(--sky), var(--grape));
  --btn-ledge: var(--grape-deep);
  --btn-ink: #fff;
}

.btn-success {
  --btn-bg: linear-gradient(140deg, var(--mint), #34d9a6);
  --btn-ledge: var(--mint-deep);
  --btn-ink: #06301f;
}

.btn-fun {
  --btn-bg: linear-gradient(140deg, var(--sunny), var(--tangerine));
  --btn-ledge: var(--tangerine-deep);
  --btn-ink: #3a1d00;
}

.btn-secondary {
  --btn-bg: var(--card);
  --btn-ledge: var(--line-strong);
  --btn-ink: var(--ink);
  border: 1px solid var(--line);
}

.btn-danger {
  --btn-bg: #fff0ef;
  --btn-ledge: #f3b6b1;
  --btn-ink: var(--danger-deep);
}

[data-theme="dark"] .btn-danger {
  --btn-bg: #3a1512;
  --btn-ledge: #6b201a;
  --btn-ink: #ff9d95;
}

.btn-link {
  --btn-bg: transparent;
  --btn-ledge: transparent;
  --btn-ink: var(--primary);
  padding: 0.3rem 0;
  box-shadow: none;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.btn-link:active {
  transform: none;
}

.btn-sm {
  padding: 0.34rem 0.7rem;
  font-size: 0.78rem;
  box-shadow: 0 3px 0 var(--btn-ledge);
}

.btn-sm:active {
  transform: translateY(3px);
}

.btn-lg {
  padding: 0.7rem 1.4rem;
  font-size: 0.98rem;
}

.btn-block {
  width: 100%;
}

/* --------------------------------------------------------------------------
   6. Subject stickers, chips, badges
   -------------------------------------------------------------------------- */

.sticker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  border-radius: 13px;
  font-size: 1.2rem;
  background: var(--hue, var(--sky));
  box-shadow: 0 3px 0 var(--hue-deep, var(--sky-deep));
  transform: rotate(-5deg);
  transition: transform 0.25s var(--ease-pop);
}

.sticker-sm {
  width: 30px;
  height: 30px;
  flex: 0 0 30px;
  font-size: 0.92rem;
  border-radius: 10px;
}

.sticker-lg {
  width: 54px;
  height: 54px;
  flex: 0 0 54px;
  font-size: 1.6rem;
  border-radius: 17px;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.22rem 0.6rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.72rem;
  font-weight: 800;
  white-space: nowrap;
}

.chip-primary {
  background: color-mix(in srgb, var(--sky) 14%, transparent);
  border-color: color-mix(in srgb, var(--sky) 26%, transparent);
  color: var(--sky-deep);
}

.chip-success,
.free-tag {
  background: color-mix(in srgb, var(--mint) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--mint) 30%, transparent);
  color: var(--mint-deep);
}

.chip-warn,
.paid-tag {
  background: color-mix(in srgb, var(--sunny) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--sunny) 34%, transparent);
  color: var(--sunny-deep);
}

.chip-grape {
  background: color-mix(in srgb, var(--grape) 14%, transparent);
  border-color: color-mix(in srgb, var(--grape) 28%, transparent);
  color: var(--grape-deep);
}

[data-theme="dark"] .chip-primary,
[data-theme="dark"] .chip-success,
[data-theme="dark"] .chip-warn,
[data-theme="dark"] .chip-grape,
[data-theme="dark"] .free-tag,
[data-theme="dark"] .paid-tag {
  color: var(--ink);
}

.free-tag,
.paid-tag {
  display: inline-flex;
  padding: 0.2rem 0.55rem;
  border-radius: var(--r-pill);
  font-size: 0.7rem;
  font-weight: 800;
}

/* A scrollable row of filter pills — cheaper vertically than a select stack */
.chip-row {
  display: flex;
  gap: 0.35rem;
  overflow-x: auto;
  padding-bottom: 0.2rem;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}

/* The "showing Class 7" picker. This was a row of chips until it was pointed
   out that twenty-eight active classes is not a row, it is a horizontal
   scroller you have to drag through to reach your own year. One select says
   the same thing in one line, and the browser's own picker is better at
   twenty-eight options than anything built out of pills.

   Capped rather than stretched: a filter that spans the whole content column
   reads as the page's subject instead of as a control, and at 320px it has the
   line to itself anyway. */
.class-scope {
  display: flex;
  align-items: end;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

/* Doubled up with .field on purpose: .field is defined further down the
   stylesheet, so at equal specificity it would win the gap and the stacking. */
.field.class-scope-field {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 0.45rem;
  width: min(24rem, 100%);
}

.class-scope-label {
  font-family: var(--font-display);
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* The select is a grid item in a minmax(0, 1fr) track, so it can be narrower
   than its longest option ("Skill learners", "Postgraduates") instead of
   setting a floor for the row. */
.class-scope-field select {
  min-width: 0;
}

/* Present with scripting off, hidden by the auto-submit handler otherwise. It
   still needs a rule of its own so it lines up with the select's bottom edge
   rather than with the label above it. */
.class-scope-go {
  flex: 0 0 auto;
}

/* The catalog's compact header — title, the learner's own class as a small
   badge, and a two-segment "My class / Explore other classes" toggle that
   replaces the old full-width dropdown-plus-sentence block. Kept in
   `.page-head`'s existing flex row (title on the left, this on the right)
   rather than a new wrapper, so it collapses to a stacked column on mobile
   exactly the way `.page-head` already does. */
.compact-head {
  align-items: flex-start;
  min-width: 0;
}

.compact-head h1 {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.class-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent);
  padding: 0.15rem 0.55rem;
  border-radius: var(--r-pill);
}

.scope-toggle {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.15rem;
  min-width: 0;
  max-width: 100%;
  background: var(--card-soft);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 0.2rem;
}

/* Shared look for both segments — the plain "My class" link and the
   <details>'s own <summary> — so the two read as one control even though
   one navigates and the other only discloses. */
.scope-toggle-btn {
  border: 0;
  background: transparent;
  padding: 0.4rem 0.75rem;
  border-radius: var(--r-pill);
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
  cursor: pointer;
  white-space: nowrap;
  list-style: none;
}

.scope-toggle-btn::-webkit-details-marker {
  display: none;
}

.scope-toggle-btn:hover {
  color: var(--ink);
}

a.scope-toggle-btn.is-on {
  background: linear-gradient(135deg, var(--sky), var(--grape));
  color: #fff;
}

/* The open/closed state of the <details> is a CSS attribute selector away —
   no script has to track which segment is "active" for the disclosure. */
.scope-toggle-explore[open] > summary.scope-toggle-btn {
  background: linear-gradient(135deg, var(--sky), var(--grape));
  color: #fff;
}

/* This used to position the chip strip absolutely, anchored under the
   toggle with `right: 0`, so opening it would not push the rest of the
   page down. In practice that let a strip of twenty-eight classes escape
   past the right edge of the viewport on real desktop windows — the
   containing block a `right: 0` resolves against is not always the small
   pill it visually sits under, and there is no guarantee the box then has
   anywhere to shrink to. A plain in-flow reveal cannot make that mistake:
   it can only ever be as wide as `max-width` allows, and it grows the page
   downward rather than the toggle sideways, at the cost of the rest of the
   header shifting down while it is open — a fair trade for never running
   off-screen. `max-height` + `overflow-y: auto` is the direct answer to "a
   big list" specifically: twenty-eight classes wrap into several rows
   here, and past a comfortable height the box scrolls internally instead
   of just getting taller. */
.scope-toggle-explore .class-strip {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 0.3rem;
  margin-top: 0.5rem;
  padding: 0.6rem;
  max-width: min(100%, 26rem);
  max-height: 11rem;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}

.class-chip {
  font-size: 0.72rem;
  font-weight: 700;
  padding: 0.3rem 0.65rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  white-space: nowrap;
}

.class-chip:hover {
  border-color: var(--primary);
  color: var(--primary);
}

.class-chip.is-on {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.scope-widened-note {
  width: 100%;
}

/* The "Filters" popup button sits next to "Plans" in exactly the slot the
   old class-scope toggle used to occupy — one row, no extra chrome added. */
.filters-trigger {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  min-width: 0;
}

/* A <details> rather than a <dialog> so the whole thing works with no
   script at all: <summary> is natively focusable and answers to Enter,
   Space and click without a line of JS, and its own open/closed state is
   exactly what `[open]` below keys off. */
.filters-popup {
  position: relative;
}

.filters-popup-btn {
  list-style: none;
  cursor: pointer;
}

.filters-popup-btn::-webkit-details-marker {
  display: none;
}

/* One <summary> has to serve as both the closed-state "Filters" button and
   the open-state "Close" button, because a <details> only ever gets one —
   so the two labels are both always in the markup and CSS shows exactly
   one of them depending on `[open]`, rather than swapping text with JS. */
.filters-label-close {
  display: none;
}

.filters-popup[open] .filters-label-open {
  display: none;
}

.filters-popup[open] .filters-label-close {
  display: inline;
}

/* Closed, the panel does not exist as far as layout is concerned. Open, it
   becomes a full-viewport scrim with the actual card centred inside it —
   the same "float above everything, cannot be squeezed by a neighbour"
   approach the class-strip's floating version got into trouble with
   earlier is fine here on purpose, because this time the box is centred by
   the viewport itself (`position: fixed`, not anchored to a narrow sibling
   with `right: 0`), so there is nowhere for it to run off the edge of. */
.filters-panel {
  display: none;
}

.filters-popup[open] .filters-panel {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 300;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(15, 20, 40, 0.45);
}

/* The summary floats above the scrim once open, in the corner, so "✕
   Close" is always reachable without hunting for it inside the card. */
.filters-popup[open] .filters-popup-btn {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 301;
}

.filters-panel-card {
  width: min(26rem, 100%);
  max-height: 88vh;
  overflow-y: auto;
  background: var(--card);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  padding: 1.1rem 1.2rem;
}

.filters-panel-title {
  margin: 0 0 0.8rem;
  font-size: 1.05rem;
}

.filters-form {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.field-label-block {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}

.field-label-text {
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}

.filters-form input[type="search"] {
  width: 100%;
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--card-soft);
  font-size: 0.88rem;
  color: var(--ink);
}

.filters-form input[type="search"]:focus {
  outline: none;
  background: var(--card);
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}

.filters-form .scope-toggle {
  width: 100%;
  justify-content: stretch;
}

.filters-form .scope-toggle a.scope-toggle-btn {
  flex: 1;
  text-align: center;
}

.filters-form .scope-toggle-explore {
  flex: 1;
  min-width: 0;
}

.filters-form .scope-toggle summary.scope-toggle-btn {
  display: block;
  width: 100%;
  text-align: center;
}

.filters-panel-foot {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.2rem;
}

.filters-panel-foot .btn {
  flex: 1;
}

@media (max-width: 640px) {
  .compact-head {
    flex-direction: column;
    align-items: stretch;
  }

  .filters-trigger {
    width: 100%;
  }

  .filters-trigger .filters-popup {
    flex: 1;
  }

  .filters-popup-btn {
    display: block;
    width: 100%;
    text-align: center;
  }

  .filters-trigger > a.btn {
    flex: 1;
  }

  /* On a phone the panel becomes a bottom sheet instead of a centred card —
     more reachable with a thumb, and it does not have to fight the on-screen
     keyboard for headroom the way a vertically centred card would. */
  .filters-popup[open] .filters-panel {
    align-items: flex-end;
    padding: 0;
  }

  .filters-panel-card {
    width: 100%;
    max-height: 92vh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
  }
}

.chip-row::-webkit-scrollbar {
  height: 5px;
}

.chip-row::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: var(--r-pill);
}

/* Career-tree route filter: three <button>s styled as chips (`.chip` gives
   the shape, this adds the reset-button plumbing and the pressed state).
   Each carries its own route colour via `--route-colour` so the pressed chip
   matches the dot beside its label instead of the generic sky/grape chip
   gradient every other active chip on the site uses. */
.route-filter-btn {
  cursor: pointer;
  appearance: none;
  font: inherit;
  transition: transform 0.16s var(--ease-pop), border-color 0.16s ease, color 0.16s ease;
}

.route-filter-btn:hover {
  transform: translateY(-2px);
}

.route-filter-btn[aria-pressed="true"] {
  background: color-mix(in srgb, var(--route-colour, var(--sky)) 20%, var(--card));
  border-color: var(--route-colour, var(--sky));
  color: var(--ink);
  box-shadow: 0 2px 0 color-mix(in srgb, var(--route-colour, var(--sky)) 55%, transparent);
}

a.chip {
  cursor: pointer;
  transition: transform 0.16s var(--ease-pop), border-color 0.16s ease, color 0.16s ease;
}

a.chip:hover {
  transform: translateY(-2px);
  border-color: var(--primary);
  color: var(--primary);
}

.chip.is-on {
  background: linear-gradient(140deg, var(--sky), var(--grape));
  border-color: transparent;
  color: #fff;
  box-shadow: 0 3px 0 var(--grape-deep);
}

.chip.is-on:hover {
  color: #fff;
}

/* Most chips are a symbol and two words we wrote ourselves, so `nowrap` on
   .chip is right: they should never break across lines. A chip carrying a
   name we did not choose is the exception. "📚 Introduction to Computer
   Science and Programming" on a lab card is wider than the card, and because
   nowrap makes that width the card's minimum, on a phone it used to set the
   floor for the whole column and run the page off the right-hand edge. */
.chip-wrap {
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: start;
  max-width: 100%;
}

.eyebrow {
  display: block;
  margin: 0 0 0.1rem;
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--primary);
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.5rem 0;
}

.meta-row > span:not([class]) {
  display: inline-flex;
  padding: 0.2rem 0.55rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.72rem;
  font-weight: 700;
}

.quick-overview {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
}

.info-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.32rem 0.7rem;
  border-radius: var(--r-pill);
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: 0 2px 0 var(--line-strong);
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 800;
}

.success-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--mint) 16%, transparent);
  color: var(--mint-deep);
  font-weight: 800;
  font-size: 0.8rem;
}

[data-theme="dark"] .success-badge {
  color: var(--mint);
}

/* --------------------------------------------------------------------------
   7. Cards, panels, tiles
   -------------------------------------------------------------------------- */

.panel-box,
.course-card,
.list-box,
.narrow-box,
.form-box,
.stat-card,
.question-panel,
.result-box,
.certificate-box,
.hero-panel,
.stats-box,
.info-box {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}

.panel-box {
  padding: var(--pad-card);
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
  /* Several panel heads carry a search box or a filter beside the heading, and
     those have a minimum width of their own. Wrapping lets the control drop
     onto its own line on a narrow screen instead of pushing the panel — and
     with it the whole page — past the right-hand edge. */
  flex-wrap: wrap;
}

.panel-head h2,
.panel-head h3 {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 1.02rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.panel-head .chip {
  flex-shrink: 0;
}

/* Page header block */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  margin-bottom: 0.7rem;
}

.page-head h1 {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.page-head .lead {
  margin: 0.2rem 0 0;
}

.head-actions {
  display: flex;
  gap: 0.45rem;
  flex-wrap: wrap;
}

/* The Junior dashboard's head. A child is not scanning a page, they are
   looking for the one thing to press, so the headline is bigger and the
   actions get a line of their own from tablet width down rather than
   competing with it for the right-hand edge. */
.page-head-big h1 {
  font-size: clamp(1.5rem, 3.4vw, 2.2rem);
  line-height: 1.1;
}

.page-head-big .head-actions {
  gap: 0.6rem;
}

@media (max-width: 1024px) {
  .page-head-big .head-actions {
    width: 100%;
  }

  .page-head-big .head-actions .btn {
    flex: 1 1 12rem;
    justify-content: center;
  }
}

.lead {
  color: var(--ink-soft);
  font-size: 0.95rem;
  max-width: 62ch;
}

.breadcrumbs {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-bottom: 0.35rem;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--ink-faint);
  flex-wrap: wrap;
}

.breadcrumbs a {
  color: var(--ink-soft);
}

.breadcrumbs a:hover {
  color: var(--primary);
}

/* Stat tiles — press-down ledge in the tile's own hue */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.6rem;
}

.stat-card {
  position: relative;
  padding: 0.7rem 0.8rem;
  border-radius: var(--r-md);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 24%, var(--line));
  background: linear-gradient(155deg, color-mix(in srgb, var(--hue, var(--sky)) 13%, var(--card)), var(--card) 78%);
  box-shadow: 0 4px 0 color-mix(in srgb, var(--hue, var(--sky)) 32%, transparent);
  display: grid;
  gap: 0.1rem;
  transition: transform 0.2s var(--ease-pop), box-shadow 0.2s ease;
}

.stat-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 7px 0 color-mix(in srgb, var(--hue, var(--sky)) 32%, transparent);
}

/* Some stat cards go somewhere. The card already lifts on hover, so all a
   linked one needs is to stop inheriting link blue and an underline that
   would cut through the number. */
a.stat-card {
  color: inherit;
  text-decoration: none;
}

.stat-card .game-label,
.stat-card .tile-label {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.stat-card strong {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 2.6vw, 1.85rem);
  line-height: 1.05;
  color: var(--hue-deep, var(--sky-deep));
}

[data-theme="dark"] .stat-card strong {
  color: var(--hue, var(--sky));
}

.stat-card > span:last-child {
  font-size: 0.74rem;
  color: var(--ink-soft);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   Streak, level and weekly goal

   Three cards that answer three different questions — am I keeping it up, how
   far have I come, and am I on track this week — so they sit side by side
   rather than stacking into one long scroll. Each takes its own hue so the eye
   can tell them apart at a glance without reading the headings.
   -------------------------------------------------------------------------- */
.goal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: var(--gap);
  margin-bottom: var(--gap);
}

.goal-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: var(--pad-card);
  border-radius: var(--r-lg);
  background: linear-gradient(160deg, color-mix(in srgb, var(--hue, var(--sky)) 14%, var(--card)), var(--card) 72%);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 26%, var(--line));
  box-shadow: var(--shadow-sm);
}

.goal-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.goal-head div {
  display: grid;
  gap: 0.1rem;
  min-width: 0;
}

.goal-head strong {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--ink);
  line-height: 1.2;
}

.goal-head span {
  font-size: 0.75rem;
  line-height: 1.35;
}

.goal-ico {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-md);
  background: var(--card);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 30%, var(--line));
  font-size: 1.15rem;
}

/* Seven days, oldest on the left, so the run reads left to right like a
   sentence. A missed day stays visible rather than disappearing — the gap is
   the point. */
.streak-week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.3rem;
  list-style: none;
  margin: 0.1rem 0 0;
  padding: 0;
}

.streak-day {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border: 1px solid transparent;
  color: var(--ink-faint);
  font-family: var(--font-display);
  font-size: 0.7rem;
  line-height: 1;
}

.streak-day.is-on {
  background: var(--hue-deep, var(--hue, var(--sky)));
  color: #fff;
  box-shadow: var(--shadow-sm);
}

.streak-day.is-today {
  border-color: var(--hue-deep, var(--hue, var(--sky)));
}

/* A month of days, same square as the weekly streak so the two read as one
   idea at two zoom levels. Fifteen across leaves two tidy rows for 30 days. */
.attend-grid {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: 0.28rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (max-width: 560px) {
  .attend-grid {
    grid-template-columns: repeat(10, 1fr);
  }
}

/* The child cards sit in a plain card-grid, but each one carries a stack of
   labelled bars, so it needs the same bottom-anchored action row the step
   cards use — otherwise cards of different heights float their buttons. */
.child-card .row-actions {
  margin-top: auto;
  padding-top: var(--gap-xs);
}

.child-card {
  display: flex;
  flex-direction: column;
}

.child-card .panel-head h2 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.goal-line {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
}

.goal-line-label {
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
}

.goal-line-value {
  font-family: var(--font-display);
  font-size: 0.78rem;
  color: var(--ink);
  white-space: nowrap;
}

/* Folded away by default: most weeks nobody changes their target, and an open
   form would cost more height than the card it lives in. */
.goal-edit {
  margin-top: auto;
  padding-top: 0.4rem;
  border-top: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 22%, var(--line));
}

.goal-edit > summary {
  cursor: pointer;
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--ink-soft);
  list-style: none;
}

.goal-edit > summary::-webkit-details-marker {
  display: none;
}

.goal-edit > summary::before {
  content: "⚙️ ";
}

.goal-edit > summary:hover {
  color: var(--ink);
}

.goal-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-top: 0.55rem;
}

.goal-form button {
  grid-column: 1 / -1;
}

@media (max-width: 420px) {
  .goal-form {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   Next steps

   Three cards that each answer a different question, so each wears its own
   colour and states its job in the eyebrow before naming a course. Without
   that label they read as three suggestions of equal weight, which is exactly
   what a plain recommendation list already is.
   -------------------------------------------------------------------------- */
.step-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--gap-sm);
}

.step-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.8rem 0.85rem;
  border-radius: var(--r-lg);
  background: linear-gradient(165deg, color-mix(in srgb, var(--hue, var(--sky)) 15%, var(--card)), var(--card) 70%);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 28%, var(--line));
}

.step-kind {
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--hue-deep, var(--hue, var(--sky)));
}

.step-card h3 {
  margin: 0;
  font-size: 0.98rem;
  line-height: 1.25;
}

.step-why {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--ink-soft);
}

.step-card .btn {
  margin-top: auto;
}

/* Small hue-tinted panel used inside grids */
.tile {
  display: grid;
  gap: 0.3rem;
  padding: 0.65rem 0.75rem;
  border-radius: var(--r-md);
  background: linear-gradient(155deg, color-mix(in srgb, var(--hue, var(--sky)) 10%, var(--card)), var(--card) 80%);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 22%, var(--line));
}

.tile strong {
  font-family: var(--font-display);
  font-size: 0.95rem;
  color: var(--ink);
}

.tile span {
  font-size: 0.76rem;
}

/* A quietly tinted band that separates full-width sections */
.muted-section {
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--primary) 6%, transparent));
  border-radius: var(--r-xl);
}

/* Course cards */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
  gap: 0.7rem;
}

.course-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.85rem 0.9rem 0.9rem;
  overflow: hidden;
  transition: transform 0.22s var(--ease-pop), box-shadow 0.22s ease;
}

.course-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 5px;
  background: var(--hue, var(--sky));
}

.course-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
}

.course-card:hover .sticker {
  transform: rotate(6deg) scale(1.08);
}

.card-top {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-top: 0.2rem;
}

.card-top h3 {
  margin: 0;
  font-size: 1rem;
  line-height: 1.25;
}

.card-top .course-tag {
  display: block;
  font-size: 0.7rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--hue-deep, var(--sky-deep));
}

[data-theme="dark"] .card-top .course-tag {
  color: var(--hue, var(--sky));
}

.course-card p {
  margin: 0;
  font-size: 0.83rem;
  color: var(--ink-soft);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: auto;
  padding-top: 0.55rem;
}

.price-tag {
  font-family: var(--font-display);
  font-size: 1.05rem;
  color: var(--ink);
}

/* Generic list rows */
.list-box {
  padding: 0.45rem 0.6rem;
  display: grid;
  gap: 0;
  /* An implicit `auto` column is as wide as its widest row, so one long line
     stretches the list and everything around it. Naming the column minmax(0,
     1fr) lets a row be narrower than its content and wrap inside instead. */
  grid-template-columns: minmax(0, 1fr);
}

.list-item {
  display: grid;
  gap: 0.1rem;
  padding: 0.5rem 0.15rem;
  border-bottom: 1px solid var(--line);
}

.list-item:last-child {
  border-bottom: none;
}

.list-item strong {
  font-weight: 800;
  font-size: 0.88rem;
}

.list-item span,
.list-item small {
  color: var(--ink-soft);
  font-size: 0.76rem;
}

.row-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.15rem;
  border-bottom: 1px solid var(--line);
  min-width: 0;
}

/* A row holding a whole marking form is many times taller than the avatar
   beside it, and centring strands the initials halfway down an empty column. */
.row-item:has(form.stack-sm) {
  align-items: flex-start;
}

/* Rows carrying a picture or video get taller, so top-align them instead. */
.row-item:has(.q-media) {
  align-items: flex-start;
}

.row-item:has(.q-media) .q-media {
  max-width: 260px;
  margin-top: 0.4rem;
  margin-bottom: 0;
}

.row-item:last-child {
  border-bottom: none;
}

.row-body {
  min-width: 0;
  flex: 1;
  display: grid;
  gap: 0.05rem;
}

.row-body strong {
  font-size: 0.88rem;
  font-weight: 800;
  /* This used to say overflow:hidden + text-overflow:ellipsis, which does
     nothing without white-space:nowrap and quietly clipped a long word instead.
     A question is worth reading in full, so it wraps — and a word with no
     spaces in it (a formula, a URL) breaks rather than running off the edge. */
  overflow-wrap: anywhere;
}

.row-body small {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.row-end {
  flex-shrink: 0;
  text-align: right;
  display: grid;
  gap: 0.1rem;
}

.row-end strong {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--primary);
}

.row-actions {
  display: flex;
  gap: 0.35rem;
  flex-shrink: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.row-actions form.row-delete-form {
  display: contents;
}

/* Per-row bulk-select checkbox (admin_students.html and similar list pages).
   flex-shrink: 0 so a long name never squeezes it down to nothing on a
   narrow screen. */
.row-select {
  flex-shrink: 0;
}

/* Toolbar that appears once at least one row is ticked. Reuses .qb-check's
   checkbox styling (question-bank bulk actions) rather than a third set of
   checkbox rules, but its own flex layout since it only ever holds three
   things -- a select-all, a count, and one button -- not the question
   bank's exam-picker column. */
.bulk-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
}

/* A class selector setting its own `display` always beats the browser's
   default [hidden]{display:none} rule, no matter the specificity math --
   author CSS outranks the user-agent stylesheet outright. Without this line
   the JS toggle `bulkBar.hidden = ...` would have zero visible effect and
   the bar would render permanently open. */
.bulk-bar[hidden] {
  display: none;
}

.bulk-bar .chip {
  margin-right: auto;
}

/* Obvious feedback that a row is part of the batch -- the checkbox alone is
   easy to miss, especially right after "Select all" ticks two hundred of
   them at once with no other visual change to show for it. */
.row-item.is-picked {
  background: color-mix(in srgb, var(--primary) 8%, transparent);
  border-radius: var(--r-sm);
}

.scroll-y {
  max-height: 320px;
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 0.2rem;
}

.scroll-y-sm {
  max-height: 220px;
}

.scroll-y-lg {
  max-height: 460px;
}

/* Two ways for a list to stop being a fixed-height box.

   .list-full is for a list whose length the person chose — a practice sheet of
   30 questions. A scrollbar inside a section they sized themselves is just in
   the way, so the section is exactly as tall as its questions and the page
   scrolls, once.

   .scroll-y-fit is for a list nobody sized — the whole question bank in an
   assessment picker. It grows with its content like .list-full, but stops at
   most of the window so the buttons underneath it stay reachable. Short list:
   no scrollbar at all. Long list: it scrolls inside, as before. */
.list-full {
  max-height: none;
  overflow-y: visible;
}

.scroll-y-fit {
  max-height: 70vh;
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 0.2rem;
}

/* Empty states — an invitation, not a shrug */
.empty {
  display: grid;
  justify-items: center;
  gap: 0.2rem;
  padding: 1.2rem 0.8rem;
  text-align: center;
  color: var(--ink-soft);
}

.empty .empty-ico {
  font-size: 1.7rem;
}

.empty strong {
  font-family: var(--font-display);
  color: var(--ink);
  font-size: 0.95rem;
}

.empty p {
  margin: 0;
  font-size: 0.8rem;
}

/* Medal ranks for leaderboards */
.rank {
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-family: var(--font-display);
  font-size: 0.82rem;
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
}

.rank-1 {
  background: linear-gradient(140deg, #ffd75e, var(--sunny));
  border-color: var(--sunny-deep);
  color: #4a3000;
}
.rank-2 {
  background: linear-gradient(140deg, #e7ecf7, #c2ccdf);
  border-color: #a8b3c9;
  color: #35405a;
}
.rank-3 {
  background: linear-gradient(140deg, #f2b98a, #d98d52);
  border-color: #b56f38;
  color: #4a2607;
}

/* --------------------------------------------------------------------------
   8. Tabs — the main tool for cutting scroll
   -------------------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: 0.3rem;
  padding: 0.25rem;
  margin-bottom: 0.7rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
}

.tabs::-webkit-scrollbar {
  display: none;
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.42rem 0.8rem;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: 0.83rem;
  font-weight: 800;
  white-space: nowrap;
  cursor: pointer;
  scroll-snap-align: start;
  transition: background 0.18s ease, color 0.18s ease, transform 0.18s var(--ease-pop);
}

.tab:hover {
  background: var(--card-soft);
  color: var(--ink);
}

.tab[aria-selected="true"] {
  background: linear-gradient(140deg, var(--sky), var(--grape));
  color: #fff;
  box-shadow: 0 3px 0 var(--grape-deep);
  transform: translateY(-1px);
}

.tab-panel[hidden] {
  display: none;
}

.tab-panel {
  animation: panel-in 0.28s ease both;
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   9. Grids
   -------------------------------------------------------------------------- */

.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.7rem;
}

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.7rem;
}

.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.7rem;
}

.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0.7rem;
}

.two-column {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 0.7rem;
}

.split-rail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 0.8rem;
  align-items: start;
}

/* The rail beside the main column. Pinned, so the lab checklist and the course
   contents stay beside your work while you scroll it.

   The cap is the whole point. `position: sticky` on an element taller than the
   window pins it at the top and then never moves it again, which means its
   lower panels cannot be reached at all — on the lab page that is Check
   yourself, What you need, Done looks like and Stuck?, four panels out of five.
   Capping it at the window and giving it its own scrollbar is what makes them
   reachable, and it costs nothing on the pages where the rail is short: no
   overflow, no scrollbar, no visible change. */
.sticky-rail {
  position: sticky;
  top: calc(var(--topbar-h) + 0.6rem);
  max-height: calc(100vh - var(--topbar-h) - 1.2rem);
  overflow-y: auto;
  /* Reaching the end of the rail should not then start scrolling the page
     underneath it — that is how you lose your place in both at once. */
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* overflow-y: auto computes overflow-x to auto as well, so the cards' press-
     down ledge and focus rings need somewhere to land or they get clipped. */
  padding: 2px 4px;
  margin: -2px -4px;
}

/* --------------------------------------------------------------------------
   10. Forms
   -------------------------------------------------------------------------- */

.form-box {
  padding: 0.9rem 1rem;
  display: grid;
  gap: 0.7rem;
}

.form-box label,
.field {
  display: grid;
  gap: 0.25rem;
  font-size: 0.8rem;
  font-weight: 800;
  color: var(--ink);
  min-width: 0;
}

.form-box input,
.form-box select,
.form-box textarea,
.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--card-soft);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.88rem;
  font-weight: 600;
  transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}

.form-box textarea,
.field textarea {
  resize: vertical;
  min-height: 62px;
}

/* Plain disclosure summaries — "Read what you sent", "What they handed in" —
   sit inside cards rather than in a fold component, so they get the link
   treatment instead of the browser's default black triangle text. */
.panel-box > details > summary,
.row-body > details > summary {
  cursor: pointer;
  padding: 0.2rem 0;
  font-weight: 800;
  font-size: 0.84rem;
  color: var(--primary);
}

.panel-box > details[open] > summary,
.row-body > details[open] > summary {
  margin-bottom: 0.3rem;
}

.form-box input::placeholder,
.form-box textarea::placeholder {
  color: var(--ink-faint);
  font-weight: 500;
}

.form-box input:focus,
.form-box select:focus,
.form-box textarea:focus,
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  background: var(--card);
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem;
}

.form-grid-3 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.6rem;
}

.fieldset {
  display: grid;
  gap: 0.55rem;
  padding: 0.75rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
}

.fieldset > .fieldset-title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: var(--ink);
}

.form-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
}

/* Only the padding lives here. The layout itself is set once, further down,
   next to the note that explains it — two competing display values for the
   same class is how a breakpoint ends up pointing at nothing. */
.filter-bar {
  padding: 0.7rem 0.8rem;
}

.search-field {
  position: relative;
}

.search-field input {
  padding-left: 2rem;
}

.search-field::before {
  content: "🔎";
  position: absolute;
  left: 0.6rem;
  bottom: 0.5rem;
  font-size: 0.85rem;
  pointer-events: none;
  opacity: 0.6;
}

/* Choice rows for quizzes and assessments */
.choice-row,
.checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.3rem 0;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
  font-weight: 600;
  font-size: 0.87rem;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s var(--ease-pop);
  /* An option can be a whole sentence. Let the row shrink to its column and let
     the text inside it wrap, instead of the row insisting on one long line. */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The tickbox and the letter badge keep their size (they set flex-shrink
   themselves); the answer text is the part that gives way. */
.choice-row > span:not(.opt-key),
.checkbox-row > span:not(.opt-key) {
  min-width: 0;
}

.choice-row:hover,
.checkbox-row:hover {
  border-color: var(--primary);
  transform: translateX(3px);
}

.choice-row input,
.checkbox-row input {
  accent-color: var(--primary);
  width: 17px;
  height: 17px;
  flex-shrink: 0;
}

.choice-row:has(input:checked),
.checkbox-row:has(input:checked) {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 12%, transparent);
}

.error-message {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.7rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
  color: var(--danger-deep);
  font-weight: 700;
  font-size: 0.85rem;
}

[data-theme="dark"] .error-message {
  color: #ff9d95;
}

.small-note {
  color: var(--ink-soft);
  font-size: 0.76rem;
  margin: 0.3rem 0 0;
}

.alert {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  font-weight: 700;
  font-size: 0.86rem;
}

.alert.success {
  background: color-mix(in srgb, var(--mint) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--mint) 30%, transparent);
  color: var(--mint-deep);
}

.alert.warning {
  background: color-mix(in srgb, var(--sunny) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--sunny) 34%, transparent);
  color: var(--sunny-deep);
}

[data-theme="dark"] .alert.success {
  color: var(--mint);
}
[data-theme="dark"] .alert.warning {
  color: var(--sunny);
}

/* --------------------------------------------------------------------------
   11. Progress, charts, tables
   -------------------------------------------------------------------------- */

.progress-bar {
  width: 100%;
  height: 9px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 9%, transparent);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--sky), var(--grape));
  transition: width 0.9s var(--ease-pop);
}

.progress-fill.accent {
  background: linear-gradient(90deg, var(--mint), #4ee0b4);
}

.progress-fill.hot {
  background: linear-gradient(90deg, var(--sunny), var(--tangerine));
}

/* One-question-at-a-time exam popup (assessment.html). JS-only: with no JS,
   .quiz-scrim never gets added and .quiz-modal-active never gets applied, so
   every question shows at once in the plain in-page layout and the form
   still submits fine.

   This turns the page's own .narrow-box.wide INTO the popup card (adding a
   class to it) rather than nesting it inside a second, differently-sized
   card -- .narrow-box.wide already carries its own width (860px), padding,
   background and border-radius, and a second box wrapped around it with its
   own width/padding fought that box instead of centring it, which is what
   was clipping the question and its Next/Submit buttons off-screen.
   z-index 500 is deliberately above everything else in this stylesheet
   (the highest other fixed element is the confetti burst at 999, which is
   celebratory and transient, not a competing piece of chrome) so no floating
   button or sticky bar can poke through an exam in progress. */
.quiz-scrim {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  z-index: 499;
}

.narrow-box.quiz-modal-active {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 500;
  width: min(680px, 94vw);
  max-height: 90vh;
  overflow-y: auto;
  scrollbar-width: thin;
  box-shadow: var(--shadow-lg, var(--shadow-md));
}

.quiz-modal-active .question-panel {
  border: none;
  padding: 0;
  margin: 0;
}

.quiz-progress-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  font-weight: 700;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.quiz-nav {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  margin-top: 0.9rem;
}

/* Review-before-submit step (assessment.html). Deliberately its own panel
   above the Submit button -- not a relabelled Next button sitting in the
   same tap-zone -- so a student who has been tapping the same spot on
   screen to advance can't land on Submit by habit. The number grid lets
   them jump straight back to any unanswered question before committing. */
.quiz-review-panel {
  border: 2px solid var(--mint, var(--sky));
  border-radius: var(--radius-md, 14px);
  padding: 1rem;
  margin-top: 1rem;
  background: color-mix(in srgb, var(--mint, var(--sky)) 10%, transparent);
}

.quiz-review-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.quiz-review-chip {
  min-width: 2.4rem;
  height: 2.4rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  border: none;
  font-weight: 800;
  font-size: 0.85rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--mint, #4ee0b4) 30%, transparent);
  color: var(--ink);
}

.quiz-review-chip.is-unanswered {
  background: color-mix(in srgb, var(--tangerine, #ff8a3d) 35%, transparent);
}

#quizSubmitBtn.quiz-submit-armed {
  background: linear-gradient(90deg, var(--mint, #4ee0b4), #2fb98f);
}

.progress-row {
  display: grid;
  gap: 0.2rem;
}

.progress-row .progress-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.73rem;
  color: var(--ink-soft);
  font-weight: 700;
}

/* Progress ring for lesson / course completion */
.ring {
  --pct: 0;
  width: 72px;
  height: 72px;
  flex: 0 0 72px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: conic-gradient(var(--sky) calc(var(--pct) * 1%), color-mix(in srgb, var(--ink) 10%, transparent) 0);
}

.ring::after {
  content: attr(data-label);
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  background: var(--card);
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: var(--ink);
}

.bar-chart {
  display: grid;
  gap: 0.4rem;
  margin-top: 0.4rem;
}

.bar-row {
  display: grid;
  grid-template-columns: minmax(70px, 88px) 1fr 46px;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.76rem;
}

.bar-row > span:first-child {
  color: var(--ink-soft);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar-row > strong:last-child {
  text-align: right;
  font-family: var(--font-display);
  font-size: 0.82rem;
  color: var(--ink);
}

.bar-track {
  height: 10px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}

.bar-fill {
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--sky), #74a0ff);
  transition: width 0.9s var(--ease-pop);
}

.bar-fill.accent {
  background: linear-gradient(90deg, var(--mint), #4ee0b4);
}

.scrollable-chart {
  max-height: 250px;
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 0.25rem;
}

.pagination-box {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
}

.pagination-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--ink-soft);
}

/* Three things share one line above the grid: the narrowing note + "Clear
   the filters" (left), how many courses are showing (centre), and the
   pager (right). The two outer columns are equal, empty flex tracks
   (`flex: 1 1 0`) rather than sized to their own content — that is what
   keeps the centre column visually centred on the row regardless of how
   long the sentence on the left is or whether the pager on the right is
   even there. Either outer column can render nothing at all and the middle
   still sits in the middle. */
.catalog-status-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0 0 0.5rem;
}

.catalog-status-text {
  min-width: 0;
  flex: 1 1 0;
}

.catalog-status-text p {
  margin: 0;
}

.catalog-status-tally {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.catalog-status-pagination {
  min-width: 0;
  flex: 1 1 0;
  display: flex;
  justify-content: flex-end;
}

.catalog-status-pagination .pagination-actions {
  justify-content: flex-end;
}

@media (max-width: 640px) {
  .catalog-status-row {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .catalog-status-text,
  .catalog-status-pagination {
    flex: 0 0 auto;
    width: 100%;
  }

  .catalog-status-pagination,
  .catalog-status-pagination .pagination-actions {
    justify-content: center;
  }
}

/* The category chip that used to be a whole section heading, now living on
   the card itself between the subject icon and the class tag — the same
   hue the card's own top border already carries, so the chip and the card
   agree about which subject this is without saying it twice in two colors. */
.chip-category {
  background: color-mix(in srgb, var(--hue, var(--sky)) 16%, var(--card));
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 32%, transparent);
  color: var(--hue-deep, var(--sky-deep));
}

[data-theme="dark"] .chip-category {
  color: var(--hue, var(--sky));
}

/* An empty-state card sitting directly in the flat grid (rather than inside
   its own section, as before) needs to claim every column itself or it
   renders as one narrow tile among empty tracks. */
.card-grid > .empty {
  grid-column: 1 / -1;
}

/* --------------------------------------------------------------------------
   12. Modal — the daily challenge
   -------------------------------------------------------------------------- */

.quiz-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.8rem;
}

.quiz-modal.hidden,
.hidden {
  display: none !important;
}

.quiz-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 14, 40, 0.62);
  backdrop-filter: blur(3px);
}

.quiz-modal-panel {
  position: relative;
  z-index: 1;
  width: min(620px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  padding: 1rem 1.1rem 1.1rem;
  border-radius: var(--r-xl);
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  animation: pop-in 0.3s var(--ease-pop) both;
}

@keyframes pop-in {
  from {
    opacity: 0;
    transform: translateY(18px) scale(0.96);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.quiz-modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.8rem;
  margin-bottom: 0.7rem;
}

.quiz-modal-header h3 {
  margin: 0;
  font-size: 1.2rem;
}

.close-quiz {
  width: 34px;
  height: 34px;
  flex: 0 0 34px;
  border: none;
  border-radius: 50%;
  background: var(--card-soft);
  color: var(--ink-soft);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.18s ease, transform 0.18s var(--ease-pop);
}

.close-quiz:hover {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger-deep);
  transform: rotate(90deg);
}

.quiz-status {
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.7rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--mint) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--mint) 28%, transparent);
  color: var(--mint-deep);
  font-weight: 800;
  font-size: 0.86rem;
}

.quiz-status.is-wrong {
  background: color-mix(in srgb, var(--sunny) 16%, transparent);
  border-color: color-mix(in srgb, var(--sunny) 32%, transparent);
  color: var(--sunny-deep);
}

[data-theme="dark"] .quiz-status {
  color: var(--mint);
}
[data-theme="dark"] .quiz-status.is-wrong {
  color: var(--sunny);
}

.quiz-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}

#quiz-question-text {
  margin: 0 0 0.7rem;
  font-size: clamp(1.05rem, 2.2vw, 1.32rem);
  line-height: 1.32;
}

.quiz-options {
  display: grid;
  gap: 0.42rem;
}

.option-btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  text-align: left;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.88rem;
  cursor: pointer;
  transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s var(--ease-pop);
}

.option-btn:hover:not(:disabled) {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 9%, transparent);
  transform: translateX(4px);
}

/* This used to be .option-btn .opt-key, so the same span inside a .choice-row —
   which is how the assessment, mock test, practice and lesson quiz pages write
   it — rendered as a bare letter with no badge around it. The badge belongs to
   the class, not to one of its two parents. */
.opt-key {
  width: 24px;
  height: 24px;
  flex: 0 0 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--card);
  border: 1px solid var(--line);
  font-family: var(--font-display);
  font-size: 0.8rem;
}

.option-btn.correct-option {
  background: color-mix(in srgb, var(--mint) 16%, transparent);
  border-color: var(--mint);
  color: var(--mint-deep);
  animation: nudge 0.4s ease;
}

.option-btn.wrong-option {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-color: var(--danger);
  color: var(--danger-deep);
  animation: shake 0.35s ease;
}

[data-theme="dark"] .option-btn.correct-option {
  color: var(--mint);
}
[data-theme="dark"] .option-btn.wrong-option {
  color: #ff9d95;
}

@keyframes nudge {
  0%,
  100% {
    transform: scale(1);
  }
  45% {
    transform: scale(1.03);
  }
}

@keyframes shake {
  0%,
  100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-5px);
  }
  75% {
    transform: translateX(5px);
  }
}

.quiz-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-top: 0.8rem;
}

.quiz-summary h4 {
  margin: 0;
  font-size: 1.15rem;
}

.quiz-summary-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: 0.5rem;
  margin: 0.6rem 0;
}

.score-pill {
  display: grid;
  gap: 0.05rem;
  padding: 0.55rem 0.6rem;
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px solid var(--line);
  text-align: center;
}

.score-pill strong {
  font-family: var(--font-display);
  font-size: 1.3rem;
  color: var(--hue, var(--primary));
}

.score-pill span {
  font-size: 0.68rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
}

.recap-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.3rem;
  max-height: 190px;
  overflow-y: auto;
}

.recap-list li {
  display: flex;
  align-items: flex-start;
  gap: 0.45rem;
  padding: 0.4rem 0.55rem;
  border-radius: var(--r-sm);
  background: var(--card-soft);
  font-size: 0.8rem;
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   13. Home hero and catalog
   -------------------------------------------------------------------------- */

.hero {
  padding: 1.4rem 1rem 0.8rem;
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: 1.4rem;
  align-items: center;
}

.hero-copy {
  min-width: 0;
}

.hero h1 {
  font-size: clamp(2rem, 5.2vw, 3.3rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0.3rem 0 0.5rem;
}

.hero h1 .hi-word {
  display: inline-block;
  padding: 0 0.35rem;
  border-radius: 12px;
  background: linear-gradient(140deg, var(--sunny), var(--tangerine));
  color: #3a1d00;
  transform: rotate(-2deg);
  box-shadow: 0 5px 0 var(--tangerine-deep);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.9rem;
}

/* The subject sticker sheet — the hero's thesis: what you can learn today */
.sticker-sheet {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.55rem;
  padding: 0.85rem;
  border-radius: var(--r-xl);
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-md);
}

.sheet-tile {
  display: grid;
  justify-items: center;
  gap: 0.2rem;
  padding: 0.7rem 0.4rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 24%, transparent);
  color: var(--ink);
  font-size: 0.72rem;
  font-weight: 800;
  text-align: center;
  transition: transform 0.24s var(--ease-pop);
}

.sheet-tile:hover {
  transform: translateY(-4px) rotate(-2deg);
}

.sheet-tile .sheet-ico {
  font-size: 1.5rem;
  line-height: 1;
}

/* The 70/20/10 learning model — a labelled proportion bar plus one card per
   band. The bar's three segments are flex-basis percentages, not a chart
   library, so it never needs JS and reflows for free at any width. */
.model-bar {
  display: flex;
  height: 34px;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  margin-bottom: 0.9rem;
}

.model-bar span {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 800;
  font-size: 0.8rem;
}

.model-seg-70 {
  flex: 70;
  background: linear-gradient(135deg, var(--mint), var(--mint-deep));
}

.model-seg-20 {
  flex: 20;
  background: linear-gradient(135deg, var(--sky), var(--sky-deep));
}

.model-seg-10 {
  flex: 10;
  background: linear-gradient(135deg, var(--sunny), var(--sunny-deep));
  color: #3a2900 !important;
}

.model-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.7rem;
}

/* Four highlight tiles for the things that make this platform different —
   parent/mentor monitoring, exam prep, question banks, weekly tests. Each
   is a solid gradient card (not hue-tinted like a subject tile) so this
   section reads as a distinct "why us" moment, not more subject browsing. */
.highlight-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.7rem;
}

.highlight-card {
  border-radius: var(--r-lg);
  padding: var(--pad-card);
  color: #fff;
  box-shadow: var(--shadow-sm);
}

.highlight-card .sticker {
  font-size: 1.35rem;
}

.highlight-card h3 {
  margin: 0.35rem 0 0.2rem;
  font-size: 0.92rem;
}

.highlight-card p {
  margin: 0;
  font-size: 0.78rem;
  opacity: 0.94;
}

.highlight-parent {
  background: linear-gradient(135deg, var(--berry), var(--bubble));
}

.highlight-exam {
  background: linear-gradient(135deg, var(--sky), var(--ocean));
}

.highlight-bank {
  background: linear-gradient(135deg, var(--grape), var(--sky));
}

.highlight-schedule {
  background: linear-gradient(135deg, var(--tangerine), var(--sunny));
  color: #3a2400;
}

/* Weekly test schedule strip — a standing rhythm, shown as a row of days
   rather than a paragraph, so "weekly" reads as something actually
   happening rather than a marketing claim. */
.week-strip {
  display: flex;
  gap: 0.5rem;
}

.day-chip {
  flex: 1;
  min-width: 0;
  text-align: center;
  padding: 0.55rem 0.3rem;
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px solid var(--line);
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.day-chip strong {
  display: block;
  font-size: 0.8rem;
  color: var(--ink);
  margin-bottom: 0.1rem;
}

.day-chip.is-on {
  border-color: var(--mint);
  background: color-mix(in srgb, var(--mint) 14%, var(--card-soft));
}

@media (max-width: 720px) {
  .highlight-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 640px) {
  .model-grid,
  .highlight-grid {
    grid-template-columns: 1fr;
  }

  .week-strip {
    flex-wrap: wrap;
  }

  .day-chip {
    flex: 1 1 calc(50% - 0.25rem);
  }
}

.catalog-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  margin: 0 0 0.5rem;
}

.catalog-header h2 {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 1.15rem;
}

.catalog-header span {
  color: var(--ink-soft);
  font-size: 0.78rem;
  font-weight: 700;
}

.catalog-section {
  margin-bottom: 1rem;
}

.mini-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.mini-list a {
  padding: 0.32rem 0.65rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.76rem;
  font-weight: 700;
  transition: transform 0.18s var(--ease-pop), border-color 0.18s ease;
}

.mini-list a:hover {
  transform: translateY(-2px);
  border-color: var(--primary);
  color: var(--primary);
}

/* Collapsible sections keep long pages short */
.fold {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  overflow: hidden;
}

.fold + .fold {
  margin-top: 0.5rem;
}

.fold > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.65rem 0.9rem;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 0.98rem;
  color: var(--ink);
  list-style: none;
}

.fold > summary::-webkit-details-marker {
  display: none;
}

.fold > summary::after {
  content: "▾";
  margin-left: auto;
  color: var(--ink-faint);
  transition: transform 0.22s ease;
}

.fold[open] > summary::after {
  transform: rotate(180deg);
}

.fold > summary:hover {
  background: var(--card-soft);
}

.fold-body {
  padding: 0 0.9rem 0.85rem;
}

/* Pricing */
.pricing-card {
  align-items: stretch;
}

.pricing-card.featured {
  border-color: var(--grape);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--grape) 22%, transparent), var(--shadow-md);
}

.price-block {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
  margin: 0.2rem 0 0.4rem;
}

.price-block strong {
  font-family: var(--font-display);
  font-size: 2rem;
  line-height: 1;
  color: var(--ink);
}

.price-block span {
  color: var(--ink-soft);
  font-size: 0.8rem;
  font-weight: 700;
}

.plan-features {
  list-style: none;
  margin: 0.4rem 0;
  padding: 0;
  display: grid;
  gap: 0.28rem;
}

.plan-features li {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.plan-features li::before {
  content: "✔";
  color: var(--mint);
  font-weight: 900;
  flex-shrink: 0;
}

.info-box {
  margin-top: 0.7rem;
  padding: 0.9rem 1rem;
}

.info-box h2 {
  font-size: 1.05rem;
}

.info-box p {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.86rem;
}

/* --------------------------------------------------------------------------
   14. Lessons, assessments, certificate
   -------------------------------------------------------------------------- */

.lesson-stage {
  display: grid;
  gap: 0.6rem;
}

.step-dots {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-bottom: 0.2rem;
}

.step-dot {
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.74rem;
  font-weight: 800;
  transition: transform 0.18s var(--ease-pop);
}

.step-dot:hover {
  transform: translateY(-2px);
}

.step-dot.is-current {
  background: linear-gradient(140deg, var(--sky), var(--grape));
  border-color: transparent;
  color: #fff;
  box-shadow: 0 3px 0 var(--grape-deep);
}

.step-dot.is-done {
  background: color-mix(in srgb, var(--mint) 20%, transparent);
  border-color: color-mix(in srgb, var(--mint) 38%, transparent);
  color: var(--mint-deep);
}

[data-theme="dark"] .step-dot.is-done {
  color: var(--mint);
}

.lesson-media {
  border-radius: var(--r-md);
  margin: 0.6rem 0;
  border: 1px solid var(--line);
}

/* An embedded song. The aspect ratio has to be pinned here because an iframe
   has no intrinsic size of its own — without it the video collapses to a
   150px letterbox in every browser. */
.lesson-embed {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: var(--r-md);
  background: #000;
}

.note-box {
  margin: 0.6rem 0;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  border-left: 4px solid var(--sunny);
  background: color-mix(in srgb, var(--sunny) 10%, transparent);
  font-size: 0.86rem;
}

.note-box strong {
  display: block;
  font-family: var(--font-display);
  color: var(--sunny-deep);
}

[data-theme="dark"] .note-box strong {
  color: var(--sunny);
}

.note-box p {
  margin: 0.15rem 0 0;
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   The Topic Template (interactive_course.html, Class 3 upward)
   -------------------------------------------------------------------------- */

/* The animated introduction: the same fade-and-rise `.tab-panel` already
   uses, on the step card itself. Every step here is a full page load, so a
   fresh element plays it every time with nothing to key or reset. */
.topic-intro {
  animation: panel-in 0.32s ease both;
}

@media (prefers-reduced-motion: reduce) {
  .topic-intro {
    animation: none;
  }
}

.topic-finish-banner {
  border-left-color: var(--mint, var(--sunny));
  background: color-mix(in srgb, var(--mint, var(--sunny)) 12%, transparent);
}

.topic-qr img {
  display: block;
  margin: 0 auto;
  border-radius: var(--r-md);
  background: #fff;
  padding: 0.4rem;
}

/* The Voice Settings panel -- injected by static/js/voice.js, once per
   page, right before the first "read this aloud" button. A native
   <details>/<summary> disclosure: no open/close script of its own to write
   or to get wrong. */
.voice-settings {
  margin: 0.5rem 0;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 0.5rem 0.7rem;
  background: var(--card-soft, var(--card));
}

.voice-settings summary {
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 0.9rem;
}

.voice-settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin-top: 0.55rem;
}

.voice-settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  font-size: 0.85rem;
}

.voice-settings-row input[type="range"],
.voice-settings-row select {
  max-width: 55%;
}

.voice-settings-check {
  justify-content: flex-start;
}

.voice-settings-check input {
  margin-right: 0.4rem;
}

/* Word-by-word highlighting, generalised from play.js's toddler version for
   an ordinary paragraph of body text. */
.voice-word.is-current {
  background: color-mix(in srgb, var(--sky, #3aa0ff) 22%, transparent);
  border-radius: 3px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--sky, #3aa0ff) 35%, transparent);
}

/* --------------------------------------------------------------------------
   The notebook: saved lessons, notes and highlights

   A note is the learner's own words and a highlight is the lesson's, so the
   two are drawn differently: a note sits on the page like paper, a highlight
   wears the marker stripe down its left edge. Same row in the database, two
   readings on screen.
   -------------------------------------------------------------------------- */
.note-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--gap-sm);
}

.note-item {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.65rem 0.75rem;
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px solid var(--line);
}

.note-item.is-highlight {
  border-left: 4px solid var(--hue-deep, var(--hue, var(--sunny)));
  background: color-mix(in srgb, var(--hue, var(--sunny)) 12%, var(--card));
}

.note-item p {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--ink);
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.note-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.35rem;
  border-top: 1px solid var(--line);
  font-size: 0.7rem;
}

.note-foot .text-soft {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* In the lesson sidebar the notes stack in a narrow column, so the grid
   collapses to one and the delete control tucks onto the same line. */
.sticky-rail .note-item {
  flex-direction: row;
  align-items: flex-start;
  gap: 0.5rem;
}

.sticky-rail .note-item p {
  flex: 1;
  font-size: 0.78rem;
}

.export-strip {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  flex-wrap: wrap;
}

.inline-form {
  display: contents;
}

/* --------------------------------------------------------------------------
   Reviews
   -------------------------------------------------------------------------- */
.rating-summary {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  flex-wrap: wrap;
  padding: 0.7rem 0.85rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sunny)) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--hue, var(--sunny)) 24%, var(--line));
}

.rating-score {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.rating-score strong {
  font-family: var(--font-display);
  font-size: 1.6rem;
  line-height: 1;
  color: var(--ink);
}

.rating-score .text-soft {
  font-size: 0.75rem;
}

.rating-summary p {
  flex: 1 1 220px;
}

.rating-stars {
  color: var(--sunny-deep);
  letter-spacing: 0.06em;
  font-size: 0.9rem;
}

[data-theme="dark"] .rating-stars {
  color: var(--sunny);
}

/* Five real radios, each showing the run of stars it stands for, so the choice
   is read rather than counted. Hovering half a star widget is a coin flip on a
   touch screen; a row you tap is not. */
.star-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  border: 0;
  margin: 0;
  padding: 0;
}

.star-option {
  cursor: pointer;
}

.star-option input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.star-option span {
  display: inline-block;
  padding: 0.24rem 0.55rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--card-soft);
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  font-size: 0.82rem;
  transition: transform 0.16s var(--ease-pop);
}

.star-option input:checked + span {
  border-color: var(--sunny-deep);
  background: color-mix(in srgb, var(--sunny) 18%, var(--card));
  color: var(--sunny-deep);
  transform: scale(1.04);
}

.star-option input:focus-visible + span {
  outline: 3px solid var(--sky);
  outline-offset: 2px;
}

[data-theme="dark"] .star-option input:checked + span {
  color: var(--sunny);
}

.lesson-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line);
}

.quiz-checkpoint {
  margin-top: 0.7rem;
  padding: 0.75rem 0.85rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--grape) 8%, var(--card-soft));
  border: 1px solid color-mix(in srgb, var(--grape) 20%, transparent);
}

.quiz-checkpoint h4 {
  margin: 0 0 0.4rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.question-panel {
  padding: 0.8rem 0.9rem;
  margin-bottom: 0.6rem;
  /* On the assessment and mock test pages this is a <fieldset>, and a fieldset
     defaults to min-inline-size: min-content — it will not shrink below its
     widest line for anybody. One long question then widens the paper, the
     column and the page. Nothing else here needs it, so it is safe to set for
     every question panel. */
  min-inline-size: 0;
}

.question-title {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-size: 0.98rem;
  font-weight: 700;
  /* The question sits between a number and a marks chip. Wrapping puts the
     chip on the next line rather than squeezing the question, and the two
     width rules matter because this is often a <legend>: a legend is sized to
     fit its own content and would otherwise set the fieldset's minimum width. */
  flex-wrap: wrap;
  width: 100%;
  min-width: 0;
  overflow-wrap: anywhere;
}

.question-title > .rank,
.question-title > .chip {
  flex-shrink: 0;
}

.question-title span {
  color: var(--ink-faint);
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 700;
}

.q-media {
  max-width: 420px;
  width: 100%;
  margin-bottom: 0.6rem;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
}

.result-hero {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 1rem 1.1rem;
}

.result-box {
  padding: 0.9rem 1rem;
}

.result-item p {
  margin: 0.2rem 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.result-item p strong {
  color: var(--ink);
}

.verdict {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--r-pill);
  font-size: 0.74rem;
  font-weight: 800;
}

.verdict.ok {
  background: color-mix(in srgb, var(--mint) 16%, transparent);
  color: var(--mint-deep);
}

.verdict.no {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger-deep);
}

[data-theme="dark"] .verdict.ok {
  color: var(--mint);
}
[data-theme="dark"] .verdict.no {
  color: #ff9d95;
}

.certificate-box {
  position: relative;
  text-align: center;
  padding: 1.8rem 1.4rem;
  border: 3px solid var(--sunny);
  background:
    radial-gradient(circle at 12% 10%, color-mix(in srgb, var(--sunny) 16%, transparent), transparent 30%),
    radial-gradient(circle at 88% 90%, color-mix(in srgb, var(--grape) 14%, transparent), transparent 32%),
    var(--card);
}

.certificate-box .cert-seal {
  font-size: 2.6rem;
  line-height: 1;
  margin-bottom: 0.3rem;
  display: inline-block;
  animation: float-seal 3.2s ease-in-out infinite;
}

@keyframes float-seal {
  0%,
  100% {
    transform: translateY(0) rotate(-4deg);
  }
  50% {
    transform: translateY(-7px) rotate(4deg);
  }
}

.certificate-box h1 {
  font-size: 1.5rem;
  margin: 0.2rem 0;
}

.certificate-box h2 {
  font-size: 1.9rem;
  margin: 0.5rem 0;
  color: var(--grape);
}

.certificate-box h3 {
  font-size: 1.15rem;
  margin: 0.3rem 0;
}

.certificate-box p {
  color: var(--ink-soft);
  font-size: 0.88rem;
  margin: 0.2rem 0;
}

.cert-code {
  font-family: var(--font-code);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
}

/* The same six facts the printed certificate carries, in the same order, so
   the screen is a preview of the PDF rather than a different document. */
.cert-facts {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 1px;
  margin: 1rem auto 0.7rem;
  padding: 0.6rem 0;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sunny)) 12%, transparent);
}

.cert-facts div {
  display: grid;
  gap: 0.1rem;
  padding: 0 0.3rem;
  border-left: 1px solid var(--card);
}

.cert-facts div:first-child {
  border-left: 0;
}

.cert-facts strong {
  font-size: 1.05rem;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.cert-facts span {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.cert-topics {
  max-width: 44ch;
  margin: 0 auto 0.6rem;
}

/* The filter row. A grid rather than a wrapping flex line, because a <select>
   contributes the width of its longest option to a flex layout: the Track
   filter is as wide as "Data science and analytics (12)", the Kind filter is
   as wide as "Both kinds", and eight of them side by side come out as a ragged
   staircase that wraps in a different place on every page. Equal columns line
   them up instead, and the count follows the width rather than the page.

   auto-fit, so a bar with two controls fills the row and a bar with eight
   makes two tidy rows of four; minmax(0, …) on the track so a long option can
   never set a floor and push the page sideways. */
.filter-bar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
  align-items: end;
  gap: var(--gap-sm);
}

/* .field is already min-width: 0, but the select inside it is not, and in a
   grid track it is the select's own minimum that would win. */
.filter-bar .field,
.filter-bar .field select,
.filter-bar .field input {
  min-width: 0;
}

/* The one control that earns more room: a search box the width of a Kind
   dropdown shows about four characters of what you typed. */
.filter-bar .search-field {
  grid-column: 1 / -1;
}

.filter-bar .btn,
.filter-bar .link-btn {
  align-self: end;
}

.filter-bar .link-btn {
  padding-bottom: 0.55rem;
  justify-self: start;
}

.cert-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--gap);
}

/* Each card is a shrunk version of the printed certificate — same accent, same
   double edge — so the shelf reads as a shelf of the real thing. */
.cert-card {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: var(--pad-card);
  border-radius: var(--r-lg);
  border: 2px solid color-mix(in srgb, var(--hue, var(--sunny)) 45%, transparent);
  outline: 1px solid color-mix(in srgb, var(--hue, var(--sunny)) 22%, transparent);
  outline-offset: -6px;
  background:
    radial-gradient(circle at 92% 6%, color-mix(in srgb, var(--hue, var(--sunny)) 14%, transparent), transparent 42%),
    var(--card);
  box-shadow: var(--shadow-sm);
  transition: transform 0.18s var(--ease-pop), box-shadow 0.18s ease;
}

.cert-card:hover,
.cert-card:focus-within {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
}

.cert-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
}

.cert-card-seal {
  font-size: 1.5rem;
  line-height: 1;
}

.cert-card-grade {
  display: inline-grid;
  place-content: center;
  min-width: 38px;
  height: 38px;
  padding: 0 0.4rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--hue, var(--sunny)) 18%, transparent);
  color: var(--hue-deep, var(--hue, var(--sunny)));
  font-size: 0.95rem;
  font-weight: 800;
}

.cert-card h2 {
  font-size: 1rem;
  margin-bottom: 0.1rem;
}

.cert-card p {
  font-size: 0.8rem;
  margin: 0;
}

.cert-card-actions {
  display: flex;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.5rem;
}

.cert-card-actions .btn {
  flex: 1;
}

@media (max-width: 620px) {
  .cert-facts {
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem 1px;
  }

  .cert-facts div:nth-child(4) {
    border-left: 0;
  }
}

.certificate-actions {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.9rem;
}

.narrow-box {
  width: min(560px, 100%);
  margin: 0 auto;
  padding: 1.1rem 1.2rem;
}

.narrow-box.wide {
  width: min(860px, 100%);
}

.auth-head {
  text-align: center;
  margin-bottom: 0.8rem;
}

.auth-head .sticker {
  margin: 0 auto 0.4rem;
}

.mini-link-row {
  margin: 0.2rem 0 0;
  text-align: center;
  font-size: 0.82rem;
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   14b. Signup wizard, learner segments, completion meter
   -------------------------------------------------------------------------- */

.form-alert {
  margin: 0 0 0.8rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--berry) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--berry) 36%, transparent);
  color: var(--berry-deep);
  font-size: 0.84rem;
  font-weight: 700;
}

[data-theme="dark"] .form-alert {
  color: var(--berry);
}

/* The one field register.html's JS points at after a server-side error
   (duplicate username/email, a still-missing field, a weak password) --
   outlined and auto-focused so the fix is obvious on the step itself,
   not just named in the banner up top. Clears the moment they start typing. */
.field-invalid {
  border-color: var(--berry) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--berry) 22%, transparent);
}

.form-note-good {
  margin: 0 0 0.8rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--mint) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--mint) 38%, transparent);
  color: var(--mint-deep);
  font-size: 0.84rem;
  font-weight: 700;
}

[data-theme="dark"] .form-note-good {
  color: var(--mint);
}

/* Same family as .form-note-good/.form-note-bad, the amber ("caution, but
   not an error") member -- used by admin_assessment_schedule.html and left
   undefined until verify_layout.py caught it, which meant the note rendered
   with no background, border or colour: plain text sitting where a callout
   should be. */
.form-note-warn {
  margin: 0 0 0.8rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--sunny) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--sunny) 34%, transparent);
  color: var(--sunny-deep);
  font-size: 0.84rem;
  font-weight: 700;
}

[data-theme="dark"] .form-note-warn {
  color: var(--ink);
}

.wizard {
  display: grid;
  gap: 0.9rem;
}

.wizard-meter {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.8rem 0.9rem;
  border-radius: var(--r-lg);
  background: var(--card-soft);
  border: 1px solid var(--line);
}

.wizard-meter-copy {
  display: grid;
  gap: 0.16rem;
  min-width: 0;
}

.wizard-meter-copy strong {
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1.2;
}

.wizard-meter-copy .text-soft {
  font-size: 0.8rem;
}

.step-rail {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.rail-step {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.32rem 0.62rem;
  border-radius: 999px;
  background: var(--card-soft);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.75rem;
  font-weight: 800;
  cursor: pointer;
  transition: transform 0.18s var(--ease-pop), background 0.18s ease;
}

.rail-step:hover {
  transform: translateY(-2px);
}

.rail-num {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink) 10%, transparent);
  font-size: 0.68rem;
}

.rail-step.is-active {
  background: linear-gradient(140deg, var(--sky), var(--grape));
  border-color: transparent;
  color: #fff;
  box-shadow: 0 3px 0 var(--grape-deep);
}

.rail-step.is-active .rail-num {
  background: rgba(255, 255, 255, 0.28);
}

.rail-step.is-done {
  background: color-mix(in srgb, var(--mint) 20%, transparent);
  border-color: color-mix(in srgb, var(--mint) 38%, transparent);
  color: var(--mint-deep);
}

[data-theme="dark"] .rail-step.is-done {
  color: var(--mint);
}

.wizard-step {
  display: grid;
  gap: 0.7rem;
  animation: stepIn 0.28s var(--ease-pop) both;
}

@keyframes stepIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

.step-heading {
  margin: 0;
  font-size: 1.15rem;
}

.step-heading + .small-note {
  margin-top: -0.5rem;
}

/* Learner segment picker ---------------------------------------------------- */

.segment-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem;
}

.segment-card {
  position: relative;
  display: block;
  cursor: pointer;
}

.segment-card input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.segment-body {
  display: grid;
  gap: 0.2rem;
  height: 100%;
  padding: 0.75rem 0.8rem;
  border-radius: var(--r-lg);
  background: var(--card);
  border: 2px solid var(--line);
  box-shadow: 0 3px 0 color-mix(in srgb, var(--ink) 8%, transparent);
  transition: transform 0.18s var(--ease-pop), border-color 0.18s ease, box-shadow 0.18s ease;
}

.segment-card:hover .segment-body {
  transform: translateY(-3px);
  border-color: var(--hue, var(--sky));
}

.segment-card input:focus-visible + .segment-body {
  outline: 3px solid var(--hue, var(--sky));
  outline-offset: 2px;
}

.segment-card input:checked + .segment-body {
  border-color: var(--hue, var(--sky));
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, var(--card));
  box-shadow: 0 4px 0 var(--hue-deep, var(--sky-deep));
}

.segment-ico {
  font-size: 1.7rem;
  line-height: 1;
}

.segment-body strong {
  font-family: var(--font-display);
  font-size: 1rem;
}

.segment-tag {
  font-size: 0.74rem;
  font-weight: 800;
  color: var(--hue-deep, var(--sky-deep));
}

.segment-blurb {
  font-size: 0.76rem;
  color: var(--ink-soft);
  line-height: 1.4;
}

/* Conditional fields -------------------------------------------------------- */

.field-slot {
  min-width: 0;
}

.field-slot[hidden] {
  display: none;
}

.field-slot-wide {
  grid-column: 1 / -1;
}

.field-label {
  display: block;
  margin-bottom: 0.24rem;
  font-size: 0.78rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.req-star {
  color: var(--berry);
  font-style: normal;
}

.req-star[hidden] {
  display: none;
}

/* Interest chips ------------------------------------------------------------ */

.pick-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.pick-chip {
  position: relative;
  cursor: pointer;
}

.pick-chip input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.pick-chip span {
  display: inline-block;
  padding: 0.34rem 0.7rem;
  border-radius: 999px;
  background: var(--card-soft);
  border: 1px solid var(--line);
  font-size: 0.78rem;
  font-weight: 700;
  transition: transform 0.16s var(--ease-pop), background 0.16s ease;
}

.pick-chip:hover span {
  transform: translateY(-2px);
}

.pick-chip input:focus-visible + span {
  outline: 3px solid var(--sky);
  outline-offset: 2px;
}

.pick-chip input:checked + span {
  background: linear-gradient(140deg, var(--sky), var(--grape));
  border-color: transparent;
  color: #fff;
  box-shadow: 0 3px 0 var(--grape-deep);
}

/* Master data rows ---------------------------------------------------------- */

/* Group, usage count and the tiny group/order editor share one line under the
   name, so a long master list still reads as a list rather than as forms. */
.master-form-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin-top: 0.3rem;
}

.master-form {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.master-form select,
.master-form input {
  padding: 0.2rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
}

.master-form input {
  width: 4.2rem;
}

/* A retired row is still shown here -- it is the only page where an admin
   needs to see what has been put away -- but it should not compete. */
.row-item.is-retired {
  opacity: 0.62;
}

/* Master-list pickers ------------------------------------------------------- */

.pick-set {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--card-soft);
  padding: 0.65rem 0.7rem 0.5rem;
  display: grid;
  gap: 0.55rem;
}

.pick-set-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
}

.pick-count {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--grape-deep);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
}

.pick-search {
  flex: 1 1 9rem;
  min-width: 0;
  padding: 0.32rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card);
  color: inherit;
  font: inherit;
  font-size: 0.8rem;
}

.pick-search:focus-visible {
  outline: 3px solid var(--sky);
  outline-offset: 1px;
}

.pick-section[hidden] {
  display: none;
}

.pick-section-name {
  margin: 0 0 0.3rem;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.pick-set .pick-grid {
  margin-bottom: 0.2rem;
}

.pick-chip[hidden] {
  display: none;
}

.pick-none {
  margin: 0;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.pick-none[hidden] {
  display: none;
}

/* Wizard navigation --------------------------------------------------------- */

.wizard-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  justify-content: flex-end;
  padding-top: 0.3rem;
  border-top: 1px dashed var(--line);
}

.wizard-nav > *[hidden] {
  display: none;
}

.btn-ghost {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--ink-soft);
  box-shadow: none;
}

.btn-ghost:hover {
  background: var(--card-soft);
  color: var(--ink);
}

.btn-ghost:active {
  transform: translateY(1px);
}

@media (max-width: 640px) {
  .wizard-meter {
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
  }

  .wizard-nav {
    justify-content: stretch;
  }

  .wizard-nav .btn {
    flex: 1 1 140px;
  }

  .segment-grid {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   14c. Completion meter, badges, referrals
   -------------------------------------------------------------------------- */

.completion-banner {
  display: flex;
  align-items: center;
  gap: 1rem;
  border-left: 5px solid var(--hue, var(--sky));
}

.completion-copy {
  display: grid;
  gap: 0.18rem;
  min-width: 0;
}

.completion-copy strong {
  font-family: var(--font-display);
  font-size: 1.05rem;
}

.completion-copy .text-soft {
  font-size: 0.82rem;
  line-height: 1.45;
}

.segment-grid-tight {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.45rem;
}

.segment-grid-tight .segment-body {
  padding: 0.55rem 0.6rem;
}

.segment-grid-tight .segment-ico {
  font-size: 1.3rem;
}

.segment-grid-tight .segment-body strong {
  font-size: 0.88rem;
}

.segment-grid-tight .segment-tag {
  font-size: 0.68rem;
}

/* Badge cards -------------------------------------------------------------- */

.badge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.5rem;
}

.badge-card {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.6rem 0.7rem;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--sky)) 13%, var(--card));
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 34%, transparent);
  box-shadow: 0 3px 0 color-mix(in srgb, var(--hue-deep, var(--sky-deep)) 30%, transparent);
  transition: transform 0.18s var(--ease-pop);
}

.badge-card:hover {
  transform: translateY(-3px);
}

.badge-card.is-locked {
  background: var(--card-soft);
  border-color: var(--line);
  box-shadow: none;
}

.badge-card.is-locked .badge-medal {
  filter: grayscale(1);
  opacity: 0.55;
}

.badge-medal {
  font-size: 1.7rem;
  line-height: 1.1;
}

.badge-body {
  display: grid;
  gap: 0.2rem;
  min-width: 0;
  flex: 1;
}

.badge-body strong {
  font-family: var(--font-display);
  font-size: 0.94rem;
  line-height: 1.2;
}

.badge-family {
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.badge-goal {
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.shelf-intro {
  margin: 0 0 0.7rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px dashed var(--line);
  font-size: 0.84rem;
  color: var(--ink-soft);
  line-height: 1.5;
}

.shelf-intro strong {
  color: var(--ink);
}

@media (max-width: 640px) {
  .completion-banner {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* --------------------------------------------------------------------------
   14d. Printables — download cards, printed-sheet cues
   -------------------------------------------------------------------------- */

/* Nine sheets have to fit in one panel without turning it into a scroll, so the
   cards are wide-and-short rather than tall, and the grid fills every column it
   can at 250px. */
.download-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: var(--gap-sm);
}

.download-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--line);
  border-left: 4px solid var(--hue, var(--sky));
  border-radius: var(--r-md);
  background: var(--card);
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  transition: transform 0.18s var(--ease-pop), box-shadow 0.18s ease,
    border-color 0.18s ease;
}

/* The dog-ear: a printed sheet's folded corner, drawn once with a gradient so
   it costs no extra markup. */
.download-card::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 18px;
  height: 18px;
  background: linear-gradient(
    225deg,
    var(--card-soft) 0 50%,
    color-mix(in srgb, var(--hue, var(--sky)) 30%, transparent) 50% 100%
  );
  border-bottom-left-radius: 4px;
}

.download-card:hover,
.download-card:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 45%, var(--line));
}

.download-ico {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--hue, var(--sky)) 14%, transparent);
  font-size: 1rem;
  line-height: 1;
}

.download-body {
  display: grid;
  gap: 0.14rem;
  min-width: 0;
}

.download-body strong {
  font-family: var(--font-display);
  font-size: 0.95rem;
  line-height: 1.2;
}

.download-body .text-soft {
  font-size: 0.76rem;
  line-height: 1.4;
}

.download-go {
  flex: none;
  align-self: center;
  margin-left: auto;
  padding-right: 0.15rem;
  color: var(--hue, var(--sky));
  font-weight: 900;
  transition: transform 0.18s var(--ease-pop);
}

.download-card:hover .download-go {
  transform: translateY(2px);
}

/* A button that reads as a link — used where the action changes what is on
   screen rather than navigating anywhere. */
.link-btn {
  padding: 0;
  border: none;
  background: none;
  color: var(--primary);
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 800;
  text-align: center;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.link-btn:hover {
  color: var(--grape);
}

/* --------------------------------------------------------------------------
   14e. Mock tests — mode picker, exam clock, score hero
   -------------------------------------------------------------------------- */

.mode-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--gap-sm);
  margin-bottom: 0.8rem;
}

.mode-card {
  display: grid;
  gap: 0.3rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  text-align: center;
  justify-items: center;
  transition: transform 0.18s var(--ease-pop), box-shadow 0.18s ease;
}

.mode-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.mode-ico {
  font-size: 1.5rem;
  line-height: 1;
}

.mode-card strong {
  font-family: var(--font-display);
  font-size: 1rem;
}

.mode-card .text-soft {
  font-size: 0.78rem;
  line-height: 1.4;
}

.mode-card .meta-row {
  justify-content: center;
}

.mode-card .btn {
  margin-top: 0.3rem;
}

/* The clock sticks to the top of the viewport so a learner never has to scroll
   back up to find out how long is left. */
.exam-bar {
  position: sticky;
  top: calc(var(--topbar-h) + 0.4rem);
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
  margin-bottom: 0.9rem;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}

.exam-clock {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
}

.exam-time {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--hue, var(--sky));
}

.exam-cap {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.exam-track {
  flex: 1;
  min-width: 120px;
  height: 8px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--hue, var(--sky)) 16%, transparent);
  overflow: hidden;
}

.exam-track span {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--hue, var(--sky));
  transition: width 1s linear;
}

.exam-facts {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.exam-bar.is-urgent .exam-time {
  color: var(--bubble);
  animation: exam-pulse 1s ease-in-out infinite;
}

.exam-bar.is-urgent .exam-track span {
  background: var(--bubble);
}

@keyframes exam-pulse {
  50% {
    opacity: 0.45;
  }
}

.score-hero {
  display: flex;
  align-items: center;
  gap: 1.2rem;
  flex-wrap: wrap;
  padding: var(--pad-card);
  margin-bottom: 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}

/* One conic gradient does the whole dial — no SVG, no canvas, and it reads
   correctly the moment the page paints even if scripts never run. */
.score-dial {
  flex: none;
  display: grid;
  place-content: center;
  justify-items: center;
  width: 116px;
  height: 116px;
  border-radius: 50%;
  background: conic-gradient(
    var(--hue, var(--sky)) calc(var(--pct, 0) * 1%),
    color-mix(in srgb, var(--hue, var(--sky)) 14%, transparent) 0
  );
  position: relative;
}

.score-dial::before {
  content: "";
  position: absolute;
  inset: 9px;
  border-radius: 50%;
  background: var(--card);
}

.score-dial strong,
.score-dial span {
  position: relative;
  z-index: 1;
}

.score-dial strong {
  font-family: var(--font-display);
  font-size: 1.7rem;
  line-height: 1;
  color: var(--hue, var(--sky));
}

.score-dial span {
  font-size: 0.74rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.score-copy {
  display: grid;
  gap: 0.35rem;
  min-width: 0;
}

.score-copy h1 {
  font-size: 1.5rem;
}

.row-item.is-right {
  border-left: 4px solid var(--mint);
}

.row-item.is-wrong {
  border-left: 4px solid var(--sunny);
}

/* --------------------------------------------------------------------------
   15. Motion, confetti, utilities
   -------------------------------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(12px);
}

.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.45s ease, transform 0.45s var(--ease-pop);
}

.confetti-piece {
  position: fixed;
  top: -12px;
  width: 9px;
  height: 14px;
  z-index: 999;
  pointer-events: none;
  border-radius: 2px;
  animation: confetti-fall linear forwards;
}

@keyframes confetti-fall {
  to {
    transform: translateY(105vh) rotate(720deg);
    opacity: 0;
  }
}

.to-top {
  position: fixed;
  right: 0.9rem;
  bottom: 0.9rem;
  z-index: 80;
  width: 42px;
  height: 42px;
  border: none;
  border-radius: 50%;
  background: linear-gradient(140deg, var(--sky), var(--grape));
  color: #fff;
  font-size: 1.1rem;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--grape-deep), var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity 0.24s ease, transform 0.24s var(--ease-pop), visibility 0.24s;
}

.to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.to-top:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 var(--grape-deep);
}

.no-results {
  display: none;
  padding: 1rem;
  text-align: center;
  color: var(--ink-soft);
  font-weight: 700;
}

.stack {
  display: grid;
  gap: 0.6rem;
}

.stack-sm {
  display: grid;
  gap: 0.35rem;
}

.row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.mt-0 {
  margin-top: 0;
}
.mt {
  margin-top: 0.6rem;
}
.mb-0 {
  margin-bottom: 0;
}
/* The mirror of .mt, which existed on its own for a long time. */
.mb {
  margin-bottom: 0.6rem;
}
.text-soft {
  color: var(--ink-soft);
}
.text-right {
  text-align: right;
}
.text-center {
  text-align: center;
}
.grow {
  flex: 1;
  min-width: 0;
}
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   16. Career — goals, interests and the aptitude check
   -------------------------------------------------------------------------- */

/* A role card is a small pitch: what the job is, what it pays, and three
   buttons to claim it. The buttons sit on an auto-margin top so every card in a
   row ends with them on the same line however long the summary runs. */
.role-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.role-card.is-chosen {
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 55%, var(--line));
  box-shadow: 0 5px 0 color-mix(in srgb, var(--hue, var(--sky)) 34%, transparent);
}

.role-card-head {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.role-card > p {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.5;
}

/* Pay is the thing learners scan for, so it gets its own tinted strip rather
   than being another line of body text they have to hunt through. */
.role-salary {
  display: grid;
  gap: 0.05rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sky)) 10%, var(--card-soft));
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 20%, var(--line));
}

.role-salary > span:first-child {
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.role-salary strong {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--hue-deep, var(--sky-deep));
}

[data-theme="dark"] .role-salary strong {
  color: var(--hue, var(--sky));
}

.role-pick {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: auto;
  padding-top: 0.45rem;
  border-top: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 20%, var(--line));
}

.role-pick .btn {
  flex: 1 1 auto;
}

/* Interest rating: 0 to 5 as a row of dots. Radios rather than a slider,
   because a slider gives no feedback about which value you landed on and is
   miserable on a phone. */
.interest-row {
  display: grid;
  gap: 0.5rem;
  align-content: start;
}

.interest-row legend {
  padding: 0;
}

.rate-scale {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.rate-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-size: 0.9rem;
  cursor: pointer;
  transition: transform 0.18s var(--ease-pop), background 0.18s ease, color 0.18s ease;
}

.rate-dot:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 45%, var(--line));
}

/* :has keeps the checked state on the label without any script. Browsers
   without it still show a working radio group, just without the fill. */
.rate-dot:has(input:checked),
.rate-dot.is-on {
  background: var(--hue-deep, var(--sky-deep));
  border-color: transparent;
  color: #fff;
  box-shadow: var(--shadow-sm);
}

.rate-dot:has(input:focus-visible) {
  outline: 3px solid var(--sky);
  outline-offset: 2px;
}

/* The constraints form reuses .mode-card as a radio tile, so it needs the same
   checked treatment the rating dots get. */
.mode-card:has(input:checked),
.mode-card.is-on {
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 55%, var(--line));
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, var(--card));
  box-shadow: var(--shadow-sm);
}

.mode-card:has(input:focus-visible) {
  outline: 3px solid var(--sky);
  outline-offset: 2px;
}

label.mode-card {
  cursor: pointer;
  font-size: 0.84rem;
  font-weight: 700;
}

.check-line {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.84rem;
  font-weight: 600;
  cursor: pointer;
}

.check-line input {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--sky-deep);
}

/* A long list of tick boxes — the course picker in the lab editor. Two columns
   rather than one because the list is names, and a name is short: one column
   would run a hundred courses down the page and put the Save button somewhere
   you have to go looking for. `auto-fill` rather than a fixed two, so it
   collapses to one column on a phone without a media query. */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.3rem 0.9rem;
  /* The children are flex rows that would otherwise refuse to shrink below
     their content, which is what pushes a grid sideways off the page. */
  min-width: 0;
}

.check-grid .check-line {
  min-width: 0;
}

/* Starter code and expected output. Monospace because whitespace is the
   content here — an indent that looks wrong in the box is an indent the
   learner will be handed. */
.code-input {
  font-family: var(--font-code);
  font-size: 0.8rem;
  line-height: 1.5;
  white-space: pre;
  overflow-x: auto;
}

/* Aptitude questions. Options are stacked full-width rather than in a grid so
   the eye travels straight down one column — comparing four answers is easier
   when they all start at the same x. */
.apt-question {
  border: none;
  border-top: 1px dashed var(--line);
  padding: 0.6rem 0 0;
  margin: 0;
}

.apt-question:first-of-type {
  border-top: none;
  padding-top: 0;
}

.apt-ask {
  padding: 0;
  margin-bottom: 0.45rem;
  font-family: var(--font-display);
  font-size: 0.95rem;
  line-height: 1.4;
}

.apt-options {
  display: grid;
  gap: 0.3rem;
}

.apt-option {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  font-size: 0.85rem;
  line-height: 1.4;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease;
}

.apt-option:hover {
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 45%, var(--line));
}

.apt-option:has(input:checked) {
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 60%, var(--line));
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, var(--card));
}

.apt-option input {
  margin-top: 0.15rem;
  accent-color: var(--sky-deep);
}

/* Skipping is a real answer here, so it gets a real option rather than being
   the absence of one — but a quiet one, so it never looks like the easy way out. */
.apt-skip {
  background: transparent;
  border-style: dashed;
  color: var(--ink-soft);
  font-size: 0.8rem;
}

/* The tree itself. A fixed height with the diagram scaled to fit inside it,
   rather than a box that grows with the graph — a path with forty nodes would
   otherwise push everything else off the screen. */
.career-graph {
  position: relative;
  min-height: 320px;
  max-height: 62vh;
  overflow: hidden;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background:
    radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--ink) 9%, transparent) 1px, transparent 0)
    0 0 / 22px 22px,
    var(--card-soft);
  display: grid;
  place-items: center;
}

.career-svg {
  width: 100%;
  height: 100%;
  min-height: 320px;
  cursor: grab;
  touch-action: none;
}

.career-svg:active {
  cursor: grabbing;
}

/* Edges sit behind the boxes and are never interactive — a click near a line
   should select the node it points at, not the line. */
.career-edges {
  pointer-events: none;
}

.career-node {
  cursor: pointer;
}

/* Labels are drawn inside the node group, so let the whole box own the hover
   rather than only the pixels the glyphs cover. */
.career-nodes text {
  pointer-events: none;
  user-select: none;
}

.career-node rect {
  transition: filter 0.18s ease;
}

.career-node:hover rect,
.career-node:focus-visible rect {
  filter: brightness(1.06) saturate(1.15);
}

.career-node:focus-visible rect {
  stroke-width: 3;
}

.career-node-kind {
  font-family: var(--font-body);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  fill: var(--ink-soft);
}

.career-node-label {
  font-family: var(--font-display);
  font-size: 13px;
  fill: var(--ink);
}

/* One tooltip, moved around. Positioned inside the graph box so it can never
   be clipped by the page, and pointer-events off so it cannot steal the hover
   from the node that opened it. */
.career-tip {
  position: absolute;
  z-index: 4;
  max-width: 240px;
  display: grid;
  gap: 0.15rem;
  padding: 0.5rem 0.65rem;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--card);
  box-shadow: var(--shadow-md);
  font-size: 0.76rem;
  line-height: 1.4;
  pointer-events: none;
}

.career-tip strong {
  font-family: var(--font-display);
  font-size: 0.86rem;
}

.tip-kind {
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.tip-pay {
  font-weight: 700;
  color: var(--mint-deep, var(--mint));
}

/* A legend swatch. Small enough to sit inside a chip without changing its
   height. */
.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  display: inline-block;
  flex: 0 0 9px;
}

/* The same path as a numbered list — the accessible copy of the diagram, and
   the only copy on a printout. */
.path-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
  counter-reset: step;
}

.path-step {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-left: 4px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
}

.path-step.is-have {
  border-left-color: var(--mint);
}

.path-step.is-partial {
  border-left-color: var(--sunny);
}

.path-step.is-missing {
  border-left-color: var(--sky);
}

.path-step.is-target {
  border-left-color: var(--tangerine, var(--sunny));
  background: color-mix(in srgb, var(--sunny) 10%, var(--card));
}

.path-step-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: 0 0 30px;
  border-radius: 10px;
  color: #fff;
  font-size: 0.95rem;
  box-shadow: var(--shadow-sm);
}

/* Score bars carry meaning in their colour: green is strong, amber is middling,
   blue is early days. Named after the palette so the template can pass the word
   straight through from Python. */
.bar-fill.mint {
  background: linear-gradient(90deg, var(--mint), #4ee0b4);
}

.bar-fill.sunny {
  background: linear-gradient(90deg, var(--sunny), #ffc94d);
}

.bar-fill.sky {
  background: linear-gradient(90deg, var(--sky), #74a0ff);
}

/* The goal shelf on the dashboard. Tinted so it reads as the learner's own
   stated aim rather than another thing the platform is suggesting at them. */
.goal-shelf {
  border-color: color-mix(in srgb, var(--sky) 45%, var(--line));
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--sky) 9%, var(--card)),
    var(--card) 60%
  );
}

[data-theme="dark"] .goal-shelf {
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--sky) 14%, var(--card)),
    var(--card) 60%
  );
}

/* --------------------------------------------------------------------------
   17. Catalogs — universities, exams and scholarships
   -------------------------------------------------------------------------- */

/* These three cards share a shape on purpose. A learner comparing an
   institution, an exam and a scholarship is doing one job — working out whether
   this is within reach — so the answer should sit in the same place each time. */
.uni-card,
.exam-card,
.scholarship-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.uni-card .role-pick,
.exam-card .role-pick,
.scholarship-card .role-pick {
  margin-top: auto;
}

.uni-card-head {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}

.uni-card.is-preferred {
  border-color: color-mix(in srgb, var(--mint) 55%, var(--line));
  box-shadow: 0 5px 0 color-mix(in srgb, var(--mint) 30%, transparent);
}

.exam-card.is-cleared {
  border-color: color-mix(in srgb, var(--mint) 45%, var(--line));
}

/* The rank badge earns its size — it is the one number people scan a list of
   institutions for. */
.uni-rank {
  flex: 0 0 auto;
  align-self: flex-start;
  padding: 0.2rem 0.5rem;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--sunny) 22%, var(--card));
  border: 1px solid color-mix(in srgb, var(--sunny) 45%, var(--line));
  font-family: var(--font-display);
  font-size: 0.82rem;
  white-space: nowrap;
}

[data-theme="dark"] .uni-rank {
  background: color-mix(in srgb, var(--sunny) 16%, var(--card));
}

/* Eligibility rules are label-and-value pairs, so they are marked up as a
   definition list rather than a paragraph pretending to be one. */
.fact-list {
  margin: 0;
  display: grid;
  gap: 0.35rem;
}

.fact-row {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: 0.5rem;
  align-items: baseline;
}

.fact-row dt {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.fact-row dd {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.45;
}

@media (max-width: 560px) {
  .fact-row {
    grid-template-columns: 1fr;
    gap: 0.1rem;
  }
}

/* --------------------------------------------------------------------------
   18. Responsive — tablet then phone
   -------------------------------------------------------------------------- */

@media (max-width: 1100px) {
  /* Stacked, but still minmax(0, …): a single bare 1fr track is
     minmax(auto, 1fr), so the widest thing in the main column — a lab's code
     editor, a wide table — would set a floor for the column and take the
     whole page with it. One column should mean "as wide as there is", not
     "as wide as my widest child". */
  .split-rail {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Stacked, the rail is just the next section down the page. Everything the
     pinned version needs has to come back off, or it becomes a short scrolling
     box sitting in the middle of an ordinary page. */
  .sticky-rail {
    position: static;
    max-height: none;
    overflow-y: visible;
    padding: 0;
    margin: 0;
  }
}

@media (max-width: 1024px) {
  :root {
    --rail-w: 216px;
  }

  .grid-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .hero-grid {
    grid-template-columns: 1fr;
    gap: 1rem;
  }

  .sticker-sheet {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   Tablet rail (861–1024)

   Between a phone and a laptop the drawer is the wrong answer — there is
   plenty of room for a permanent rail — but a 216px one costs a fifth of the
   page. So the rail stays put and drops to icons, and the topbar grows a
   button that gives the words back for anyone who wants them.

   `--rail-w` is set on .app-shell rather than :root so that the wide override
   is one class away, and .sidebar reads it as a descendant either way.

   app.js adds `.rail-icons` and forces every group open while this is active:
   a collapsed group here would be a divider with nothing under it.
   -------------------------------------------------------------------------- */

@media (min-width: 861px) and (max-width: 1024px) {
  .app-shell {
    --rail-w: 78px;
  }

  .app-shell.rail-wide {
    --rail-w: 216px;
  }

  .rail-toggle {
    display: inline-flex;
  }

  .app-shell.rail-icons .sidebar {
    padding-inline: 0.42rem;
  }

  .app-shell.rail-icons .sidebar-brand {
    justify-content: center;
    padding: 0.2rem 0 0.7rem;
  }

  .app-shell.rail-icons .sidebar-brand > div {
    display: none;
  }

  .app-shell.rail-icons .nav-text {
    /* Clipped rather than display:none — the label stays in the accessibility
       tree, so a screen reader still reads "Courses" and not "book emoji". */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .app-shell.rail-icons .sidebar-nav a {
    justify-content: center;
    gap: 0;
    padding: 0.62rem 0.3rem;
  }

  .app-shell.rail-icons .sidebar-nav a:hover {
    /* The slide-right nudge reads as a wobble when there is no label to slide. */
    transform: none;
  }

  .app-shell.rail-icons .sidebar-nav a .nav-ico {
    font-size: 1.22rem;
    width: auto;
    flex: 0 0 auto;
  }

  /* The heading becomes a rule. Still announced, still in the tab order, just
     no longer taking three lines of a 78px column to say "Where it leads". */
  .app-shell.rail-icons .nav-group > summary {
    height: 1px;
    padding: 0;
    margin: 0.5rem 0.55rem;
    background: rgba(255, 255, 255, 0.16);
    border-radius: 1px;
    overflow: hidden;
    text-indent: -999em;
    cursor: default;
    pointer-events: none;
  }

  .app-shell.rail-icons .nav-group > summary::after,
  .app-shell.rail-icons .nav-count {
    display: none;
  }

  .app-shell.rail-icons .sidebar-nav > .nav-group:first-child > summary {
    margin-top: 0;
  }

  .app-shell.rail-icons .rail-streak {
    justify-content: center;
    padding: 0.5rem 0.2rem;
    font-size: 0.7rem;
  }

  .app-shell.rail-icons .rail-streak span:last-child {
    display: none;
  }

  .app-shell.rail-icons .sidebar-footer .btn-block {
    padding-inline: 0;
    font-size: 0;
  }

  .app-shell.rail-icons .sidebar-footer .btn-block::before {
    content: "\1f44b";
    font-size: 1.05rem;
  }
}

@media (max-width: 860px) {
  .app-shell {
    display: block;
  }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(80vw, 270px);
    flex-basis: auto;
    transform: translateX(-102%);
    transition: transform 0.26s var(--ease-pop);
    box-shadow: var(--shadow-lg);
  }

  .sidebar.mobile-open {
    transform: none;
  }

  .mobile-menu-toggle {
    display: inline-flex;
  }

  .tabbar {
    display: block;
  }

  .app-shell-body .content-area {
    padding-bottom: 4.6rem;
  }

  .to-top {
    bottom: 4.4rem;
  }

  .grid-2,
  .grid-3,
  .grid-4 {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  }

  .sticker-sheet {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (max-width: 640px) {
  body {
    font-size: 14.5px;
  }

  .content-area,
  .nav-wrap {
    padding: 0.6rem 0.7rem;
  }

  .hero {
    padding: 0.9rem 0.7rem 0.5rem;
  }

  .container {
    width: 100%;
  }

  /* Same reason as .split-rail above, and it matters most here: every card
     grid on the site collapses through this rule, and a card carrying a
     nowrap chip — `.chip` is nowrap by design — would otherwise set a floor
     of "the longest course title on the page" for the whole phone layout. */
  .grid-2,
  .grid-3,
  .grid-4,
  .two-column {
    grid-template-columns: minmax(0, 1fr);
  }

  .card-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .sticker-sheet {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .form-grid,
  .form-grid-3 {
    grid-template-columns: 1fr;
  }

  /* One control per row. Two 10.5rem columns would just about fit at 360px and
     would be two unreadable dropdowns rather than one usable one. */
  .filter-bar {
    grid-template-columns: minmax(0, 1fr);
  }

  .filter-bar .btn,
  .filter-bar .link-btn {
    justify-self: stretch;
    text-align: center;
  }

  .page-head {
    align-items: flex-start;
  }

  .head-actions {
    width: 100%;
  }

  .head-actions .btn {
    flex: 1;
  }

  .bar-row {
    grid-template-columns: minmax(58px, 72px) 1fr 40px;
    font-size: 0.72rem;
  }

  .lesson-nav,
  .quiz-actions {
    flex-direction: column-reverse;
  }

  .lesson-nav .btn,
  .quiz-actions .btn {
    width: 100%;
  }

  .row-item {
    flex-wrap: wrap;
  }

  .row-actions {
    width: 100%;
    justify-content: flex-start;
  }

  .certificate-box {
    padding: 1.2rem 0.9rem;
  }

  .certificate-box h2 {
    font-size: 1.4rem;
  }

  .quiz-modal {
    padding: 0.5rem;
    align-items: flex-end;
  }

  .quiz-modal-panel {
    max-height: 88vh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
  }

  .user-badge span {
    display: none;
  }

  .user-badge {
    padding: 0.3rem;
  }
}

@media (max-width: 380px) {
  .stats-grid,
  .sticker-sheet {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .topbar {
    padding: 0.45rem 0.55rem;
  }

  .topbar-title h2 {
    font-size: 0.92rem;
  }

  .tabbar a {
    font-size: 0.58rem;
  }
}

/* --------------------------------------------------------------------------
   19. Accessibility and print
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }

  .confetti-piece {
    display: none;
  }
}

@media print {
  .sidebar,
  .topbar,
  .tabbar,
  .to-top,
  .site-header,
  .site-footer,
  .certificate-actions,
  .head-actions {
    display: none !important;
  }

  body {
    background: #fff;
  }

  .content-area {
    padding: 0;
  }

  .certificate-box {
    box-shadow: none;
  }

  /* A capped, scrolling rail prints as whatever happened to be scrolled into
     view. On paper there is no viewport to pin to, so let it run its full
     length like any other section. */
  .sticky-rail {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

/* --------------------------------------------------------------------------
   20. Catalog admin — the master data behind every career path
   --------------------------------------------------------------------------
   These pages are for one or two people, not for children, so they are quieter
   than the rest of the app: denser rows, smaller type, no stickers. The only
   colour that carries meaning here is the unlisted state, because an unlisted
   row still shows up in the list and must not look like a live one.
-------------------------------------------------------------------------- */

.form-note-bad {
  margin: 0 0 0.8rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--tangerine) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--tangerine) 40%, transparent);
  color: var(--tangerine-deep);
  font-size: 0.84rem;
  font-weight: 700;
}

[data-theme="dark"] .form-note-bad {
  color: var(--tangerine);
}

.req-mark {
  color: var(--tangerine-deep);
  font-weight: 800;
}

/* A field that would look absurd at a third of the width — a taxonomy path, a
   URL, anything with a sentence in it — claims the whole row instead. */
.field-wide {
  grid-column: 1 / -1;
}

.catalog-blurb {
  margin: 0;
  max-width: 62ch;
  font-size: 0.86rem;
  color: var(--muted);
}

.catalog-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  flex-wrap: wrap;
}

.catalog-row.is-hidden-row {
  opacity: 0.55;
}

.catalog-row.is-hidden-row .row-body strong {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.catalog-id {
  flex: 0 0 auto;
  min-width: 2.6rem;
  font-family: var(--font-mono, ui-monospace, "SFMono-Regular", Menlo, monospace);
  font-size: 0.74rem;
  color: var(--muted);
}

.catalog-meta {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  font-size: 0.76rem;
  color: var(--muted);
}

.catalog-meta span:not(:last-child)::after {
  content: " ·";
}

/* The relationship panels. A link row is one line: what it points at, the few
   fields that qualify the link, and the way to remove it. */
.link-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
}

.link-row + .link-row {
  margin-top: 0.4rem;
}

.link-row .grow {
  min-width: 8rem;
}

.link-row-note {
  font-size: 0.76rem;
  color: var(--muted);
}

.link-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.5rem;
  align-items: end;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--line);
}

.link-form .field {
  margin: 0;
}

.icon-btn-danger {
  border: 1px solid color-mix(in srgb, var(--tangerine) 40%, transparent);
  background: transparent;
  color: var(--tangerine-deep);
  border-radius: var(--r-sm, 8px);
  padding: 0.15rem 0.45rem;
  font-size: 0.78rem;
  cursor: pointer;
  line-height: 1.4;
}

.icon-btn-danger:hover {
  background: color-mix(in srgb, var(--tangerine) 12%, transparent);
}

[data-theme="dark"] .icon-btn-danger {
  color: var(--tangerine);
}

@media (max-width: 640px) {
  .link-form {
    grid-template-columns: 1fr;
  }

  .catalog-id {
    display: none;
  }
}

/* ==========================================================================
   21. Units and lessons
   A unit is a small group of pieces about one topic. It has to read as one
   thing without becoming a heavy card, because a course can have a dozen of
   them and the page still needs to fit on a phone screen.
   ========================================================================== */

.unit-block {
  border-left: 3px solid color-mix(in srgb, var(--hue, var(--blue)) 45%, transparent);
  padding-left: 0.85rem;
  margin-bottom: 1.1rem;
}

.unit-block:last-child {
  margin-bottom: 0;
}

.unit-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
  font-family: var(--font-display);
}

.unit-badge {
  display: inline-grid;
  place-items: center;
  min-width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--hue, var(--blue)) 18%, transparent);
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 700;
}

.unit-kinds {
  font-size: 0.85rem;
  letter-spacing: 0.06em;
}

.unit-edit {
  border-top: 1px dashed var(--line);
  padding-top: 0.8rem;
  margin-top: 0.8rem;
}

.unit-edit:first-of-type {
  border-top: 0;
  margin-top: 0;
}

.unit-edit-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.6rem;
}

.unit-delete {
  margin-top: 0.35rem;
}

@media (max-width: 640px) {
  .unit-block {
    padding-left: 0.6rem;
  }
}

/* ==========================================================================
   22. Difficulty tiers and drag-and-drop
   ========================================================================== */

.tier-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gap-sm);
}

.tier-pill {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "ico body" "track track";
  align-items: center;
  gap: 0.2rem 0.5rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--r-md);
  background: var(--card);
  border: 1px solid var(--line);
}

/* The tier a learner is standing on is the one fact this strip exists to say,
   so it gets the accent and everything else stays quiet. */
.tier-pill.is-on {
  border-color: var(--hue, var(--line-strong));
  box-shadow: var(--shadow-sm);
}

.tier-ico {
  grid-area: ico;
  font-size: 1.15rem;
}

.tier-body {
  grid-area: body;
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  min-width: 0;
}

.tier-body strong {
  font-size: 0.85rem;
}

.tier-track {
  grid-area: track;
  height: 5px;
  margin-top: 0.35rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  overflow: hidden;
}

.tier-track span {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--hue, var(--line-strong));
}

.match-board {
  display: grid;
  gap: var(--gap);
  margin-top: var(--gap-sm);
}

.match-tray {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-xs);
  padding: 0.65rem;
  border-radius: var(--r-md);
  border: 2px dashed var(--line-strong);
  background: var(--card-soft);
}

/* Without JavaScript the tray is only a reminder of what the choices are —
   the selects below carry the answer either way. */
.match-tray[aria-hidden="true"] .match-card {
  cursor: default;
}

.match-card {
  padding: 0.4rem 0.75rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-strong);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: grab;
}

.match-card.is-lifted {
  border-color: var(--hue, var(--line-strong));
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.match-card.is-placed {
  opacity: 0.45;
}

.match-rows {
  display: grid;
  gap: var(--gap-xs);
}

.match-row {
  display: grid;
  /* A plain 1fr is minmax(auto, 1fr) and will not go narrower than the longest
     phrase on the left. minmax(0, 1fr) lets the phrase wrap instead. */
  grid-template-columns: minmax(0, 1fr) minmax(9rem, 14rem);
  align-items: center;
  gap: var(--gap-sm);
  padding: 0.5rem 0.7rem;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--card);
}

.match-row.is-target {
  border-color: var(--hue, var(--line-strong));
  background: var(--card-soft);
}

.match-row.is-filled {
  border-color: var(--hue, var(--line-strong));
}

.match-left {
  font-weight: 700;
  font-size: 0.92rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.match-row select {
  width: 100%;
}

@media (max-width: 720px) {
  .tier-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .match-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .match-card.is-lifted {
    transform: none;
  }
}

/* ==========================================================================
   23. Labs
   ========================================================================== */

.lab-next {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-lg);
  border-left: 6px solid var(--hue, var(--primary));
}

.lab-next-body {
  display: flex;
  flex-direction: column;
  gap: var(--gap-xs);
  flex: 1 1 18rem;
}

.lab-card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  border-top: 4px solid var(--hue, var(--primary));
}

.lab-card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-sm);
}

/* The card heading is an h2 on the detail page and an h3 everywhere the card is
   included under a section heading of its own, so the size is pinned to both.
   Left as h2 alone, every card on the hub, band and track pages would quietly
   render a step smaller than the ones on the flat catalogue. */
.lab-card-head h2,
.lab-card-head h3 {
  font-size: 1.05rem;
  /* The same guard .lab-band-head h3 already carries. The wrapper has
     min-width: 0, which lets the column shrink, but a title like
     "Build a temperature-conversion calculator" still needs permission to
     break or it hangs out over the card's right edge. */
  overflow-wrap: anywhere;
}

.lab-kind {
  font-size: 1.6rem;
  line-height: 1;
}

/* One bar, used on the card and again beside the checklist. */
.lab-track {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  overflow: hidden;
}

.lab-track span {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--hue, var(--primary));
  transition: width 0.3s ease;
}

/* ---- the workbench ---- */

.lab-editor {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.lab-panes {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.lab-pane {
  display: block;
}

/* Without scripting every pane is shown, because there is nothing to switch
   with. The moment the bench reports itself scripted, the tabs take over. */
.lab-editor[data-scripted] .lab-pane {
  display: none;
}

.lab-editor[data-scripted] .lab-pane.is-on {
  display: block;
}

.lab-editor:not([data-scripted]) .lab-tabs {
  display: none;
}

.lab-pane textarea {
  width: 100%;
  font-family: var(--font-code);
  font-size: 0.86rem;
  line-height: 1.55;
  tab-size: 2;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  background: var(--card-soft);
}

.lab-preview {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.lab-preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-sm);
  /* A title and a run state pushed apart. In the 320px rail there is not
     always room for both on one line, and without the wrap the second one
     simply leaves the panel. */
  flex-wrap: wrap;
  min-width: 0;
}

.lab-preview-head .lab-run-state {
  margin-inline-start: auto;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.lab-run-state.is-busy { color: var(--ink-soft); }
.lab-run-state.is-good { color: var(--good, #15803d); }
.lab-run-state.is-bad { color: var(--bad, #b42318); }

.lab-frame {
  width: 100%;
  min-height: 15rem;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: #ffffff;
}

/* The frame is built and thrown away on every run, so the slot holds the space
   rather than the frame doing it — otherwise the page jumps every time the
   learner presses Run. */
.lab-frame-slot {
  min-height: 15rem;
}

.lab-frame-slot.is-hidden {
  min-height: 0;
  height: 0;
  overflow: hidden;
}

/* Output for the runtimes that print rather than draw. */
.lab-output:empty {
  display: none;
}

.lab-output {
  font-family: var(--font-code);
  font-size: 0.82rem;
  line-height: 1.6;
  max-height: 22rem;
  overflow: auto;
  padding: var(--gap-sm);
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px solid var(--line);
}

.lab-out-line {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.lab-out-line.is-bad { color: var(--bad, #b42318); }
.lab-out-line.is-warn { color: var(--warn, #b45309); }
.lab-out-line.is-good { color: var(--good, #15803d); }
.lab-out-line.is-quiet { color: var(--ink-soft); font-style: italic; }

/* ---- block coding ---- */

/* The textarea behind the palette is real and does the saving. It is only
   hidden once the palette has drawn, so a failure leaves an editable box. */
.lab-pane.is-hidden-source textarea {
  display: none;
}

.lab-pane-label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.35rem;
}

.lab-blocks {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.lab-block-palette {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.lab-block-chip {
  font: inherit;
  font-size: 0.84rem;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: var(--card-soft);
  cursor: pointer;
  /* Comfortably tappable: this palette is used most on a phone. */
  min-height: 2.4rem;
}

.lab-block-chip:hover {
  background: var(--card);
}

.lab-block-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.lab-block {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--r-sm, 8px);
  border: 1px solid var(--line);
  background: var(--card-soft);
}

.lab-block.is-repeat {
  border-color: var(--line-strong);
  background: var(--card);
}

.lab-block-name {
  font-size: 0.88rem;
  font-weight: 600;
}

.lab-block-value {
  width: 5.5rem;
  font: inherit;
  font-size: 0.84rem;
  padding: 0.2rem 0.4rem;
  border-radius: 6px;
  border: 1px solid var(--line-strong);
}

.lab-block-unit {
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.lab-block-tools {
  margin-inline-start: auto;
  display: flex;
  gap: 0.2rem;
}

.lab-block-btn {
  font: inherit;
  min-width: 2rem;
  min-height: 2rem;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: transparent;
  cursor: pointer;
}

.lab-block-btn.is-remove {
  color: var(--bad, #b42318);
}

.lab-block-add {
  font: inherit;
  font-size: 0.8rem;
  align-self: flex-start;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  border: 1px dashed var(--line-strong);
  background: transparent;
  cursor: pointer;
}

.lab-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-sm);
}

/* ---- the checklist ---- */

.lab-steps {
  display: flex;
  flex-direction: column;
  gap: var(--gap-xs);
}

.lab-step {
  display: grid;
  /* minmax(0, …) rather than a bare 1fr on the third track. A step reads
     "run `python3 tools/check_answers.py --verbose`", and a bare 1fr is
     minmax(auto, 1fr): that unbreakable path sets the track's floor, the
     floor widens the 320px rail, and the rail pushes the page off the side.
     The pair with .lab-step-text below is what lets the text break instead. */
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: start;
  gap: var(--gap-sm);
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease;
}

.lab-step:hover {
  border-color: var(--line-strong);
}

.lab-step.is-done {
  border-color: var(--success);
  background: var(--card-soft);
}

.lab-step.is-done .lab-step-text {
  text-decoration: line-through;
  color: var(--ink-faint);
}

.lab-step-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  font-size: 0.78rem;
  font-weight: 800;
}

.lab-step.is-done .lab-step-no {
  background: var(--success);
  border-color: var(--success);
  color: #ffffff;
}

.lab-step-text {
  font-size: 0.92rem;
  min-width: 0;
  /* Steps quote filenames, commands and URLs, none of which have a space in
     them to break at. `anywhere` breaks mid-token as a last resort, which is
     ugly for one line and much better than a page you have to scroll
     sideways. */
  overflow-wrap: anywhere;
}

/* ---- marking ---- */

.lab-feedback {
  border-left: 6px solid var(--success);
}

/* The safety note on an offline activity. Warm rather than alarming — this is
   "a grown-up cuts the bottle", not a hazard sign, and a red box at the top of
   a bean-planting activity would put families off the whole shelf. It earns
   its place by being above the steps, not by shouting. */

.lab-safety {
  border-left: 6px solid var(--sunny);
  background: color-mix(in srgb, var(--sunny) 8%, var(--card));
}

.lab-safety p {
  color: var(--ink-soft);
  font-size: 0.9rem;
}

.lab-code {
  margin: var(--gap-xs) 0 0;
  padding: 0.6rem 0.7rem;
  border-radius: var(--r-sm);
  background: var(--card-soft);
  border: 1px solid var(--line);
  font-family: var(--font-code);
  font-size: 0.8rem;
  white-space: pre-wrap;
  overflow-x: auto;
}

.row-item.is-open {
  border-color: var(--primary);
  background: var(--card-soft);
}

/* ---- the illustrated walk-through ----
   Picture on the left, words on the right, one row per step. The picture is
   given a fixed slot rather than being sized by its own contents, because
   every drawing is the same 160x100 frame and a column of pictures that all
   line up is the whole reason for having drawn them to one frame. */

.guide-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--gap-sm);
}

.guide-step {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: var(--gap-sm);
  align-items: start;
  padding: var(--gap-sm);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
  /* Without this the drawing's intrinsic width wins on a narrow screen and
     pushes the whole page sideways. */
  min-width: 0;
}

.guide-figure {
  margin: 0;
  padding: 0.35rem;
  border-radius: var(--r-sm);
  background: var(--card);
  border: 1px solid var(--line);
}

.lab-diagram {
  display: block;
  width: 100%;
  height: auto;
}

.guide-body {
  min-width: 0;
}

.guide-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.3rem;
  font-size: 1rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.guide-no {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 1.55rem;
  height: 1.55rem;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 700;
}

.guide-detail {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.92rem;
  line-height: 1.55;
}

.guide-watch {
  margin: 0.45rem 0 0;
  padding: 0.4rem 0.55rem;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--sunny) 12%, var(--card));
  font-size: 0.86rem;
}

/* ---- the way in: hub, ladder, tracks, pathways ---- */

/* A chip that goes somewhere. The chip already reads as a small tappable
   thing, so all a linked one needs is to stop inheriting link blue and the
   underline, and to grow a target big enough for a thumb. */
a.chip-link {
  text-decoration: none;
  color: var(--ink-soft);
  padding: 0.34rem 0.7rem;
  transition: transform 0.15s var(--ease-pop), border-color 0.15s ease;
}

a.chip-link:hover,
a.chip-link:focus-visible {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 45%, var(--line));
  color: var(--ink);
}

/* Twenty-six subjects. As a grid of cards that is a page of its own; as a
   wrapped cloud of chips it is one glance, which is the right weight for a
   secondary way in sitting under the class ladder. */
.chip-cloud {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.chip-count {
  padding: 0 0.34rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sky) 16%, var(--card-soft));
  color: var(--sky-deep);
  font-size: 0.66rem;
}

[data-theme="dark"] .chip-count {
  background: color-mix(in srgb, var(--sky) 24%, transparent);
  color: var(--ink);
}

/* One rung of the class ladder. Same press-down ledge as the stat cards, so a
   whole card that happens to be a link still looks like something you push. */
.lab-band-card,
.lab-path-card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  text-decoration: none;
  color: inherit;
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 24%, var(--line));
  box-shadow: 0 4px 0 color-mix(in srgb, var(--hue, var(--sky)) 26%, transparent);
  transition: transform 0.2s var(--ease-pop), box-shadow 0.2s ease;
}

.lab-band-card:hover,
.lab-band-card:focus-visible,
.lab-path-card:hover,
.lab-path-card:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 7px 0 color-mix(in srgb, var(--hue, var(--sky)) 26%, transparent);
}

/* The learner's own year, marked. Without it the ladder is five identical
   cards and the one question a learner actually arrives with -- "which of
   these is mine" -- goes unanswered. */
.lab-band-card.is-mine {
  border-color: color-mix(in srgb, var(--hue, var(--sky)) 55%, var(--line));
  background: linear-gradient(155deg, color-mix(in srgb, var(--hue, var(--sky)) 11%, var(--card)), var(--card) 76%);
}

.lab-band-head {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-sm);
}

/* Grow is a utility, but the heading inside it still needs the width taken
   off it: a long band label with a "You are here" chip beside it will push
   the chip off the card otherwise. */
.lab-band-head h3 {
  font-size: 1.02rem;
  overflow-wrap: anywhere;
}

/* ---- the way on from a band page ---- */

.lab-ladder-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--gap-sm);
}

.lab-step-link {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  text-decoration: none;
  color: inherit;
  transition: transform 0.2s var(--ease-pop), border-color 0.2s ease;
}

.lab-step-link:hover,
.lab-step-link:focus-visible {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--sky) 45%, var(--line));
}

/* Forward gets the accent and the right-hand alignment; back is deliberately
   quieter. Both present, equally weighted, and a learner who has just
   finished a year has to stop and read to find the way on. */
.lab-step-link.is-next {
  text-align: right;
  border-color: color-mix(in srgb, var(--sky) 40%, var(--line));
  background: color-mix(in srgb, var(--sky) 8%, var(--card));
}

/* ---- one pathway, in order ---- */

.lab-path-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.lab-path-step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--gap-sm);
  align-items: start;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
}

.lab-path-step.is-done {
  border-color: color-mix(in srgb, var(--mint) 38%, var(--line));
  background: color-mix(in srgb, var(--mint) 8%, var(--card));
}

/* A step whose lab has not been written yet. Shown as a gap rather than
   dropped, because a sequence with a silent hole in it teaches the wrong
   shape of the job -- but dashed and drained so it never reads as work
   waiting to be done. */
.lab-path-step.is-missing {
  border-style: dashed;
  background: var(--card-soft);
  color: var(--ink-faint);
}

.lab-path-no {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--hue, var(--sky)) 16%, var(--card-soft));
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 30%, var(--line));
  font-family: var(--font-display, inherit);
  font-weight: 800;
  font-size: 0.8rem;
  line-height: 1;
}

.lab-path-step.is-done .lab-path-no {
  background: color-mix(in srgb, var(--mint) 22%, var(--card));
  border-color: color-mix(in srgb, var(--mint) 40%, var(--line));
}

.lab-path-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.lab-path-title {
  font-weight: 800;
  font-size: 0.95rem;
  overflow-wrap: anywhere;
}

/* ---- the kitchen-table panel ---- */

.lab-offline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-lg);
  border-left: 6px solid var(--hue, var(--mint));
  background: linear-gradient(155deg, color-mix(in srgb, var(--hue, var(--mint)) 12%, var(--card)), var(--card) 74%);
}

@media (max-width: 720px) {
  .lab-next {
    align-items: stretch;
  }

  /* Two arrows side by side at 320px are two words each. Stacked they are
     two readable rows, and the forward one loses its right alignment because
     at full width there is nothing for it to be aligned against. */
  .lab-ladder-nav {
    grid-template-columns: minmax(0, 1fr);
  }

  /* A sticker, a heading and a "You are here" chip on one line leaves the
     heading about 100px on a small phone — and chips do not wrap, so the
     heading is the only thing that can give. Letting the chip drop to its own
     line gives the band its name back. */
  .lab-band-head {
    flex-wrap: wrap;
  }

  .lab-band-head .chip {
    margin-left: calc(40px + var(--gap-sm));
  }

  .lab-step-link.is-next {
    text-align: left;
  }

  .lab-offline {
    align-items: stretch;
  }

  .lab-step {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .lab-step-no {
    display: none;
  }

  /* On a phone the picture goes above the words at full width. Shrinking it
     into a 90px column instead would make a labelled row of five jars into
     five grey smudges, which is worse than not drawing it. */
  .guide-step {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .lab-track span {
    transition: none;
  }
}

/* ==========================================================================
   24. Mentors
   ========================================================================== */

.mentor-mine {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-lg);
  border-left: 6px solid var(--primary);
}

.mentor-card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
}

.mentor-card-head {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
}

.mentor-card-head h1,
.mentor-card-head h2 {
  line-height: 1.2;
}

.mentor-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 2px solid var(--line-strong);
  font-size: 1.15rem;
  font-weight: 800;
  text-transform: uppercase;
  color: var(--ink);
}

.mentor-avatar-sm {
  width: 2.25rem;
  height: 2.25rem;
  font-size: 0.9rem;
}

.mentor-avatar-lg {
  width: 4rem;
  height: 4rem;
  font-size: 1.5rem;
}

.mentor-load {
  display: flex;
  flex-direction: column;
  gap: var(--gap-xs);
  margin-top: auto;
}

.mentor-load-bar {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  overflow: hidden;
}

.mentor-load-bar span {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--primary);
  transition: width 0.3s ease;
}

@media (max-width: 720px) {
  .mentor-mine {
    align-items: flex-start;
  }

  .mentor-avatar-lg {
    width: 3rem;
    height: 3rem;
    font-size: 1.15rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mentor-load-bar span {
    transition: none;
  }
}

/* ==========================================================================
   25. Discussions
   ========================================================================== */

.talk-thread {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.talk-turn {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--gap-sm);
  align-items: start;
}

.talk-seat {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 2px solid var(--hue, var(--line-strong));
  font-size: 1rem;
}

.talk-body {
  padding: 0.65rem 0.85rem;
  border-radius: var(--r-md);
  background: var(--card-soft);
  border: 1px solid var(--line);
  border-left: 4px solid var(--hue, var(--line-strong));
}

.talk-body p + p {
  margin-top: var(--gap-xs);
}

.talk-who {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-xs);
  align-items: baseline;
  font-size: 0.88rem;
  margin-bottom: var(--gap-xs);
}

.talk-mine .talk-body {
  background: var(--card);
  border-color: var(--primary);
  border-left-color: var(--primary);
}

@media (max-width: 720px) {
  .talk-turn {
    grid-template-columns: 1fr;
  }

  .talk-seat {
    display: none;
  }
}

/* ==========================================================================
   26. The weekly plan
   ==========================================================================
   A plan row is a row-item that carries the colour of the kind of thing it is,
   so a week reads as a shape — mostly green means mostly labs — before any of
   the words are read. A finished row fades rather than disappears: the point of
   Sunday is seeing what got done.
   -------------------------------------------------------------------------- */

.plan-item {
  border-left: 4px solid var(--hue, var(--line-strong));
}

.plan-item.plan-done {
  opacity: 0.72;
}

.plan-item.plan-done .row-body > strong {
  text-decoration: line-through;
  text-decoration-thickness: 2px;
  text-decoration-color: var(--hue, var(--success));
}

.plan-strip {
  border-left: 6px solid var(--hue, var(--primary));
}

@media (max-width: 720px) {
  .plan-item {
    border-left-width: 6px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .plan-item,
  .plan-strip {
    transition: none;
  }
}

/* ==========================================================================
   27. System settings: the switch panel
   ==========================================================================
   Deliberately built to feel like a fuse box rather than a form. Each row is a
   physical thing you throw, wide enough to hit on a phone, and it carries the
   consequence of throwing it in plain words underneath — the question an admin
   is really asking at the switch is "what will the children stop seeing?".
   -------------------------------------------------------------------------- */

.switch-list {
  display: grid;
  gap: 0.5rem;
}

.switch-row {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.85rem;
  padding: 0.85rem 1rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}

.switch-row:hover {
  border-color: var(--hue, var(--primary));
  transform: translateY(-1px);
}

/* The real checkbox stays in the page, just out of sight: it keeps the label
   click, the keyboard focus and the form submission working for free. */
.switch-row > input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.switch {
  position: relative;
  flex: none;
  width: 3.1rem;
  height: 1.7rem;
  margin-top: 0.15rem;
  border-radius: var(--r-pill);
  background: var(--line-strong);
  transition: background 0.18s ease;
}

.switch::after {
  content: "";
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  width: 1.3rem;
  height: 1.3rem;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-sm);
  transition: transform 0.18s ease;
}

.switch-row > input:checked + .switch {
  background: var(--success);
}

.switch-row > input:checked + .switch::after {
  transform: translateX(1.4rem);
}

.switch-row > input:focus-visible + .switch {
  outline: 3px solid var(--primary);
  outline-offset: 3px;
}

.switch-row > input:checked ~ .switch-body > strong {
  color: var(--ink);
}

.switch-body {
  display: grid;
  gap: 0.2rem;
  font-size: 0.9rem;
}

.switch-body > strong {
  font-family: var(--font-display);
  color: var(--ink-soft);
}

/* An unavailable switch is shown, not hidden: knowing a feature exists but
   cannot run today is more useful than a gap where it used to be. */
.switch-row.is-blocked {
  cursor: not-allowed;
  opacity: 0.6;
  border-style: dashed;
}

.switch-row.is-blocked:hover {
  transform: none;
  border-color: var(--line);
}

.save-bar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-sm);
  padding: 0.85rem 1rem;
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-md);
}

@media (max-width: 720px) {
  .switch-row {
    padding: 0.75rem 0.8rem;
  }

  .save-bar .row-actions {
    width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .switch-row,
  .switch,
  .switch::after {
    transition: none;
  }

  .switch-row:hover {
    transform: none;
  }
}

/* ==========================================================================
   28. Play mode: the Toddler Program's screen
   ==========================================================================
   Written for a one-year-old sitting on somebody's lap, which changes almost
   every rule the rest of this stylesheet follows.

   Nothing is small. A toddler aims with a whole hand, so the tap targets here
   are the size of a coaster rather than a button. Nothing is subtle either:
   the picture carries the meaning, the words underneath are for the grown-up
   and for the voice to read. Because this section is appended below the
   responsive block in section 18, it carries its own media queries — anything
   added down here has to bring its own, or it will never adapt on a phone.
   -------------------------------------------------------------------------- */

.play-body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background:
    radial-gradient(1000px 620px at 8% -8%, var(--bg-tint-a), transparent 62%),
    radial-gradient(880px 560px at 104% 4%, var(--bg-tint-b), transparent 58%),
    var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
}

/* ---- the bar across the top ---- */

.play-top {
  position: sticky;
  top: 0;
  z-index: 10;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--gap);
  padding: 0.7rem clamp(0.8rem, 3vw, 1.6rem);
  padding-top: max(0.7rem, env(safe-area-inset-top));
  background: color-mix(in srgb, var(--card) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 2px solid var(--line);
}

.play-back {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: var(--card-soft);
  border: 2px solid var(--line);
  color: var(--ink);
  font-size: 1.4rem;
  text-decoration: none;
  transition: transform 0.15s var(--ease-pop), border-color 0.15s ease;
}

.play-back:hover {
  transform: translateX(-2px);
  border-color: var(--primary);
}

.play-top-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 3.4vw, 1.5rem);
  line-height: 1.15;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The speaker. Deliberately the loudest control on the page after the activity
   itself: it is the one thing a grown-up may need in a hurry, in a waiting
   room or a quiet carriage. */
.play-voice {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.6rem 0.95rem;
  border: 2px solid var(--mint-deep);
  border-radius: var(--r-pill);
  background: var(--mint);
  color: #fff;
  font-family: var(--font-display);
  font-size: 0.95rem;
  cursor: pointer;
  box-shadow: 0 3px 0 var(--mint-deep);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.18s ease;
}

.play-voice:active {
  transform: translateY(3px);
  box-shadow: 0 0 0 var(--mint-deep);
}

.play-voice.is-off {
  background: var(--line-strong);
  border-color: var(--ink-faint);
  color: var(--ink);
  box-shadow: 0 3px 0 var(--ink-faint);
}

.play-voice:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.play-voice-ico {
  font-size: 1.2rem;
}

.play-main {
  max-width: 1180px;
  margin: 0 auto;
  padding: clamp(1rem, 3vw, 2rem) clamp(0.8rem, 3vw, 1.6rem) 3rem;
}

/* ---- the hub ---- */

.play-intro {
  max-width: 46rem;
  margin: 0 auto 1.6rem;
  text-align: center;
}

.play-lede {
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.6vw, 1.35rem);
  line-height: 1.45;
  color: var(--ink-soft);
}

.play-hint {
  margin: 0;
  font-size: 0.95rem;
  color: var(--ink-faint);
}

.play-section {
  margin: 2rem 0 0.9rem;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 3vw, 1.6rem);
}

.play-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: clamp(0.7rem, 2vw, 1.1rem);
}

.play-grid-wide {
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

/* A card is a picture with a name under it. That is the entire vocabulary of
   this screen, and it is enough. */
.play-card {
  display: grid;
  gap: 0.45rem;
  padding: 0.7rem;
  border: 3px solid var(--hue, var(--primary));
  border-radius: var(--r-xl);
  background: var(--card);
  box-shadow: 0 6px 0 var(--hue-deep, var(--primary-deep));
  color: var(--ink);
  text-decoration: none;
  transition: transform 0.14s var(--ease-pop), box-shadow 0.14s ease;
}

.play-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 9px 0 var(--hue-deep, var(--primary-deep));
}

.play-card:active {
  transform: translateY(6px);
  box-shadow: 0 0 0 var(--hue-deep, var(--primary-deep));
}

.play-card-pic {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 14%, var(--card-soft));
}

.play-card-pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.play-card-emoji {
  font-size: clamp(2.4rem, 8vw, 3.4rem);
  line-height: 1;
}

/* The star for a finished session. It sits on the picture rather than beside
   the title because that is where a child looks. */
.play-card-tick {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  background: var(--sunny);
  font-size: 1.1rem;
  box-shadow: var(--shadow-sm);
}

.play-card-name {
  font-family: var(--font-display);
  font-size: 1.15rem;
  line-height: 1.2;
}

.play-card-sub {
  font-size: 0.86rem;
  color: var(--ink-soft);
}

.play-card-bar {
  display: block;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--line);
  overflow: hidden;
}

.play-card-bar-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--mint);
}

.play-empty {
  display: grid;
  justify-items: center;
  gap: 0.6rem;
  padding: 3rem 1rem;
  text-align: center;
  color: var(--ink-soft);
}

.play-empty-face {
  margin: 0;
  font-size: 3.6rem;
}

/* The way out, for the grown-up. Quiet on purpose — small type a toddler will
   not aim at, at the bottom where they will not reach. */
.play-footnote {
  margin: 2.4rem auto 0;
  max-width: 44rem;
  text-align: center;
  font-size: 0.88rem;
  color: var(--ink-faint);
}

.play-footnote a {
  color: var(--ink-soft);
}

/* ---- the session ---- */

/* Every pane on the play screen sets its own `display`, and a rule that sets
   `display` beats the browser's own `[hidden] { display: none }`. Which meant
   the start pane, the stage and the "all done" pane were all on screen at
   once, stacked, and the whole thing scrolled. Nothing in play mode is allowed
   to scroll, so hidden means hidden here, loudly. */
.play-body [hidden] {
  display: none !important;
}

.play-start,
.play-finish {
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  padding: clamp(1.6rem, 6vw, 3.4rem) 1rem;
  text-align: center;
}

.play-start-emoji,
.play-finish-emoji {
  margin: 0;
  font-size: clamp(4rem, 18vw, 7rem);
  line-height: 1;
  animation: play-bob 2.6s ease-in-out infinite;
}

.play-start-title,
.play-finish-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 5.5vw, 2.6rem);
  line-height: 1.15;
}

.play-start-sub,
.play-finish-sub,
.play-start-note {
  margin: 0;
  color: var(--ink-soft);
}

.play-start-note {
  margin-top: 0.6rem;
  font-size: 0.9rem;
  color: var(--ink-faint);
}

/* The single biggest thing on the site. It has to be, because it is the tap
   that buys permission to play every sound in the session. */
.play-start-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.4rem;
  padding: 1rem 2.2rem;
  border: 3px solid var(--hue-deep, var(--primary-deep));
  border-radius: var(--r-pill);
  background: var(--hue, var(--primary));
  color: #fff;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 4vw, 1.7rem);
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 7px 0 var(--hue-deep, var(--primary-deep));
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.play-start-btn:active {
  transform: translateY(7px);
  box-shadow: 0 0 0 var(--hue-deep, var(--primary-deep));
}

.play-start-btn-soft {
  background: var(--card);
  color: var(--ink);
  border-color: var(--line-strong);
  box-shadow: 0 5px 0 var(--line-strong);
  font-size: clamp(1rem, 3vw, 1.2rem);
  padding: 0.75rem 1.5rem;
}

.play-start-btn-soft:active {
  transform: translateY(5px);
  box-shadow: 0 0 0 var(--line-strong);
}

.play-finish-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.7rem;
  margin-top: 0.6rem;
}

@keyframes play-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

/* The row of beads along the top of a session. It is a map more than a
   control: how many things there are, which one we are on, which are done. */
.play-dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  margin-bottom: 0.9rem;
}

.play-dot {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  border: 2px solid var(--line-strong);
  background: var(--card);
  font-size: 1.15rem;
  cursor: pointer;
  transition: transform 0.15s var(--ease-pop), border-color 0.15s ease;
}

.play-dot.is-done {
  border-color: var(--mint);
  background: color-mix(in srgb, var(--mint) 18%, var(--card));
}

.play-dot.is-current {
  transform: scale(1.18);
  border-color: var(--hue, var(--primary));
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--hue, var(--primary)) 22%, transparent);
}

.play-card-big {
  position: relative;
  display: grid;
  gap: 1rem;
  padding: clamp(0.8rem, 2.4vw, 1.4rem);
  border: 3px solid var(--hue, var(--primary));
  border-radius: var(--r-xl);
  background: var(--card);
  box-shadow: 0 8px 0 var(--hue-deep, var(--primary-deep));
}

/* The picture. Emoji and video are laid on top of one another in the same
   grid cell rather than swapped in and out of the flow, so that changing from
   one to the other cannot change the height of anything. */
.play-media {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 180px;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: color-mix(in srgb, var(--hue, var(--primary)) 12%, var(--card-soft));
}

.play-media > .play-emoji-big,
.play-media > .play-video {
  grid-area: 1 / 1;
}

.play-emoji-big {
  font-size: clamp(5rem, 26vw, 11rem);
  line-height: 1;
  animation: play-bob 3.2s ease-in-out infinite;
  transition: opacity 0.25s ease;
}

/* The video frame is never taken out of the layout, only faded out.
   A player inside a `display: none` box is refused permission to play on
   phones, and a player built inside one may never report itself ready at all
   -- which is exactly how this screen used to end up showing a picture and
   silence where a song should have been. */
.play-video {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.play-media.is-video > .play-video {
  opacity: 1;
  pointer-events: auto;
}

.play-media.is-video > .play-emoji-big {
  opacity: 0;
}

.play-video iframe,
.play-video > div {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Sound can be refused even when playing is allowed, so the song runs on
   silently and this sits over it. One tap and the sound is back for the rest
   of the visit -- which is a far better outcome than a frozen screen. */
.play-sound-fix {
  position: absolute;
  left: 50%;
  bottom: 8%;
  transform: translateX(-50%);
  z-index: 5;
  padding: 0.85rem 1.4rem;
  border: 3px solid var(--sunny-deep);
  border-radius: var(--r-pill);
  background: var(--sunny);
  color: #fff;
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.6vh, 1.2rem);
  cursor: pointer;
  box-shadow: 0 5px 0 var(--sunny-deep);
}

.play-sound-fix:active {
  transform: translateX(-50%) translateY(5px);
  box-shadow: 0 0 0 var(--sunny-deep);
}

/* And when a video has genuinely gone, the grown-up gets a way out instead of
   a black rectangle. Small and out of a toddler's way, on purpose. */
.play-video-out {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  z-index: 5;
  padding: 0.45rem 0.85rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--ink);
  font-size: 0.82rem;
  text-decoration: none;
}

.play-words {
  text-align: center;
}

.play-step-title {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 4.6vw, 2.1rem);
  line-height: 1.15;
}

/* The sentence being read aloud. Large, short lines, generous leading — it is
   the caption to the voice, and an adult reads it over a child's head. */
.play-step-say {
  margin: 0 auto;
  max-width: 34rem;
  font-size: clamp(1.05rem, 3.2vw, 1.35rem);
  line-height: 1.65;
  color: var(--ink-soft);
}

.play-step-area {
  margin: 0.7rem 0 0;
  font-family: var(--font-display);
  font-size: 0.9rem;
  color: var(--hue-deep, var(--primary-deep));
}

.play-controls {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: clamp(0.6rem, 3vw, 1.4rem);
  margin-top: 1.1rem;
}

/* The way in to the grown-up's note. Parked at the end of the row rather than
   in it, so the three big buttons a toddler aims at stay centred. */
.play-grownup-btn {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.6rem 0.9rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--card-soft);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-size: 0.85rem;
  cursor: pointer;
}

.play-nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-width: 4.2rem;
  padding: 0.9rem 1.1rem;
  border: 3px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1.3rem;
  cursor: pointer;
  box-shadow: 0 5px 0 var(--line-strong);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.play-nav-btn:active {
  transform: translateY(5px);
  box-shadow: 0 0 0 var(--line-strong);
}

.play-nav-main {
  min-width: 9rem;
  border-color: var(--hue-deep, var(--primary-deep));
  background: var(--hue, var(--primary));
  color: #fff;
  box-shadow: 0 5px 0 var(--hue-deep, var(--primary-deep));
}

.play-nav-main:active {
  box-shadow: 0 0 0 var(--hue-deep, var(--primary-deep));
}

/* The line that fills while an activity screen waits before moving on. It
   exists so the grown-up can see the change coming rather than be surprised
   by it; the script builds it, so it is invisible until then. */
.play-dwell {
  position: absolute;
  left: 50%;
  bottom: 0.45rem;
  transform: translateX(-50%);
  z-index: 4;
  display: block;
  width: min(22rem, 70%);
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--line);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.play-dwell.is-running {
  opacity: 1;
}

.play-dwell > i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--hue, var(--primary));
}

/* The note to the adult. Set apart from everything else in tone as well as
   position — this is the only text on the screen that is not read aloud, and
   the only thing here a toddler is not meant to touch. It opens *over* the
   picture, never below it: a note of unknown length underneath the buttons is
   exactly how a screen ends up scrolling. */
.play-grownup {
  position: absolute;
  left: 50%;
  bottom: 0.6rem;
  transform: translateX(-50%);
  z-index: 9;
  width: min(40rem, calc(100% - 1.2rem));
  max-height: 62%;
  overflow-y: auto;
  padding: 0.95rem 1.1rem;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: 0 14px 34px rgba(15, 23, 42, 0.22);
  font-size: 0.92rem;
  color: var(--ink-soft);
}

.play-grownup strong {
  display: block;
  margin-bottom: 0.25rem;
  font-family: var(--font-display);
  color: var(--ink);
}

.play-grownup p {
  margin: 0;
}

.play-grownup-close {
  margin-top: 0.7rem;
  padding: 0.5rem 1rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--card-soft);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 0.85rem;
  cursor: pointer;
}

/* Between one session and the next. Left alone it counts down and the next
   session simply begins — which is the entire promise of this screen. The two
   buttons are for the grown-up who wants to hurry it along or stop. */
.play-nextup {
  position: absolute;
  inset: 0;
  z-index: 12;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 0.4rem;
  padding: 1rem;
  border-radius: var(--r-xl);
  background: color-mix(in srgb, var(--card) 93%, transparent);
  backdrop-filter: blur(8px);
  text-align: center;
}

.play-nextup-emoji {
  margin: 0;
  font-size: clamp(2.6rem, 9vh, 4.5rem);
  line-height: 1;
  animation: play-bob 2.6s ease-in-out infinite;
}

.play-nextup-kicker {
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.85rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.play-nextup-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 4.4vh, 2.1rem);
  line-height: 1.15;
}

.play-nextup-sub {
  margin: 0;
  color: var(--ink-soft);
}

.play-nextup-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.7rem;
  margin-top: 0.5rem;
}

/* The reward strip -- stars, streak and any sticker that just landed.
 *
 * It appears in two places, the finish card and the next-up card, and is
 * deliberately the plainest thing on either: no animation, no colour of its
 * own, no size that competes with the big Start button underneath. The
 * celebration is what it says, not how it moves. A strip that bounced would
 * pull a child's eye away from the countdown that is about to start the next
 * session for them.
 *
 * Laid out as rows of icon-then-words rather than centred text, because a
 * six-year-old reading these is reading down a column of left edges, and
 * centred lines give them a different starting point for every line. */
.play-reward {
  display: grid;
  gap: 0.35rem;
  width: min(24rem, 100%);
  margin: 0.7rem 0 0;
  padding: 0.7rem 0.9rem;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 10%, var(--card));
  list-style: none;
  text-align: left;
}

.play-reward-line {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  align-items: baseline;
  gap: 0.5rem;
  font-weight: 700;
  color: var(--ink);
}

.play-reward-ico {
  font-size: 1.15rem;
  line-height: 1.3;
  text-align: center;
}

/* The two rare lines, and the only ones that get to look different. A sticker
   is the one a child will go and tell somebody about; an award happens once per
   course and has their name printed on it. Everything else here is a number
   they will see again tomorrow. */
.play-reward-line.is-sticker .play-reward-words,
.play-reward-line.is-award .play-reward-words {
  color: var(--hue-deep, var(--primary));
}

.play-reward-line.is-award {
  font-size: 1.05rem;
}

/* Rarer than either of them: a learner climbs three levels in their whole time
   here, and the line only appears on the one lesson that did it. */
.play-reward-line.is-level {
  font-size: 1.05rem;
}

.play-reward-line.is-level .play-reward-words {
  color: var(--hue-deep, var(--primary));
}

.play-reward-line.is-done .play-reward-words,
.play-reward-line.is-streak .play-reward-words {
  font-weight: 600;
  color: var(--ink-soft);
}

/* The wrappers. Nothing decorative — they exist to hold the width of a
   session together and to be the element the hue is set on, which is why the
   hue variables are read from here by everything inside. */
.play-session,
.play-stage {
  display: block;
  max-width: 940px;
  margin: 0 auto;
}

.play-stage-wrap {
  position: relative;
  display: block;
}

/* A session card is a plain card with more room for its lines: the name, what
   is in it, and how far through it you are. */
.play-card-session {
  align-content: start;
}

.play-card-session .play-card-name {
  font-size: 1.25rem;
}

/* ---- the locked screen ----------------------------------------------------
   The lesson page, and only the lesson page, is nailed to the height of the
   display. A toddler swipes at everything; a screen that slides away in the
   middle of a song is a screen they have lost, and they will not scroll it
   back. So the document becomes a grid exactly one viewport tall, the picture
   takes whatever height is left after the beads and the buttons, and the words
   are clamped to two lines — they are the caption to a voice that is reading
   them out anyway.

   Type is sized in `vh` here rather than `vw`, because the thing that runs out
   on this screen is height, not width. -------------------------------------- */

.play-body.play-locked {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
}

.play-locked .play-top {
  padding-block: 0.45rem;
  padding-top: max(0.45rem, env(safe-area-inset-top));
}

.play-locked .play-main {
  display: grid;
  width: 100%;
  min-height: 0;
  overflow: hidden;
  padding: clamp(0.4rem, 1.6vh, 1rem) clamp(0.6rem, 3vw, 1.4rem)
           max(clamp(0.4rem, 1.6vh, 1rem), env(safe-area-inset-bottom));
}

.play-locked .play-session {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  width: 100%;
  min-height: 0;
}

.play-locked .play-stage-wrap {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: clamp(0.35rem, 1.3vh, 0.85rem);
  min-height: 0;
}

.play-locked .play-dots {
  flex-wrap: nowrap;
  overflow: hidden;
  margin-bottom: 0;
}

.play-locked .play-dot {
  width: clamp(1.8rem, 4.4vh, 2.4rem);
  height: clamp(1.8rem, 4.4vh, 2.4rem);
  font-size: clamp(0.85rem, 2.4vh, 1.1rem);
}

.play-locked .play-card-big {
  grid-template-rows: minmax(0, 1fr) auto;
  gap: clamp(0.35rem, 1.3vh, 0.8rem);
  min-height: 0;
  overflow: hidden;
  padding: clamp(0.45rem, 1.4vh, 1rem);
}

.play-locked .play-media {
  width: 100%;
  height: 100%;
  min-height: 0;
}

/* Filling the space rather than holding 16:9 is deliberate: the player letter-
   boxes itself, and a fixed ratio on a short landscape screen is what pushes
   the buttons off the bottom. */
.play-locked .play-video {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
}

.play-locked .play-emoji-big {
  font-size: clamp(3.5rem, 22vh, 10rem);
}

.play-locked .play-step-title {
  margin-bottom: 0.15rem;
  font-size: clamp(1.05rem, 3.4vh, 1.8rem);
}

.play-locked .play-step-say {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: clamp(0.9rem, 2.3vh, 1.25rem);
  line-height: 1.45;
}

.play-locked .play-controls {
  margin-top: 0;
  flex-wrap: nowrap;
}

.play-locked .play-nav-btn {
  padding: clamp(0.5rem, 1.6vh, 0.9rem) 1rem;
  font-size: clamp(1rem, 2.8vh, 1.3rem);
}

.play-locked .play-start,
.play-locked .play-finish {
  align-content: center;
  min-height: 0;
  overflow: hidden;
  padding: 1rem;
}

.play-locked .play-start-emoji,
.play-locked .play-finish-emoji {
  font-size: clamp(3rem, 16vh, 6.5rem);
}

.play-locked .play-start-title,
.play-locked .play-finish-title {
  font-size: clamp(1.4rem, 5vh, 2.4rem);
}

/* ---- small screens ---- */

@media (max-width: 720px) {
  .play-top {
    grid-template-columns: auto 1fr auto;
    gap: 0.5rem;
    padding-inline: 0.7rem;
  }

  .play-top-title {
    font-size: 1.05rem;
  }

  /* On a phone the word beside the speaker is the first thing to go: the icon
     already says everything, and the title needs the room. */
  .play-voice-label {
    display: none;
  }

  .play-voice {
    padding: 0.6rem 0.8rem;
  }

  .play-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }

  .play-grid-wide {
    grid-template-columns: 1fr;
  }

  .play-dot {
    width: 2.2rem;
    height: 2.2rem;
    font-size: 1rem;
  }

  .play-nav-btn {
    min-width: 3.6rem;
    padding: 0.8rem 0.9rem;
  }

  .play-nav-main {
    min-width: 7rem;
  }

  /* On a phone the grown-up's button loses its word too. The hand already
     says what it is, and the three big buttons need every pixel. */
  .play-grownup-btn {
    padding: 0.55rem 0.6rem;
    font-size: 0.95rem;
  }

  .play-grownup-word {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .play-start-emoji,
  .play-finish-emoji,
  .play-nextup-emoji,
  .play-emoji-big {
    animation: none;
  }

  .play-card,
  .play-dot,
  .play-back,
  .play-nav-btn,
  .play-start-btn,
  .play-voice {
    transition: none;
  }

  .play-card:hover {
    transform: none;
  }
}

/* The invitation to leave the reading page for play mode. It belongs to
   section 28 but is styled beside the ordinary course page, because that is
   where it appears. */
.play-invite {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.85rem;
  margin-top: 0.9rem;
  padding: 0.85rem 1rem;
  border: 3px solid var(--hue, var(--primary));
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 10%, var(--card));
  color: var(--ink);
  text-decoration: none;
  box-shadow: 0 5px 0 var(--hue-deep, var(--primary-deep));
  transition: transform 0.14s var(--ease-pop), box-shadow 0.14s ease;
}

.play-invite:active {
  transform: translateY(5px);
  box-shadow: 0 0 0 var(--hue-deep, var(--primary-deep));
}

.play-invite-face {
  font-size: 2.2rem;
  line-height: 1;
}

.play-invite strong {
  display: block;
  font-family: var(--font-display);
  font-size: 1.1rem;
}

.play-invite small {
  color: var(--ink-soft);
}

.play-invite-go {
  font-size: 1.4rem;
  color: var(--hue-deep, var(--primary-deep));
}

@media (max-width: 720px) {
  .play-invite {
    grid-template-columns: auto 1fr;
  }

  .play-invite-go {
    display: none;
  }
}

/* The song swap rows in the lesson admin. A picture beside the box, because
   the fastest way to check you pasted the right link is to see the still. */
.song-row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 0.8rem;
  align-items: start;
  padding: 0.7rem 0;
  border-top: 1px solid var(--line);
}

.song-thumb {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--card-soft);
  font-size: 1.6rem;
  text-decoration: none;
}

.song-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.song-fields {
  display: grid;
  gap: 0.35rem;
}

@media (max-width: 720px) {
  .song-row {
    grid-template-columns: 1fr;
  }

  .song-thumb {
    max-width: 220px;
  }
}

/* ==========================================================================
   29. Learn mode: the screen every year above the Toddler Program shares
       (Playgroup 2-3, Nursery 3-4, LKG 4-5, UKG 5-6)
   ==========================================================================
   Every rule in this section is nested under `.learn-body`, and that is not a
   stylistic preference — it is the whole reason the section exists. Learn mode
   runs on the same markup and the same script as play mode, so without that
   prefix each of these rules would land on the toddler screen too, and the
   toddler screen is finished. Nothing below may be written without it.

   What actually differs at four years old:

   - **Calmer.** A one-year-old needs contrast to find the thing; a
     four-year-old finds it anyway. The ledges come down from eight pixels to
     five, the borders from three to two, the picture background from a wash of
     colour to something nearer the card.
   - **Smaller pictures, more words.** The emoji stops being the screen and
     starts being an illustration, and the sentence gets three lines instead of
     two, at a size meant to be looked at rather than glanced past. It is being
     read aloud with each word lighting up, which is only worth doing if the
     words are big enough to follow.
   - **A bar, not beads.** Twelve coasters along the top of a session says
     nothing to a child who cannot count them. A bar filling up says "nearly
     there", which is a thing a four-year-old understands and cares about.
   - **Somewhere to answer.** The whole point of this age group: a question,
     two to four pictures, one tap.

   Like section 28 this sits below the responsive block in section 18, so it
   carries its own media queries. Anything added here needs its own too.
   -------------------------------------------------------------------------- */

/* ---- the quieter surfaces ---- */

.learn-body .play-card {
  border-width: 2px;
  box-shadow: 0 5px 0 var(--hue-deep, var(--primary-deep));
}

.learn-body .play-card:hover {
  box-shadow: 0 7px 0 var(--hue-deep, var(--primary-deep));
}

.learn-body .play-card-big {
  border-width: 2px;
  box-shadow: 0 5px 0 var(--hue-deep, var(--primary-deep));
}

.learn-body .play-media {
  background: color-mix(in srgb, var(--hue, var(--primary)) 7%, var(--card-soft));
}

/* The note under a heading on the hub. It is for the parent deciding which of
   twenty courses to open, so it reads at body size and stops well short of the
   full width. */
.learn-section-note {
  margin: -0.4rem 0 1rem;
  max-width: 44rem;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ink-soft);
}

.learn-grid {
  grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
}

/* The end-of-year checklist. A list of sentences, not cards: a parent reads it
   once, top to bottom, and is either reassured or knows what to work on. */
.learn-readiness {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.learn-readiness li {
  display: grid;
  grid-template-columns: 2rem 1fr;
  align-items: start;
  gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  line-height: 1.5;
}

.learn-readiness-ico {
  font-size: 1.3rem;
  line-height: 1.3;
}

/* ---- the progress bar that used to be beads ---- */

/* Same element, same ids, same click handlers — the beads simply become
   segments of one bar. They stay tappable, which is how a grown-up jumps back
   to the bit that needs saying again. */
.learn-body .play-dots {
  flex-wrap: nowrap;
  gap: 3px;
  width: min(38rem, 100%);
  margin: 0 auto 0.9rem;
  padding: 3px;
  border-radius: var(--r-pill);
  background: var(--line);
  overflow: hidden;
}

.learn-body .play-dot {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  height: 12px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--card);
  /* The emoji inside is meaningless at this size; it is kept in the DOM for
     the toddler screen and for anyone reading the page with a screen reader. */
  font-size: 0;
  color: transparent;
  overflow: hidden;
}

.learn-body .play-dot.is-done {
  background: var(--mint);
}

.learn-body .play-dot.is-current {
  transform: none;
  background: var(--hue, var(--primary));
  box-shadow: none;
}

/* ---- the words, lighting up as they are read ---- */

/* Deliberately understated. The word being spoken gets weight and the hue
   behind it; the words already read fade back. A child following along sees
   where the voice is; a child not following along sees a normal sentence. */
.learn-body .play-step-say.has-words {
  color: var(--ink);
}

.learn-body .play-word {
  border-radius: 0.35rem;
  padding: 0 0.08em;
  transition: background-color 0.12s linear, color 0.12s linear;
}

.learn-body .play-word.is-said {
  color: var(--ink-soft);
}

.learn-body .play-word.is-now {
  background: color-mix(in srgb, var(--hue, var(--primary)) 26%, transparent);
  color: var(--ink);
  font-weight: 800;
}

/* ---- the answer buttons ---- */

/* Two to four of them, always pictures with a word underneath, always big
   enough to hit with a whole hand. `auto-fit` rather than a fixed count so two
   choices get half the width each and four get a quarter, without the template
   having to know which. */
.learn-body .play-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: clamp(0.4rem, 1.4vh, 0.8rem);
  margin-top: 0.2rem;
}

.learn-body .play-choice {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 0.2rem;
  padding: clamp(0.5rem, 1.6vh, 0.9rem) 0.5rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-display);
  cursor: pointer;
  box-shadow: 0 4px 0 var(--line-strong);
  transition: transform 0.12s ease, box-shadow 0.12s ease,
              border-color 0.15s ease, background 0.2s ease;
}

.learn-body .play-choice:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 var(--line-strong);
}

.learn-body .play-choice:disabled {
  cursor: default;
}

.learn-body .play-choice-face {
  font-size: clamp(1.8rem, 6vh, 3rem);
  line-height: 1;
}

.learn-body .play-choice-label {
  font-size: clamp(0.85rem, 2.2vh, 1.1rem);
  line-height: 1.15;
  text-align: center;
}

/* Right. The button turns green and stays that way while the praise is spoken,
   so the answer is still visible when the child looks back at it. */
.learn-body .play-choice.is-right {
  border-color: var(--mint-deep);
  background: color-mix(in srgb, var(--mint) 24%, var(--card));
  box-shadow: 0 4px 0 var(--mint-deep);
  opacity: 1;
}

/* Wrong. A shake and a warm colour rather than a red cross: nothing here is
   allowed to feel like a mark. The class is removed again after 800ms, and the
   button simply stops responding. */
.learn-body .play-choice.is-wrong {
  animation: learn-shake 0.4s ease;
  border-color: var(--tangerine-deep, var(--line-strong));
  background: color-mix(in srgb, var(--tangerine, var(--sunny)) 18%, var(--card));
}

/* After two wrong answers the screen stops waiting and points. The pulse is
   the only animation on this page that repeats, because it has to be noticed
   by somebody who has stopped looking. */
.learn-body .play-choice.is-hint {
  border-color: var(--hue, var(--primary));
  animation: learn-point 1.1s ease-in-out infinite;
}

@keyframes learn-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

@keyframes learn-point {
  0%, 100% { box-shadow: 0 4px 0 var(--hue-deep, var(--primary-deep)); }
  50% { box-shadow: 0 4px 0 var(--hue-deep, var(--primary-deep)),
                    0 0 0 8px color-mix(in srgb, var(--hue, var(--primary)) 20%, transparent); }
}

/* The reward. It lands over the picture, not beside it, so nothing on the
   screen moves when it appears — a layout that jumps at the moment of getting
   something right is a layout that teaches the wrong lesson. */
.learn-body .play-sticker {
  position: absolute;
  left: 50%;
  top: 38%;
  z-index: 8;
  transform: translate(-50%, -50%) scale(0.4);
  font-size: clamp(3.5rem, 16vh, 7rem);
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.28));
  transition: transform 0.42s var(--ease-pop), opacity 0.32s ease;
}

.learn-body .play-sticker.is-pop {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}

/* ---- the locked session screen ---- */

/* The card gains two more rows: one for the line a sentence is built up on and
   one for the answers. Both are `auto`, so on the screens that have neither —
   every toddler session, and every screen that is just a song — they collapse
   to nothing and the picture takes the space back. The same card doing four
   jobs, without a second layout. */
.learn-body.play-locked .play-card-big {
  grid-template-rows: minmax(0, 1fr) auto auto auto;
}

/* A question screen is mostly words and buttons, so the picture gives way.
   Without this the emoji keeps its full height and the answers are pushed off
   the bottom of a short screen, which is the one failure this whole layout
   exists to prevent. */
.learn-body.play-locked .play-card-big.is-quiz .play-media {
  max-height: 34vh;
}

.learn-body.play-locked .play-card-big.is-quiz .play-emoji-big {
  font-size: clamp(2.6rem, 14vh, 5.5rem);
}

.learn-body.play-locked .play-emoji-big {
  font-size: clamp(3rem, 18vh, 8rem);
}

/* Three lines rather than two, and bigger. At this age the sentence is not a
   caption to the voice — it is the thing being learned. */
.learn-body.play-locked .play-step-say {
  -webkit-line-clamp: 3;
  line-clamp: 3;
  font-size: clamp(1rem, 2.7vh, 1.5rem);
  line-height: 1.5;
}

.learn-body.play-locked .play-dots {
  margin-bottom: 0;
}

.learn-body.play-locked .play-dot {
  width: auto;
  height: clamp(9px, 1.5vh, 13px);
  font-size: 0;
}

/* ---- choosing a school year ---- */

/* Two years, LKG and UKG, behind one door. The request was explicitly that the
   two look identical, so this is the only thing on any learn screen that
   mentions a year at all — a pair of buttons at the top of the hub, for the
   grown-up, because plenty of families have one child in each. It is not
   spoken and it is not a child's decision. */
.learn-body .learn-years {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0.6rem;
  margin: 0 0 0.4rem;
}

.learn-body .learn-year {
  display: grid;
  gap: 0.1rem;
  padding: 0.7rem 0.9rem;
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  color: var(--ink-soft);
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.2s ease, color 0.15s ease;
}

.learn-body .learn-year:hover {
  border-color: var(--line-strong);
  color: var(--ink);
}

/* The year you are looking at. Filled rather than merely outlined, because
   "which one am I on" has to be answerable at a glance from across a table. */
.learn-body .learn-year.is-on {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 12%, var(--card));
  color: var(--ink);
  box-shadow: 0 3px 0 var(--primary-deep, var(--primary));
}

.learn-body .learn-year-name {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 800;
}

.learn-body .learn-year-ages {
  font-size: 0.85rem;
}

.learn-body .learn-year-count {
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.learn-body .learn-year-note {
  margin-top: 0;
}

/* ---- tapping the word you heard ---- */

/* Same buttons as a picture question, but the choices are words out of the
   sentence and there is nothing to look at except the letters. So: no emoji
   row, wider boxes, and the words left in reading order rather than shuffled —
   a child hunting for "doctor" should find it where it was in the sentence,
   because that is the skill, not recall. */
.learn-body .play-choices-word {
  grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
}

.learn-body .play-choices-word .play-choice {
  padding: clamp(0.45rem, 1.4vh, 0.8rem) 0.6rem;
}

.learn-body .play-choices-word .play-choice-label {
  font-size: clamp(1rem, 2.8vh, 1.5rem);
  font-weight: 800;
  letter-spacing: 0.01em;
}

/* ---- putting a sentence back together ---- */

/* The line the words land on, above the jumbled ones. It is always in the
   layout once a build screen starts, even while empty, so that placing the
   first word does not shove the buttons down the screen mid-tap. */
.learn-body .play-build {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-height: clamp(2.4rem, 6vh, 3.4rem);
  margin-top: 0.3rem;
  padding: 0.35rem 0.5rem;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 7%, var(--card));
}

/* A word that has been placed. It arrives rather than appears, which is the
   only cue that it went somewhere — there is no other feedback on this screen
   until the sentence is finished. */
.learn-body .play-build-word {
  padding: 0.15rem 0.5rem;
  border-radius: var(--r-md, 0.6rem);
  background: var(--card);
  border: 2px solid var(--mint-deep);
  box-shadow: 0 2px 0 var(--mint-deep);
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 2.5vh, 1.35rem);
  font-weight: 800;
  color: var(--ink);
  animation: learn-land 0.28s var(--ease-pop);
}

@keyframes learn-land {
  from { transform: translateY(-0.5rem) scale(0.85); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* The jumbled words underneath. Chips rather than cards: no picture, no
   caption, tighter, and as many across as will fit, because a seven-word
   sentence in two-per-row would run off the bottom of a phone. */
.learn-body .play-choices-build {
  grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
  gap: 0.35rem;
}

.learn-body .play-choice-chip {
  padding: clamp(0.4rem, 1.2vh, 0.7rem) 0.5rem;
  box-shadow: 0 3px 0 var(--line-strong);
}

.learn-body .play-choice-chip .play-choice-label {
  font-size: clamp(0.9rem, 2.4vh, 1.3rem);
  font-weight: 800;
}

/* A chip that has been used stays on screen, greyed, rather than disappearing.
   Words vanishing as they are tapped would make the remaining ones jump around
   under a finger that is already moving. */
.learn-body .play-choice-chip:disabled {
  opacity: 0.35;
  box-shadow: none;
  transform: translateY(3px);
}

/* Ordering, so that the sentence being built always sits above the words still
   waiting, whatever order the script happened to append them to the card in. */
.learn-body .play-build {
  order: 1;
}

.learn-body .play-choices {
  order: 2;
}

/* ---- reading it back out loud ---- */

/* Two buttons, side by side, and neither of them marks anything. There is no
   microphone here on purpose: it would need a permission a parent has to
   grant, it would judge an accent it has no business judging, and a
   five-year-old told by a machine that they read it wrong stops reading. */
.learn-body .play-choices-read {
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
}

.learn-body .play-choices-read .play-choice-label {
  font-size: clamp(0.95rem, 2.5vh, 1.25rem);
  font-weight: 800;
}

/* "Read it to me" is the quieter of the two. It can be pressed as many times
   as anyone wants, so it should not look like the way forward. */
.learn-body .play-choice-soft {
  border-color: var(--line);
  background: color-mix(in srgb, var(--hue, var(--primary)) 9%, var(--card));
  box-shadow: 0 4px 0 var(--line);
}

/* On a reading screen the sentence is the entire content, so it gets the room
   the picture would otherwise have had, and all of it — no clamp, because a
   sentence with its last two words cut off cannot be read aloud. */
.learn-body.play-locked .play-card-big.is-reading .play-media {
  max-height: 22vh;
}

.learn-body.play-locked .play-card-big.is-reading .play-step-say {
  -webkit-line-clamp: unset;
  line-clamp: unset;
  display: block;
  overflow: visible;
  font-size: clamp(1.3rem, 4vh, 2.2rem);
  font-weight: 800;
  line-height: 1.45;
}

/* ---- thinking, for children who cannot read yet (ages 2-4) ----

   Matching, sorting and remembering. Three screens, one shelf: the tray sits
   between the picture and the answer buttons and holds whatever the activity
   needs looked at — the things to remember, or the one picture waiting to be
   put in a basket. It is empty and `hidden` on every other kind of screen, so
   it costs nothing anywhere else.

   These are still `.learn-body` rules. A three-year-old and a five-year-old get
   the same screen by request; what differs is only which activities their
   content asks for. */

.learn-body .play-tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(0.4rem, 1.6vh, 0.9rem);
  min-height: 0;
  margin: 0.2rem 0 0.4rem;
}

/* A tile is a picture with its word underneath, and it is not tappable. Same
   proportions as an answer button so that the thing you looked at and the thing
   you tap look like they belong to one another — but flat, no ledge, because a
   ledge is this screen's whole vocabulary for "you can press this". */
.learn-body .play-tile {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 0.15rem;
  padding: clamp(0.35rem, 1.2vh, 0.7rem) 0.6rem;
  border: 2px dashed var(--line);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 6%, var(--card));
}

/* Except the one picture currently being sorted, which is emphatically the
   subject of the screen and is drawn solid and larger to say so. */
.learn-body .play-tile-now {
  border-style: solid;
  border-color: var(--hue, var(--primary));
  background: var(--card);
}

.learn-body .play-tile-now .play-choice-face {
  font-size: clamp(2.4rem, 8vh, 4rem);
}

.learn-body .play-tile-now .play-choice-label {
  font-size: clamp(1rem, 2.6vh, 1.3rem);
  font-weight: 800;
}

/* "2 more after this" is for the grown-up deciding whether there is time to
   finish, not for the child. It is deliberately small and quiet. */
.learn-body .play-tray-left {
  font-size: clamp(0.75rem, 1.8vh, 0.9rem);
  color: var(--ink-soft);
}

/* ---- tap the two that go together ---- */

/* Six tiles at most, and they want to be square-ish: a matching board read as
   a row of wide rectangles stops looking like a board. */
.learn-body .play-choices-match {
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
}

.learn-body .play-choice-tile .play-choice-face {
  font-size: clamp(1.6rem, 5.5vh, 2.8rem);
}

/* Picked up, waiting for its partner. Not `is-hint` — that pulses, and means
   "the screen is telling you". This means "you told the screen", so it lifts
   and holds still. */
.learn-body .play-choice.is-picked {
  transform: translateY(-3px);
  border-color: var(--hue, var(--primary));
  background: color-mix(in srgb, var(--hue, var(--primary)) 16%, var(--card));
  box-shadow: 0 7px 0 var(--hue-deep, var(--primary-deep));
}

/* A matched pair stays on the board rather than disappearing. A three-year-old
   who has found two pairs should be able to see that they found them. */
.learn-body .play-choices-match .play-choice:disabled.is-right {
  opacity: 0.75;
}

/* ---- put each one in the right basket ---- */

/* Two baskets, always side by side and always the same size. A basket that
   grows because its word is longer looks like the answer. */
.learn-body .play-choices-sort {
  grid-template-columns: 1fr 1fr;
}

.learn-body .play-choice-bin {
  border-style: dashed;
  border-width: 3px;
  padding: clamp(0.6rem, 2vh, 1.1rem) 0.6rem;
}

.learn-body .play-choice-bin .play-choice-label {
  font-weight: 800;
}

/* ---- look, remember, answer ---- */

/* The tiles to remember get more room than an ordinary tray, because looking
   at them properly is the entire first half of the activity. */
.learn-body .play-tray-memory .play-tile {
  padding: clamp(0.5rem, 1.8vh, 0.9rem) 0.9rem;
}

.learn-body .play-tray-memory .play-choice-face {
  font-size: clamp(2rem, 7vh, 3.4rem);
}

/* ---- typing the answer in (Class 1 and up) ----

   One box and one button, side by side, and the box is the wide one. It is not
   in the grid the other activities use: a text field stretched to a grid cell
   and sat next to an equally wide button reads as two buttons, and children tap
   the wrong one. */
.learn-body .play-choices-type {
  display: flex;
  align-items: stretch;
  gap: clamp(0.4rem, 1.4vh, 0.8rem);
}

.learn-body .play-type-box {
  flex: 1 1 auto;
  min-width: 0;
  padding: clamp(0.5rem, 1.6vh, 0.9rem) 0.75rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 3.2vh, 1.8rem);
  font-weight: 800;
  /* Wide letter spacing, because the child is checking their spelling letter by
     letter and a tight display font makes "rn" and "m" the same shape. */
  letter-spacing: 0.06em;
  text-align: center;
  box-shadow: inset 0 2px 0 color-mix(in srgb, var(--line-strong) 30%, transparent);
  transition: border-color 0.12s ease, background 0.12s ease;
}

.learn-body .play-type-box::placeholder {
  color: var(--ink-soft);
  font-weight: 600;
  letter-spacing: normal;
}

.learn-body .play-type-box:focus {
  outline: none;
  border-color: var(--hue, var(--primary));
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--hue, var(--primary)) 22%, transparent);
}

/* The same three states the answer buttons use, said the same way, because a
   child who has learnt what the wobble and the warm colour mean on one screen
   should not have to learn it again on this one. */
.learn-body .play-type-box.is-wrong {
  animation: learn-shake 0.4s ease;
  border-color: var(--tangerine-deep, var(--line-strong));
  background: color-mix(in srgb, var(--tangerine, var(--sunny)) 18%, var(--card));
}

.learn-body .play-type-box.is-right {
  border-color: var(--mint-deep);
  background: color-mix(in srgb, var(--mint) 24%, var(--card));
  opacity: 1;
}

.learn-body .play-type-box.is-hint {
  border-color: var(--hue, var(--primary));
  animation: learn-point 1.1s ease-in-out infinite;
}

/* Narrow enough that the box keeps most of the row, wide enough to hit. */
.learn-body .play-type-check {
  flex: 0 0 auto;
  padding-left: clamp(0.7rem, 2.4vw, 1.2rem);
  padding-right: clamp(0.7rem, 2.4vw, 1.2rem);
}

/* ---- one word missing out of a sentence ---- */

/* The sentence sits on the same dashed line a built sentence does, but reads
   left to right as a sentence rather than centring as a heap of words. */
.learn-body .play-blank-line {
  justify-content: center;
  gap: 0;
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 2.6vh, 1.35rem);
  font-weight: 700;
  color: var(--ink);
}

.learn-body .play-blank-text {
  white-space: pre-wrap;
}

/* The gap. Drawn as a gap — a rule under a space — and not as a box with a
   question mark in it, because a gap is what it looks like on paper. It holds
   its width while empty so that filling it does not reflow the sentence under
   the finger that just filled it. */
.learn-body .play-blank-slot {
  display: inline-block;
  min-width: clamp(3.5rem, 14vw, 6rem);
  padding: 0 0.35rem;
  border-bottom: 3px solid var(--hue, var(--primary));
  text-align: center;
  font-weight: 800;
  color: var(--ink);
}

.learn-body .play-blank-slot.is-filled {
  border-bottom-color: var(--mint-deep);
  background: color-mix(in srgb, var(--mint) 22%, transparent);
  border-radius: var(--r-sm, 0.35rem) var(--r-sm, 0.35rem) 0 0;
  animation: learn-land 0.28s var(--ease-pop);
}

/* ---- putting them in order ---- */

/* Cards rather than chips: an ordering board is usually pictures, and the ones
   that are not still want the room, because "which comes next" is a slower
   question than "which word is this" and rewards a bigger target. */
.learn-body .play-choices-order {
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
}

/* The run so far, on the line above. Laid out in a row that wraps, so a
   seven-card week does not run off the side of a phone. */
.learn-body .play-order-line {
  gap: 0.3rem;
}

.learn-body .play-order-placed {
  position: relative;
  display: inline-grid;
  justify-items: center;
  gap: 0.05rem;
  padding: 0.2rem 0.45rem;
  border: 2px solid var(--mint-deep);
  border-radius: var(--r-md, 0.6rem);
  background: var(--card);
  box-shadow: 0 2px 0 var(--mint-deep);
  animation: learn-land 0.28s var(--ease-pop);
}

.learn-body .play-order-placed .play-choice-face {
  font-size: clamp(1.1rem, 3.4vh, 1.8rem);
  line-height: 1;
}

.learn-body .play-order-placed .play-choice-label {
  font-family: var(--font-display);
  font-size: clamp(0.7rem, 1.8vh, 0.9rem);
  font-weight: 800;
  color: var(--ink);
}

/* The position, small and in the corner. It is the answer to "how far have I
   got", which is a grown-up's question, so it is drawn like one. */
.learn-body .play-order-num {
  position: absolute;
  top: -0.45rem;
  left: -0.45rem;
  min-width: 1.1rem;
  padding: 0 0.2rem;
  border-radius: 999px;
  background: var(--mint-deep);
  color: var(--card);
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 800;
  line-height: 1.5;
  text-align: center;
}

/* ---- following a shape with a finger (Playgroup, ages 2-3) ----

   The only screen in the whole app with nothing on it to get wrong, and it
   looks like that on purpose. There is no ledge anywhere here, because a ledge
   is this screen's word for "press me" and the thing being asked for is a drag,
   not a press. There is no red, because nothing can be wrong. There is one dot
   larger than the others, and it is the one to touch next.

   `touch-action: none` on the board is not a nicety. Without it the browser
   claims the first finger-drag as a scroll, the pointer events stop arriving
   halfway down the shape, and the activity dies on exactly the devices it was
   built for. */

.learn-body .play-choices-trace {
  /* One board, not a row of buttons — the grid the other activities use would
     stretch it to the full width of the card and flatten the shape. */
  display: block;
}

.learn-body .play-trace {
  position: relative;
  width: min(100%, 32vh);
  aspect-ratio: 1 / 1;
  margin: 0 auto;
  border: 2px dashed var(--line);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 5%, var(--card));
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.learn-body .play-stone {
  position: absolute;
  transform: translate(-50%, -50%);
  width: clamp(1.4rem, 4.6vh, 2.2rem);
  height: clamp(1.4rem, 4.6vh, 2.2rem);
  padding: 0;
  border: 2px solid color-mix(in srgb, var(--hue, var(--primary)) 40%, transparent);
  border-radius: 50%;
  background: var(--card);
  cursor: pointer;
  touch-action: none;
  transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

/* Behind the finger: filled in. The trail left across the board is the whole
   reward, and it stays there until the shape is finished. */
.learn-body .play-stone.is-done {
  border-color: var(--hue-deep, var(--primary-deep));
  background: var(--hue, var(--primary));
}

/* Ahead of the finger, and next. Bigger and breathing, which is the only
   instruction a two-year-old is going to read. */
.learn-body .play-stone.is-next {
  border-width: 3px;
  border-color: var(--hue-deep, var(--primary-deep));
  animation: stone-ask 1.5s ease-in-out infinite;
}

/* Where the pencil comes back down after being lifted — the second stroke of a
   letter. A ring around it, so that when the shape breaks there is something on
   the board saying "over here now" rather than only a voice saying it. */
.learn-body .play-stone.is-lift {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--hue, var(--primary)) 22%, transparent);
}

@keyframes stone-ask {
  0%, 100% { transform: translate(-50%, -50%) scale(1.15); }
  50%      { transform: translate(-50%, -50%) scale(1.45); }
}

/* ---- two pictures, one thing changed ---- */

/* Side by side, always. Stacked, the two pictures stop being a comparison and
   become a list of eight things, and the activity is over before it starts. */
.learn-body .play-choices-spot {
  grid-template-columns: 1fr 1fr;
  align-items: start;
  gap: clamp(0.5rem, 2vw, 1.1rem);
}

.learn-body .play-spot-pic {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(0.25rem, 1vh, 0.5rem);
  padding: clamp(0.5rem, 2vh, 0.9rem);
  border: 3px solid var(--line);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 5%, var(--card));
}

/* A number rather than a word, because neither picture has a name and "One"
   and "Two" are two more things on screen that cannot be read. */
.learn-body .play-spot-badge {
  position: absolute;
  top: -0.75rem;
  left: -0.55rem;
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 2px solid var(--hue-deep, var(--primary-deep));
  border-radius: 50%;
  background: var(--card);
  font-size: 0.85rem;
  font-weight: 800;
  color: var(--ink);
}

.learn-body .play-spot-cell {
  padding: clamp(0.3rem, 1.2vh, 0.55rem) 0.3rem;
}

.learn-body .play-spot-cell .play-choice-face {
  font-size: clamp(1.5rem, 5vh, 2.6rem);
}

/* ---- small screens ---- */

@media (max-width: 720px) {
  .learn-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }

  /* The board gets the width rather than the height on a phone, where there is
     far more of the first than the second. */
  .learn-body .play-trace {
    width: min(88%, 30vh);
  }

  /* And the two pictures stay side by side here too, for the same reason the
     baskets do: the comparison is left-and-right or it is nothing. */
  .learn-body .play-choices-spot {
    grid-template-columns: 1fr 1fr;
  }

  /* A six-tile matching board on a phone is three rows of two. Two per row
     keeps every tile a comfortable thumb's width; squeezing three in makes the
     pictures too small to tell apart, which is the one thing a matching game
     cannot survive. */
  .learn-body .play-choices-match {
    grid-template-columns: 1fr 1fr;
  }

  /* The baskets stay side by side at every width. Stacked, "in this one" and
     "in that one" stop being a left-and-right choice and start being a list. */
  .learn-body .play-choices-sort {
    grid-template-columns: 1fr 1fr;
  }

  .learn-body .play-tray {
    gap: 0.4rem;
  }

  /* Four choices in a row on a phone gives each one about seventy pixels,
     which is smaller than a four-year-old's fingertip. Two rows of two. */
  .learn-body .play-choices {
    grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  }

  .learn-readiness li {
    grid-template-columns: 1.7rem 1fr;
    gap: 0.5rem;
  }

  .learn-body .play-dots {
    width: 100%;
  }

  /* Two years side by side is fine; two years plus their ages and their course
     counts is not, on a phone. One per row, which also makes each one a much
     bigger target for a parent holding the phone one-handed. */
  .learn-body .learn-years {
    grid-template-columns: 1fr;
  }

  /* Word chips get narrower rather than fewer. A seven-word sentence at two
     per row is four rows deep, which on a short screen pushes the sentence
     being built off the top. */
  .learn-body .play-choices-build {
    grid-template-columns: repeat(auto-fit, minmax(3.6rem, 1fr));
  }

  .learn-body .play-choices-word {
    grid-template-columns: repeat(auto-fit, minmax(4.2rem, 1fr));
  }

  /* The two reading buttons stay side by side at any width — they are a pair,
     and stacking them makes "I read it" look like the only one that matters. */
  .learn-body .play-choices-read {
    grid-template-columns: 1fr 1fr;
  }

  /* An ordering board goes two per row on a phone for the same reason the
     matching one does: the picture is the question. */
  .learn-body .play-choices-order {
    grid-template-columns: 1fr 1fr;
  }

  /* The typed answer keeps its row. Stacking the box above the button would
     put Check under the keyboard on the devices this is most used on. */
  .learn-body .play-type-box {
    font-size: clamp(1rem, 3vh, 1.4rem);
  }

  .learn-body .play-blank-slot {
    min-width: 3rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .learn-body .play-choice,
  .learn-body .play-word,
  .learn-body .play-sticker {
    transition: none;
  }

  .learn-body .play-choice.is-wrong,
  .learn-body .play-choice.is-hint,
  .learn-body .play-type-box.is-wrong,
  .learn-body .play-type-box.is-hint {
    animation: none;
  }

  /* A word dropped into a gap and a card landing in the run still have to
     arrive — on both screens that arrival is the only confirmation the tap did
     anything at all — they just stop dropping in to do it. */
  .learn-body .play-blank-slot.is-filled,
  .learn-body .play-order-placed {
    animation: none;
  }

  /* The sticker still has to appear, or a right answer goes unrewarded. It
     just appears rather than arriving. */
  .learn-body .play-sticker.is-pop {
    transform: translate(-50%, -50%) scale(1);
  }

  /* A placed word still has to land somewhere visible — it is the only
     confirmation the screen gives that the tap did anything — it just stops
     dropping in to do it. */
  .learn-body .play-build-word {
    animation: none;
  }

  .learn-body .learn-year,
  .learn-body .play-choice-chip {
    transition: none;
  }

  /* A picked-up tile still has to look picked up — it is the only sign the
     screen gives that the first of the two taps registered — so the colour and
     the border stay and only the lift goes. */
  .learn-body .play-choice.is-picked {
    transform: none;
  }

  /* The next stone stops breathing but stays big. On a tracing screen the size
     difference is the only thing saying which dot to touch, so it is the one
     part of the animation that cannot be given up. */
  .learn-body .play-stone {
    transition: none;
  }

  .learn-body .play-stone.is-next {
    animation: none;
    transform: translate(-50%, -50%) scale(1.35);
  }
}

/* ==========================================================================
   30. The grown-up pack: home activities and rubrics
   ==========================================================================

   The only page in the whole app with real tables in it. Everywhere else the
   data is card-shaped; here it is genuinely tabular — a rubric is a grid of
   criteria against levels and pretending otherwise would lose the reading.

   Every rule is prefixed `.pack-`, so nothing here can reach the play screens
   or the learn screens. That matters: the toddler screen must not shift by a
   pixel because a parent page grew a table.
   ========================================================================== */

/* ---- the two download buttons ---- */

.pack-sheets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0.85rem;
}

.pack-sheet {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.95rem 1.1rem;
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sky)) 28%, var(--line));
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}

.pack-sheet:hover,
.pack-sheet:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.pack-sheet-ico {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  flex: none;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sky)) 16%, transparent);
  font-size: 1.35rem;
}

.pack-sheet strong {
  display: block;
  font-family: "Baloo 2", "Nunito", sans-serif;
  font-size: 1rem;
  line-height: 1.2;
}

.pack-sheet-sub {
  display: block;
  margin-top: 0.15rem;
  color: var(--ink-soft);
  font-size: 0.78rem;
  line-height: 1.35;
}

.pack-sheet-go {
  flex: none;
  align-self: center;
  padding: 0.25rem 0.6rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--hue, var(--sky)) 14%, transparent);
  color: var(--hue-deep, var(--sky-deep));
  font-size: 0.72rem;
  font-weight: 800;
  white-space: nowrap;
}

/* ---- one category or one rubric ---- */

.pack-group {
  margin-top: 1.4rem;
}

.pack-group-name {
  margin: 0 0 0.5rem;
  font-family: "Baloo 2", "Nunito", sans-serif;
  font-size: 1.02rem;
  color: var(--hue-deep, var(--ink));
}

/* A five-column week does not fit on a phone at a readable size, and shrinking
   the text until it does is worse than letting it scroll. The wrapper scrolls
   sideways; the row heading stays put so a reader never loses which day they
   are looking at. */
.pack-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
}

.pack-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  min-width: 30rem;
}

.pack-table thead th {
  padding: 0.5rem 0.7rem;
  background: color-mix(in srgb, var(--hue, var(--sky)) 13%, var(--card));
  color: var(--hue-deep, var(--ink-soft));
  text-align: left;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.pack-table tbody th,
.pack-table tbody td {
  padding: 0.55rem 0.7rem;
  border-top: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  font-weight: 500;
  line-height: 1.4;
}

.pack-table tbody th {
  position: sticky;
  left: 0;
  font-weight: 700;
  white-space: nowrap;
  /* Opaque, because the rest of the row slides underneath it. */
  background: var(--card);
}

/* Zebra striping rather than a full grid of rules: the rows are what a reader
   tracks along, so those are the ones worth marking. */
.pack-table tbody tr:nth-child(even),
.pack-table tbody tr:nth-child(even) th {
  background: color-mix(in srgb, var(--hue, var(--sky)) 6%, var(--card));
}

.pack-table-rubric tbody th {
  position: static;
  white-space: normal;
  min-width: 8.5rem;
}

.pack-level {
  display: inline-grid;
  place-items: center;
  width: 1.35rem;
  height: 1.35rem;
  margin-right: 0.25rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--hue, var(--sky)) 20%, transparent);
  color: var(--hue-deep, var(--ink));
  font-size: 0.72rem;
  font-weight: 800;
}

/* ---- the worked examples ---- */

.pack-example {
  margin-top: 0.6rem;
  padding: 0.65rem 0.85rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sky)) 8%, transparent);
  border: 1px dashed color-mix(in srgb, var(--hue, var(--sky)) 32%, transparent);
}

.pack-example > summary {
  cursor: pointer;
  font-weight: 800;
  font-size: 0.85rem;
  color: var(--hue-deep, var(--ink));
}

.pack-example p {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  line-height: 1.5;
}

/* ---- the end-of-year checklist ---- */

.pack-checks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1.1rem;
  margin-top: 0.9rem;
}

.pack-check-group {
  padding: 0.9rem 1rem;
  border-radius: var(--r-lg);
  background: var(--card-soft);
  border: 1px solid var(--line);
}

.pack-check {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.3rem 0;
  font-size: 0.86rem;
  line-height: 1.4;
  cursor: pointer;
}

/* A box big enough to hit with a thumb. This list gets ticked standing up in a
   kitchen, not sitting at a desk. */
.pack-check input {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.08rem;
  accent-color: var(--sky);
}

.pack-check input:checked + span {
  color: var(--ink-soft);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--mint) 60%, transparent);
  text-decoration-thickness: 2px;
}

/* ---- small screens ---- */

@media (max-width: 720px) {
  .pack-sheets {
    grid-template-columns: 1fr;
  }

  .pack-sheet-go {
    display: none;
  }

  .pack-table {
    font-size: 0.82rem;
  }

  .pack-checks {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pack-sheet {
    transition: none;
  }

  .pack-sheet:hover,
  .pack-sheet:focus-visible {
    transform: none;
  }
}

/* =========================================================================
   Price plans
   ========================================================================= */

/* The pricing grid. auto-fit rather than a fixed four columns: a school that
   offers only two tiers should get two wide cards, not two narrow ones with
   half the row empty. */
.plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.9rem;
  align-items: start;
}

.plan-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: var(--pad-card);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  border-top: 4px solid var(--hue-deep, var(--line-strong));
}

/* The plan you are already on is lifted rather than tinted. Tinting it would
   have read as "this one is recommended", which is a different message. */
.plan-card.is-mine {
  border-color: var(--hue-deep, var(--primary));
  box-shadow: var(--shadow-md);
}

.plan-card.is-highlight {
  outline: 3px solid color-mix(in srgb, var(--hue-deep, var(--primary)) 35%, transparent);
  outline-offset: 3px;
}

.plan-card-head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.plan-card-head h2 {
  font-size: 1.05rem;
}

.plan-emoji {
  font-size: 1.4rem;
  line-height: 1;
}

.plan-price {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}

.plan-price strong {
  font-family: var(--font-display);
  font-size: 1.7rem;
  color: var(--hue-deep, var(--ink));
}

.plan-blurb {
  margin: 0;
  font-size: 0.82rem;
  min-height: 2.4em;
}

.plan-limits {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.3rem;
  flex: 1 1 auto;
}

.plan-limit {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.78rem;
  padding: 0.2rem 0;
  border-bottom: 1px dashed var(--line);
}

.plan-limit-name {
  color: var(--ink-soft);
  font-weight: 700;
}

.plan-limit-value {
  font-weight: 800;
  text-align: right;
  white-space: nowrap;
}

/* Not included is faded, never removed. A learner comparing tiers needs the
   gaps to line up row for row or the comparison stops working. */
.plan-limit.is-out {
  opacity: 0.45;
}

/* A link pair, not a toggle: it survives a reload and works without JS. */
.cycle-switch {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--card-soft);
  align-self: flex-start;
}

.cycle-option {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.9rem;
  border-radius: var(--r-pill);
  font-size: 0.82rem;
  font-weight: 800;
  color: var(--ink-soft);
  text-decoration: none;
}

.cycle-option.is-on {
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-sm);
}

/* The upgrade nudge on the limit page. */
.plan-nudge {
  margin: 1rem auto 0;
  max-width: 26rem;
  padding: var(--pad-card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-soft);
  text-align: center;
}

.plan-nudge-line {
  margin: 0.3rem 0;
  font-size: 0.9rem;
}

/* ---- usage meters ---- */

.meter-list {
  display: grid;
  gap: 0.75rem;
}

.meter-row {
  display: grid;
  gap: 0.3rem;
}

.meter-row.is-excluded {
  opacity: 0.5;
}

.meter-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.meter-value {
  font-size: 0.78rem;
  font-weight: 800;
  color: var(--ink-soft);
  white-space: nowrap;
}

.meter-track {
  height: 0.55rem;
  border-radius: var(--r-pill);
  background: var(--card-soft);
  border: 1px solid var(--line);
  overflow: hidden;
}

.meter-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--hue-deep, var(--primary));
  transition: width 0.3s ease;
}

/* Only the bar that has actually run out turns red. Colouring "nearly out"
   as a warning teaches people to ignore the colour. */
.meter-row.is-spent .meter-fill {
  background: var(--danger);
}

.meter-row.is-spent .meter-value {
  color: var(--danger);
}

/* ---- plan history, on the learner page and the admin one ---- */

.history-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}

.history-row {
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card-soft);
}

.history-main {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}

/* ---- the admin editor ---- */

.plan-editor {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0.9rem;
  align-items: start;
}

.limit-group {
  gap: 0.35rem;
}

.limit-line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  font-weight: 700;
}

.limit-name {
  color: var(--ink-soft);
  min-width: 0;
  overflow-wrap: anywhere;
}

.limit-inputs {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex: 0 0 auto;
}

.limit-inputs input[type="number"] {
  width: 4.5rem;
  padding: 0.3rem 0.4rem;
  font-size: 0.8rem;
  text-align: right;
}

.limit-inputs input[type="number"]:disabled {
  opacity: 0.4;
}

.inline-check {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
  cursor: pointer;
}

.inline-check input[type="checkbox"] {
  width: auto;
  margin: 0;
}

.field-actions {
  align-content: end;
  flex-direction: row;
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* ---- small screens ---- */

@media (max-width: 720px) {
  .plan-grid,
  .plan-editor {
    grid-template-columns: 1fr;
  }

  .cycle-switch {
    width: 100%;
  }

  .cycle-option {
    flex: 1 1 auto;
    justify-content: center;
  }

  /* The number and its Unlimited tick stay on one line even here — wrapping
     them apart is what makes a limit form feel unfinished on a phone. */
  .limit-line {
    flex-wrap: wrap;
  }

  .limit-inputs {
    margin-left: auto;
  }
}

/* ==========================================================================
   The curriculum map
   --------------------------------------------------------------------------
   One curriculum shown through seven boards. Three states repeat all over
   this page -- taught this year, met a stage later, outside the board's
   Grade 1 -- and they are the only thing a reader is really scanning for, so
   they get a colour, a word and a shape rather than a tick and a legend.
   Green/amber/grey and never green/red: "your board does this later" is not
   a failure, and colouring it like one would be the page arguing with itself.
   ========================================================================== */

.cmap-stage {
  margin: 0 0 0.4rem;
  color: var(--ink-soft);
  font-size: 0.9rem;
}

/* The IGCSE box. Warmer than a plain note because it is correcting the
   reader's expectation rather than adding to it. */
.cmap-warn {
  border-left: 4px solid var(--sunny-deep);
  background: color-mix(in srgb, var(--sunny) 16%, var(--card));
}

.cmap-tally {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
}

.cmap-count {
  flex: 1 1 12rem;
  padding: 0.7rem 0.9rem;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  font-size: 0.85rem;
  line-height: 1.35;
  color: var(--ink-soft);
}

.cmap-count strong {
  display: block;
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--ink);
}

.cmap-count.cmap-yes { background: color-mix(in srgb, var(--mint) 18%, var(--card)); }
.cmap-count.cmap-later { background: color-mix(in srgb, var(--sunny) 18%, var(--card)); }
.cmap-count.cmap-no { background: var(--card-soft); }
/* Extra is a fourth state and deliberately not a shade of "no". Grey would
   file "we teach this and nobody asked us to" alongside "your board leaves
   this out", which are opposite kinds of news. */
.cmap-count.cmap-extra { background: color-mix(in srgb, var(--grape) 16%, var(--card)); }

/* ---- the grid ---- */

/* The row heading carries a topic title and sometimes a sentence of
   explanation under it, so it cannot inherit the nowrap the weekly planner
   needs. Same exception the rubric table already makes. */
.cmap-grid tbody th {
  white-space: normal;
  min-width: 11rem;
  font-weight: 600;
}

.cmap-grid thead th:not(:first-child),
.cmap-grid tbody td {
  text-align: center;
}

/* The chosen board's column, lifted out of the other five. */
.cmap-grid .is-picked {
  background: color-mix(in srgb, var(--hue, var(--sky)) 15%, var(--card));
  box-shadow: inset 1px 0 0 var(--line-strong), inset -1px 0 0 var(--line-strong);
}

.cmap-mark {
  display: inline-block;
  min-width: 3.1rem;
  padding: 0.16rem 0.5rem;
  border-radius: var(--r-pill);
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.03em;
}

.cmap-mark.cmap-yes {
  background: color-mix(in srgb, var(--mint) 34%, var(--card));
  color: var(--mint-deep);
}

.cmap-mark.cmap-later {
  background: color-mix(in srgb, var(--sunny) 40%, var(--card));
  color: var(--sunny-deep);
}

/* Outline, not fill. "Not this year" should read as an absence on a page
   scanned at arm's length, and a filled grey pill reads as a third thing
   the board does. */
.cmap-mark.cmap-no {
  background: transparent;
  border: 1px dashed var(--line-strong);
  color: var(--ink-faint);
}

/* The reason under a topic title or a strand name. Small and quiet: it is
   there for the reader who stopped on that row, not for the one skimming. */
.cmap-note {
  display: block;
  margin-top: 0.25rem;
  font-size: 0.74rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink-soft);
  white-space: normal;
}

.cmap-gaps {
  margin: 0.4rem 0 0;
  padding-left: 1.1rem;
  color: var(--ink-soft);
  line-height: 1.55;
}

/* ---- the seven cards ---- */

.cmap-boards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--gap-sm);
}

.cmap-board {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.85rem 0.95rem;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, var(--card));
  color: var(--ink);
  text-decoration: none;
  transition: transform 0.16s var(--ease-pop), box-shadow 0.16s ease;
}

.cmap-board:hover,
.cmap-board:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.cmap-board strong {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--hue-deep, var(--ink));
}

.cmap-board-stage {
  font-size: 0.74rem;
  line-height: 1.35;
  color: var(--ink-soft);
}

.cmap-board-count {
  margin-top: 0.15rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
}

.cmap-board-extra {
  display: inline-block;
  margin-left: 0.3rem;
  padding: 0.05rem 0.3rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--grape) 16%, var(--card));
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink-soft);
}

/* IGCSE: on the list, and visibly not one of the six. Dimmed rather than
   removed, because a parent who was sold "IGCSE from Grade 1" needs to find
   the name and then be told what it actually means. */
.cmap-board.is-unreal {
  background: var(--card-soft);
  border-style: dashed;
}

.cmap-board.is-unreal strong {
  color: var(--ink-soft);
}

/* ===========================================================================
   Class 1 papers
   ===========================================================================

   The check-ups, subject tests and term examinations. These live inside learn
   mode's shell and use its colours, but almost nothing else about learn mode
   applies to them, because a paper is the one screen in the whole programme
   that a child is meant to be able to scroll back through.

   So: no `play-locked`, no giant single-question stage, no marking while they
   work. Every question is on the page at once, the answers are wide rows
   rather than picture tiles (these options are words -- "Tuesday", "under" --
   and a word in a square tile wraps into an unreadable stack), and nothing
   changes colour until Finish is tapped.
   =========================================================================== */

.learn-body .exam-stage {
  display: block;
  width: min(46rem, 100%);
  margin: 0 auto;
  padding: 0 0 4rem;
  text-align: left;
}

.learn-body .exam-intro {
  margin-bottom: 1.4rem;
}

.learn-body .exam-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.6rem 0 0;
}

.learn-body .exam-fact {
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--hue, var(--primary)) 14%, var(--card));
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--ink-soft);
}

.learn-body .exam-note {
  margin: 0.7rem 0 0;
  color: var(--ink-soft);
  font-size: 0.95rem;
  line-height: 1.5;
}

.learn-body .exam-form {
  display: grid;
  gap: 1rem;
}

/* A fieldset with the browser's own border and padding removed. `min-width: 0`
   is not decoration: a fieldset defaults to `min-content` sizing and a long
   question inside a grid will otherwise push the whole page sideways. */
.learn-body .exam-q {
  min-width: 0;
  margin: 0;
  padding: clamp(0.8rem, 2.4vw, 1.2rem);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}

.learn-body .exam-q-head {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  padding: 0;
  font-family: var(--font-display);
  font-size: clamp(1.02rem, 2.6vw, 1.2rem);
  line-height: 1.35;
  color: var(--ink);
}

.learn-body .exam-q-num {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 999px;
  background: var(--hue, var(--primary));
  color: #fff;
  font-size: 0.95rem;
  font-weight: 800;
}

/* Gap-fill questions arrive as two lines -- the sentence, then "Which word
   fills the gap?" -- and the second one is a rubric rather than part of the
   sentence. Stacking them keeps the sentence readable as a sentence. */
.learn-body .exam-q-line {
  display: block;
}

.learn-body .exam-q-line + .exam-q-line {
  margin-top: 0.25rem;
  font-size: 0.9em;
  font-weight: 600;
  color: var(--ink-soft);
}

.learn-body .exam-choices {
  display: grid;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

/* One row per answer, full width, with the whole row as the tap target. The
   real radio is hidden but still focusable, so the keyboard and the screen
   reader keep working while the thumb gets something 3rem tall to aim at. */
.learn-body .exam-choice {
  display: flex;
  gap: 0.65rem;
  align-items: center;
  min-height: 3rem;
  padding: 0.55rem 0.8rem;
  border: 2px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--card-soft);
  font-size: clamp(1rem, 2.4vw, 1.12rem);
  line-height: 1.3;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.learn-body .exam-choice input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.learn-body .exam-choice-mark {
  flex: none;
  width: 1.35rem;
  height: 1.35rem;
  border: 2px solid var(--line-strong);
  border-radius: 999px;
  background: var(--card);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.learn-body .exam-choice:hover {
  border-color: var(--hue, var(--primary));
}

/* The dot first, as an adjacent-sibling rule, and only then the row tint via
   `:has()`. A browser too old for `:has()` still shows a filled radio and the
   paper stays usable; the reverse order would leave such a browser with no
   visible answer at all. */
.learn-body .exam-choice input:checked + .exam-choice-mark {
  border-color: var(--hue-deep, var(--primary));
  box-shadow: inset 0 0 0 4px var(--hue, var(--primary));
}

.learn-body .exam-choice:has(input:checked) {
  border-color: var(--hue-deep, var(--primary));
  background: color-mix(in srgb, var(--hue, var(--primary)) 16%, var(--card));
}

/* The focus ring goes on the row, because the input it belongs to is a pixel
   wide and off in the corner. Without this the paper is unusable by keyboard. */
.learn-body .exam-choice:has(input:focus-visible) {
  outline: 3px solid var(--hue-deep, var(--primary));
  outline-offset: 2px;
}

.learn-body .exam-finish {
  margin-top: 1.6rem;
  text-align: center;
}

.learn-body .exam-go {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  margin-top: 0.6rem;
  padding: 0.75rem 1.8rem;
  border: none;
  border-radius: 999px;
  background: var(--hue-deep, var(--primary));
  color: #fff;
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 800;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 4px 0 color-mix(in srgb, #000 22%, var(--hue-deep, var(--primary)));
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.learn-body .exam-go:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 var(--hue-deep, var(--primary));
}

/* ---- the result ---- */

.learn-body .exam-score {
  padding: 1.2rem 1rem 1.4rem;
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--hue, var(--primary)) 15%, var(--card));
  text-align: center;
}

.learn-body .exam-score-face {
  display: block;
  font-size: clamp(2.6rem, 12vw, 4rem);
  line-height: 1;
}

.learn-body .exam-score-big {
  margin: 0.4rem 0 0;
  font-family: var(--font-display);
  font-size: clamp(2rem, 9vw, 3rem);
  font-weight: 800;
  line-height: 1;
  color: var(--ink);
}

.learn-body .exam-score-of {
  font-size: 0.42em;
  font-weight: 700;
  color: var(--ink-soft);
}

.learn-body .exam-score-band {
  margin: 0.5rem 0 0;
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--hue-deep, var(--primary));
}

.learn-body .exam-score-sub {
  margin: 0.25rem 0 0;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

.learn-body .exam-advice {
  margin-top: 1rem;
  padding: 0.9rem 1rem;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-md);
  color: var(--ink-soft);
  font-size: 0.95rem;
  line-height: 1.5;
}

.learn-body .exam-advice p {
  margin: 0;
}

.learn-body .exam-advice-kicker {
  margin-bottom: 0.2rem !important;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.learn-body .exam-review {
  display: grid;
  gap: 0.9rem;
  margin: 1.4rem 0 0;
  padding: 0;
  list-style: none;
}

/* Right and wrong are told apart by the edge and the sticker on the line, not
   by filling the card with colour. Eleven green cards and one red one is a
   page about the red one. */
.learn-body .exam-review-row {
  padding: 0.9rem 1rem;
  border: 2px solid var(--line);
  border-left-width: 6px;
  border-radius: var(--r-md);
  background: var(--card);
}

.learn-body .exam-review-row.is-right {
  border-left-color: var(--mint-deep, var(--mint));
}

.learn-body .exam-review-row.is-wrong {
  border-left-color: var(--sunny-deep, var(--sunny));
}

.learn-body .exam-review-q {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.02rem;
  line-height: 1.35;
  color: var(--ink);
}

.learn-body .exam-review-mark {
  flex: none;
}

.learn-body .exam-review-q .exam-q-line {
  flex: 1 1 100%;
}

.learn-body .exam-review-opts {
  display: grid;
  gap: 0.3rem;
  margin: 0.6rem 0 0;
  padding: 0;
  list-style: none;
}

.learn-body .exam-review-opt {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.35rem 0.6rem;
  border-radius: var(--r-sm);
  color: var(--ink-soft);
  font-size: 0.98rem;
}

.learn-body .exam-review-opt-tag {
  flex: none;
  width: 1.2rem;
  text-align: center;
}

.learn-body .exam-review-opt.is-answer {
  background: color-mix(in srgb, var(--mint) 22%, var(--card));
  color: var(--ink);
  font-weight: 700;
}

.learn-body .exam-review-opt.is-chosen:not(.is-answer) {
  background: color-mix(in srgb, var(--sunny) 22%, var(--card));
  color: var(--ink);
}

.learn-body .exam-review-tail {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--ink-soft);
}

.learn-body .exam-review-why {
  margin: 0.6rem 0 0;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   31. The question bank: tagging, tick boxes, duplicates and the preview

   The old library row was a flex line, and a long question simply pushed the
   buttons off the right edge of its panel. Everything here is a grid with a
   fixed action column instead, so the row can only ever grow downwards.
   -------------------------------------------------------------------------- */

.qb-page {
  /* min-width:0 lets the grid children shrink; without it a long unbroken
     question makes the whole page scroll sideways. */
  min-width: 0;
}

/* ---- the filter row ---------------------------------------------------- */

.qb-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--gap-sm);
  align-items: end;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-soft);
}

.qb-filter-wide {
  grid-column: span 2;
}

.qb-filter-actions {
  display: flex;
  align-items: flex-end;
}

.qb-filters .field,
.qb-filters select,
.qb-filters input {
  min-width: 0;
  max-width: 100%;
}

@media (max-width: 860px) {
  .qb-filters {
    grid-template-columns: 1fr;
  }

  .qb-filter-wide {
    grid-column: span 1;
  }
}

/* ---- the bulk action bar ----------------------------------------------- */

.qb-bulkbar {
  position: sticky;
  top: 0;
  z-index: 3;
  display: grid;
  grid-template-columns: auto auto minmax(150px, 220px) 1fr;
  align-items: center;
  gap: var(--gap-sm);
  margin-bottom: 0.5rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--card) 92%, var(--sky) 8%);
  backdrop-filter: blur(6px);
}

.qb-bulk-note {
  grid-column: 1 / -1;
}

.qb-bulk-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: flex-end;
}

.qb-bulk-exam select {
  max-height: 68px;
  font-size: 0.78rem;
}

.qb-check {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
  cursor: pointer;
}

.qb-check input {
  width: 17px;
  height: 17px;
  flex: 0 0 17px;
  accent-color: var(--primary);
  cursor: pointer;
}

.qb-check-all {
  color: var(--ink);
  white-space: nowrap;
}

@media (max-width: 860px) {
  .qb-bulkbar {
    grid-template-columns: 1fr;
    position: static;
  }

  .qb-bulk-actions {
    justify-content: flex-start;
  }
}

/* ---- a library row ----------------------------------------------------- */

.qb-list {
  padding: 0;
}

.qb-row {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: start;
  gap: 0.55rem;
  padding: 0.6rem 0.15rem;
  border-bottom: 1px solid var(--line);
}

.qb-row:last-child {
  border-bottom: none;
}

.qb-row:has(input[type="checkbox"]:checked) {
  background: color-mix(in srgb, var(--hue, var(--sky)) 8%, transparent);
  border-radius: var(--r-md);
}

.qb-row .qb-check,
.qb-row-ico {
  margin-top: 0.15rem;
}

.qb-row-body {
  min-width: 0;
  display: grid;
  gap: 0.28rem;
}

/* Three lines of question, then an ellipsis. The full text is one click away
   in the preview, so there is no reason to let one row eat the panel. */
.qb-row-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 0.88rem;
  font-weight: 800;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.qb-row-note {
  font-size: 0.76rem;
  overflow-wrap: anywhere;
}

.qb-row-exams {
  gap: 0.25rem;
}

.qb-row-media {
  max-width: 220px;
  margin: 0.2rem 0 0;
}

.qb-row-actions {
  display: grid;
  gap: 0.3rem;
  justify-items: stretch;
  width: 118px;
}

.qb-row-actions .btn {
  width: 100%;
  justify-content: center;
  white-space: nowrap;
}

@media (max-width: 720px) {
  .qb-row {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .qb-row-ico {
    display: none;
  }

  .qb-row-actions {
    grid-column: 2;
    width: 100%;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
  }
}

/* An exam tag is a chip you can click — it filters the library down to that
   exam's pool. */
.chip-exam {
  background: color-mix(in srgb, var(--grape) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--grape) 30%, transparent);
  color: var(--grape-deep);
  text-decoration: none;
  transition: transform 0.15s var(--ease-pop), background 0.15s ease;
}

.chip-exam:hover {
  background: color-mix(in srgb, var(--grape) 24%, transparent);
  transform: translateY(-1px);
}

[data-theme="dark"] .chip-exam {
  color: var(--ink);
}

/* ---- the exam picker on the add / edit form ---------------------------- */

.qb-exam-picker {
  display: grid;
  gap: 0.45rem;
}

.qb-exam-list {
  display: grid;
  gap: 0.25rem;
  max-height: 260px;
  overflow-y: auto;
  scrollbar-width: thin;
  padding: 0.35rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
}

.qb-exam-option {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.45rem;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background 0.15s ease;
}

.qb-exam-option:hover {
  background: var(--card-soft);
}

.qb-exam-option input {
  width: 17px;
  height: 17px;
  flex: 0 0 17px;
  accent-color: var(--primary);
  cursor: pointer;
}

.qb-exam-option:has(input:checked) {
  background: color-mix(in srgb, var(--grape) 12%, transparent);
}

.qb-exam-label {
  display: grid;
  gap: 0;
  min-width: 0;
}

.qb-exam-label strong {
  font-size: 0.84rem;
  font-weight: 800;
}

.qb-exam-label .small-note {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- the duplicates tab ------------------------------------------------ */

.qb-dupe {
  display: grid;
  gap: 0.4rem;
  padding: 0.7rem 0.15rem;
  border-bottom: 1px solid var(--line);
}

.qb-dupe:last-child {
  border-bottom: none;
}

.qb-dupe-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
}

.qb-dupe-head strong {
  font-size: 0.88rem;
  font-weight: 800;
  overflow-wrap: anywhere;
}

.qb-dupe-rows {
  display: grid;
  gap: 0.25rem;
  padding-left: 0.4rem;
  border-left: 3px solid color-mix(in srgb, var(--sunny) 45%, transparent);
}

.qb-dupe-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.4rem;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--danger) 7%, transparent);
  cursor: pointer;
}

.qb-dupe-row.is-keeper {
  background: color-mix(in srgb, var(--mint) 12%, transparent);
  cursor: default;
}

.qb-dupe-row input {
  width: 17px;
  height: 17px;
  flex: 0 0 17px;
  accent-color: var(--danger);
}

.qb-dupe-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  min-width: 0;
}

/* ---- the preview ------------------------------------------------------- */

/* A fake browser frame around the question, so it reads as "this is what the
   learner sees" rather than "this is another admin panel". */
.q-preview {
  display: grid;
  gap: 0.6rem;
}

.q-preview-screen {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--card);
  box-shadow: var(--shadow-sm);
}

.q-preview-bar {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.4rem 0.6rem;
  background: var(--card-soft);
  border-bottom: 1px solid var(--line);
}

.q-preview-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink-faint) 45%, transparent);
}

.q-preview-url {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-left: 0.4rem;
  padding: 0.15rem 0.5rem;
  border-radius: var(--r-pill);
  background: var(--card);
  border: 1px solid var(--line);
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink-faint);
}

/* This class sits on the .question-panel itself, so it has to unset the
   panel's own bottom margin — the frame provides the spacing now. */
.q-preview-body {
  padding: 0.7rem;
  margin-bottom: 0;
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: color-mix(in srgb, var(--sky) 5%, var(--card));
}

.q-preview-notes {
  padding: 0.7rem 0.85rem;
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
  background: var(--card-soft);
}

.q-preview-notes h3 {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-size: 0.9rem;
}

/* The submit button in a preview looks real but does nothing — better a
   button that plainly cannot be pressed than one that silently fails. */
.is-inert {
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
}

/* ---- the preview dialog ------------------------------------------------ */

.qb-modal[hidden] {
  display: none;
}

.qb-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.8rem;
}

.qb-modal-scrim {
  position: absolute;
  inset: 0;
  background: rgba(10, 14, 40, 0.62);
  backdrop-filter: blur(3px);
}

.qb-modal-card {
  position: relative;
  z-index: 1;
  width: min(680px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  scrollbar-width: thin;
  padding: 0.9rem 1rem 1rem;
  border-radius: var(--r-xl);
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  animation: pop-in 0.28s var(--ease-pop) both;
}

.qb-modal-body {
  margin-top: 0.6rem;
}

body.is-modal-open {
  overflow: hidden;
}

/* ========================================================================== */
/* 32. Bulk upload: the template card, the column guide and the row report     */
/* ========================================================================== */
/* The whole tab is three steps read top to bottom — take the template, send it
   back, and here is what every column means. The report from the upload that
   just happened jumps ahead of all of it, because that is the only thing being
   looked for on the way back. */

/* Two small helpers the report and the guide both want. Defined here rather
   than up with the chips because nothing else on the site needs a chip this
   small, and a list with no bullets is only ever a list of reasons. */
.chip-sm {
  padding: 0.14rem 0.45rem;
  font-size: 0.66rem;
}

.plain-list {
  list-style: none;
  margin: 0.25rem 0 0;
  padding: 0;
}

.plain-list > li + li {
  margin-top: 0.15rem;
}

/* ---- 1 · the template ---------------------------------------------------- */
.qb-template {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 1rem;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--mint) 12%, var(--card)),
    var(--card) 62%
  );
  border-color: color-mix(in srgb, var(--mint) 28%, var(--line));
}

.qb-template-body {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
}

.qb-template-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

/* Decoration, so it goes when the card gets narrow rather than squeezing the
   sentence that carries the meaning. */
.qb-template-ico {
  font-size: 2.6rem;
  line-height: 1;
  opacity: 0.9;
}

/* ---- the report ---------------------------------------------------------- */
.qb-report {
  border-color: color-mix(in srgb, var(--sky) 30%, var(--line));
}

.qb-report-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* A long report scrolls inside its panel instead of pushing the upload form
   off the bottom of the page — the form is the thing you want next. */
.qb-report-scroll {
  max-height: 22rem;
  overflow: auto;
  scrollbar-width: thin;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

.qb-report-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
}

.qb-report-table th,
.qb-report-table td {
  padding: 0.45rem 0.6rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
}

.qb-report-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--card-soft);
  color: var(--ink-soft);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.qb-report-table tbody th {
  width: 3.2rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.qb-report-row.is-rejected {
  background: color-mix(in srgb, var(--rose, #ff5c8a) 7%, transparent);
}

.qb-report-row.is-duplicate {
  background: color-mix(in srgb, var(--ink) 4%, transparent);
}

/* Imported, but with something worth reading. Warmer than the rejected rows
   and cooler than nothing at all, because the difference that matters in this
   table is "went in" versus "did not". */
.qb-report-row.is-warned {
  background: color-mix(in srgb, var(--sun, #ffb020) 9%, transparent);
}

/* The question text is the widest thing in the table and the least important —
   it is there to recognise the row by, so it gets clamped rather than allowed
   to set the table's width. */
.qb-report-q {
  min-width: 0;
  max-width: 22rem;
  overflow-wrap: anywhere;
}

.qb-report-q .small-note {
  display: block;
}

/* ---- the column guide ---------------------------------------------------- */
.qb-cols {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.55rem;
}

.qb-col {
  display: grid;
  gap: 0.3rem;
  align-content: start;
  min-width: 0;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card-soft);
}

/* Required columns are the ones a rejected row is usually missing, so they are
   marked on the card itself and not only in the chip. */
.qb-col.is-required {
  border-left: 3px solid color-mix(in srgb, var(--mint) 62%, transparent);
}

.qb-col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  min-width: 0;
}

.qb-col-head code {
  overflow-wrap: anywhere;
  font-size: 0.78rem;
  font-weight: 800;
}

.qb-col-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

@media (max-width: 720px) {
  .qb-template {
    grid-template-columns: minmax(0, 1fr);
  }

  .qb-template-ico {
    display: none;
  }

  .qb-cols {
    grid-template-columns: minmax(0, 1fr);
  }

  .qb-report-q {
    max-width: none;
  }
}

/* --------------------------------------------------------------------------
   The lab pathway certificate, on screen

   Drawn to echo the printed landscape page rather than to look like the rest
   of the site: a double border, a wash off the top, and the learner's name as
   the loudest thing on it. Everything is centred and nothing is a card, which
   is the point — this page is a thing you show somebody, not a thing you work
   in.

   Sized in `ch` and `%` with no fixed widths, so it survives a long name and a
   narrow phone. `overflow-wrap: anywhere` on the name because some of them are
   very long indeed and a certificate that pushes the page sideways is worse
   than one with a wrapped name.
   -------------------------------------------------------------------------- */

.cert-sheet {
  position: relative;
  padding: 2rem 1.4rem 1.4rem;
  border-radius: var(--r-lg);
  border: 3px solid var(--hue-deep, var(--sky-deep));
  background:
    linear-gradient(
      to bottom,
      color-mix(in srgb, var(--hue, var(--sky)) 20%, transparent),
      var(--card) 62%
    );
  text-align: center;
}

/* The hairline inside the border. A pseudo-element rather than a second box so
   the markup stays one section. */
.cert-sheet::before {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: calc(var(--r-lg) - 6px);
  border: 1px solid color-mix(in srgb, var(--hue-deep, var(--sky-deep)) 45%, transparent);
  pointer-events: none;
}

.cert-brand {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1rem;
  color: var(--ink);
}

.cert-band {
  margin: 0.2rem 0 0;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--hue-deep, var(--sky-deep));
}

.cert-name {
  margin: 1.3rem auto 0;
  padding-bottom: 0.5rem;
  max-width: 26ch;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 6vw, 2.5rem);
  line-height: 1.15;
  color: var(--ink);
  border-bottom: 1px solid color-mix(in srgb, var(--hue-deep, var(--sky-deep)) 55%, transparent);
  overflow-wrap: anywhere;
}

.cert-line {
  margin: 0.9rem 0 0;
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  font-size: 0.95rem;
  color: var(--ink-soft);
}

.cert-title {
  margin: 0.4rem 0 0;
  font-size: clamp(1.15rem, 4vw, 1.6rem);
  color: var(--hue-deep, var(--sky-deep));
  overflow-wrap: anywhere;
}

.cert-blurb {
  margin: 0.4rem auto 0;
  max-width: 60ch;
  font-size: 0.86rem;
  color: var(--ink-soft);
}

/* The fact strip. Auto-fit rather than a fixed column count, because the
   career-track cell is only there for pathways that name a role. */
.cert-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  gap: 0.5rem;
  margin: 1.3rem 0 0;
  padding: 0.8rem 0.6rem;
  border-radius: var(--r-md);
  border: 1px solid color-mix(in srgb, var(--hue-deep, var(--sky-deep)) 40%, transparent);
  background: color-mix(in srgb, var(--hue, var(--sky)) 12%, transparent);
}

/* Flex, not because the pair needs laying out but because `order` below only
   applies to flex or grid children. The markup reads dt-then-dd, which is what
   a definition list is for; the eye wants the number first. */
.cert-facts div {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.cert-facts dt {
  order: 2;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.cert-facts dd {
  order: 1;
  margin: 0 0 0.2rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.cert-labs {
  margin: 0.9rem auto 0;
  max-width: 70ch;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.cert-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.4rem;
  margin: 1.4rem 0 0;
  font-size: 0.74rem;
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  .cert-sheet {
    padding: 1.4rem 1rem 1.1rem;
  }

  .cert-band {
    letter-spacing: 0.2em;
  }

  .cert-foot {
    justify-content: center;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   Lab portfolio

   A record rather than a feed, so the rows are quiet: one line per lab, the
   date pushed to the far end where the eye can run down it, and no card
   chrome. Everything louder than this -- the numbers, the certificates, the
   badges -- is already above and borrows shapes that exist.
   -------------------------------------------------------------------------- */

.pf-track-head {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  flex-wrap: wrap;
  min-width: 0;
}

.pf-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  padding: 0.4rem 0.6rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--hue, var(--sky)) 8%, var(--card-soft));
}

.pf-row + .pf-row {
  margin-top: 0.3rem;
}

.pf-mark {
  font-size: 0.95rem;
  line-height: 1;
}

.pf-row a {
  font-weight: 700;
  color: inherit;
  text-decoration: none;
  /* A lab title can be a whole sentence. It gets the leftover width and wraps
     inside it rather than shoving the date off the end of the row. */
  flex: 1 1 12rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.pf-row a:hover {
  color: var(--primary);
  text-decoration: underline;
}

.pf-when {
  margin-left: auto;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
}

@media (max-width: 560px) {
  /* On a phone the date under the title reads better than a squeezed row. */
  .pf-when {
    margin-left: 0;
    flex-basis: 100%;
  }
}

/* ---------------------------------------------------------------------------
   Discount codes
   --------------------------------------------------------------------------- */

/* The code box at checkout. Monospaced and upper-cased on screen because a
   code is read off a printout or a message and typed character by character --
   the shapes matter more here than they do in a name, and lower case in the
   box next to upper case on the paper reads as a mismatch when it is not one.
   The value is normalised on the server either way; this is only what the
   learner sees while they type. */
.coupon-field input {
  font-family: var(--mono, ui-monospace, "SFMono-Regular", "Menlo", monospace);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.coupon-field input::placeholder {
  /* The hint is a word, not a code, so it keeps the ordinary shapes. */
  font-family: inherit;
  letter-spacing: normal;
  text-transform: none;
}

/* The verdict under the box. It replaces itself in place rather than pushing
   the button down, so a slow answer arriving cannot move the target somebody
   is already reaching for. */
.coupon-field .small-note {
  display: block;
  min-height: 1.25rem;
}

.small-note.is-good {
  color: var(--good, #15803d);
  font-weight: 600;
}

.small-note.is-bad {
  color: var(--bad, #b42318);
}
