/* KinetiX Evaluation Cockpit — base styles
   ---------------------------------------------------------------------------
   Light cockpit, per the KinetiX Admin Dashboard Redesign reference: DM Sans,
   a dotted #E9E9EC field, and white cards with a 24px radius floating on it.

   THE VARIABLE NAMES BELOW ARE LOAD-BEARING. Twenty-five screens were written
   against --bg-1 / --ink-0 / --line and friends, and they inherited this
   redesign for free because the names stayed put and only the values moved.
   Renaming one of these is not a rename — it is a visit to every screen file.

   Two rules that keep the palette honest:

   1. --ink-* descend from near-black to faint grey, and --bg-* ascend from the
      page backdrop to the most recessed surface. A screen that wants "slightly
      quieter text" reaches for the next --ink down and stays legible.
   2. Status colours come in two strengths. --green / --red / --amber / --blue
      are TEXT weights, already darkened to clear 4.5:1 on white. The
      --*-solid pair are the saturated fills from the reference — for bars,
      dots and icon badges, where they sit under white or against the page and
      never carry small text. Using a -solid as a text colour is the one thing
      that reliably breaks contrast here.
*/
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif&display=swap');

:root {
  /* Surfaces — page backdrop up through recessed panels */
  --bg-0: #e9e9ec;   /* the dotted field everything floats on */
  --bg-1: #ffffff;   /* card */
  --bg-2: #f7f7f9;   /* inset tile inside a card */
  --bg-3: #f0f0f3;   /* track / recessed */
  --bg-4: #e4e4e9;   /* deepest inset, scrollbar thumb */

  --line: #ececef;
  --line-soft: #f2f2f4;
  --line-strong: #e8e8ec;

  /* Ink — near-black down to faint */
  --ink-0: #141417;
  --ink-1: #2a2a2e;
  --ink-2: #5a5a63;
  --ink-3: #8a8a93;
  --ink-4: #b0b0b8;

  /* Status — TEXT weights (darkened for contrast on white) */
  --amber: #c97b00;
  --amber-dim: #f0d9ae;
  --amber-bg: rgba(255,159,10,0.14);
  --green: #1f9d4d;
  --green-dim: #bfe9cd;
  --green-bg: rgba(47,190,95,0.12);
  --green-soft: rgba(47,190,95,0.12);
  --red: #d0353a;
  --red-bg: rgba(229,72,77,0.12);
  --blue: #2b62d9;
  --blue-bg: rgba(59,123,246,0.12);
  --violet: #6b5bd2;
  --violet-bg: rgba(107,91,210,0.12);

  /* Status — SOLID fills for bars, dots, icon badges. Never for small text. */
  --green-solid: #2fbe5f;
  --blue-solid:  #3b7bf6;
  --pink-solid:  #f5318d;
  --amber-solid: #ff9f0a;
  --red-solid:   #e5484d;
  --violet-solid:#6b5bd2;
  --grey-solid:  #c9c9cf;

  /* Accent. Blue, because it has to read as a link/highlight on white — the
     old cockpit's warm #ffba5a is invisible here. The primary BUTTON is not
     accent-coloured (see .btn.primary): it is near-black, so retinting the
     accent from the Tweaks panel can never destroy button contrast. */
  --accent: #3b7bf6;
  --accent-ink: #ffffff;   /* text on a filled --accent surface */
  --accent-bg: rgba(59,123,246,0.12);
  --accent-2: #6b5bd2;

  /* Near-black, used for primary buttons and the active nav pill */
  --ink-btn: #141417;
  --ink-btn-hover: #2a2a2e;

  /* The device's top inset, behind a variable so it can be overridden for
     testing (env() cannot be faked otherwise) and tuned on-device.
     iPhone 15/16 Pro report ~59px here; notch-era iPhones ~47px; everything
     else 0. */
  --cx-sat: env(safe-area-inset-top, 0px);

  /* How far the scrolled disc row is pulled up into that inset.
     0 = the discs clear the status bar entirely and sit beneath the clock and
     the Island, which is what the device testing settled on: level with the
     Island, iOS draws its clock and battery glyphs straight over the discs.
     Raise it (e.g. 45px) to move them back up alongside the Island. */
  --cx-island-lift: 0px;

  --radius-card: 24px;
  --radius-tile: 16px;
  --shadow-card: 0 1px 2px rgba(20,20,23,0.03);
  --shadow-float: 0 1px 3px rgba(20,20,23,0.06);
  --shadow-pop: 0 12px 32px rgba(20,20,23,0.14);
}

* { box-sizing: border-box; }
html, body, #root { height: 100%; }
body {
  margin: 0;
  font-family: 'DM Sans', system-ui, sans-serif;
  background-color: var(--bg-0);
  /* The reference's dot field. Fixed so it doesn't parallax under scroll. */
  background-image: radial-gradient(circle, #d9d9de 1.1px, transparent 1.1px);
  background-size: 22px 22px;
  background-attachment: fixed;
  color: var(--ink-1);
  font-size: 13px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
.serif { font-family: 'Instrument Serif', serif; font-weight: 400; }

button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
input, textarea, select { font-family: inherit; }

/* Scrollbars */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #d3d3da; border-radius: 6px; border: 2px solid var(--bg-0); }
::-webkit-scrollbar-thumb:hover { background: #bcbcc5; }

/* ============ App shell ============ */
/* Single column now: the sidebar became the pill top bar (see .cx-topbar).
   .app / .main / .content keep their names because every screen positions
   itself inside .content. */
.app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.main {
  display: flex; flex-direction: column;
  min-width: 0; flex: 1;
}
.content { flex: 1; min-height: 0; }

/* ---- Cockpit top bar: the white pill that holds brand + nav + actions ---- */
/* The top inset is the other half of `viewport-fit=cover` in index.html.
   Opting into the full screen means the layout now extends UNDER the notch
   and the rounded corners; without this the brand name renders behind an
   iPhone's camera housing. env() resolves to 0px on hardware with no cutout,
   so this costs nothing on a laptop. */
.cx-shellpad {
  max-width: 1400px; margin: 0 auto; width: 100%;
  padding: calc(20px + env(safe-area-inset-top, 0px))
           calc(32px + env(safe-area-inset-right, 0px)) 0
           calc(32px + env(safe-area-inset-left, 0px));
}

/* The bar stays put; the sub-nav beneath it scrolls away. z-index sits under
   the notifications panel (200) and the nav widget's veil, above page content. */
.cx-topwrap {
  position: sticky; top: 0; z-index: 150;
  transition: padding 0.2s ease;
}
/* Once the page has moved, content passing beneath the bar is blurred out
   rather than showing through the gaps around the pill. */
/* Saturation stays near 1 here, unlike the glass chrome elsewhere. This band
   sits over whatever is scrolling past — a saturated blur turned the wing
   chart's pink/blue/green into a smear across the top of the screen. The job
   of this layer is to make content disappear, not to tint it. */
.cx-topwrap.scrolled {
  background: rgba(233, 233, 236, 0.86);
  -webkit-backdrop-filter: blur(18px) saturate(105%);
  backdrop-filter: blur(18px) saturate(105%);
  padding-bottom: 10px;
}
.cx-subnavwrap { padding-top: 0; }

.cx-topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px;
  background: #fff;
  border-radius: 999px;
  padding: 10px 14px 10px 12px;
  box-shadow: var(--shadow-float);
  transition: padding 0.2s ease, box-shadow 0.2s ease;
}
/* Condensed: the wordmark goes, the logo and the controls stay. On a phone
   that is ~120px of width returned to the nav pills, and a shorter bar. */
.cx-topbar.condensed { padding: 6px 12px 6px 8px; box-shadow: 0 4px 18px rgba(20,20,23,0.10); }
.cx-topbar.condensed .cx-brand-text {
  max-width: 0; opacity: 0; margin: 0;
  overflow: hidden; pointer-events: none;
}
.cx-brand-text {
  max-width: 320px; opacity: 1;
  overflow: hidden;
  transition: max-width 0.24s ease, opacity 0.16s ease;
  min-width: 0;
}
.cx-topbar.condensed .cx-brand-mark { width: 34px; height: 34px; }
.cx-topbar.condensed .cx-brand-mark img { width: 32px; height: 32px; }
.cx-topbar.condensed .cx-round-btn,
.cx-topbar.condensed .cx-avatar-btn { width: 38px; height: 38px; }

@media (prefers-reduced-motion: reduce) {
  .cx-topwrap, .cx-topbar, .cx-brand-text { transition: none; }
}
/* min-width:0 (not flex-shrink:0) so the brand block is what gives way on a
   narrow phone — the action buttons beside it are fixed-size and cannot. */
.cx-brand { display: flex; align-items: center; gap: 12px; padding-left: 4px; min-width: 0; }
.cx-brand-mark {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  flex-shrink: 0;
}
.cx-brand-mark img { width: 38px; height: 38px; display: block; }
.cx-brand-name {
  font-size: 17px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.1; color: var(--ink-0);
  /* A long school name must truncate rather than push the actions off-screen. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cx-brand-sub  {
  font-size: 10px; font-weight: 600; letter-spacing: 1.4px; color: var(--ink-3); text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cx-nav { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; justify-content: center; }
.cx-nav-pill {
  border: none; border-radius: 999px;
  padding: 11px 18px;
  font-size: 14px; font-weight: 500;
  color: var(--ink-0); background: transparent;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}
.cx-nav-pill:hover { background: var(--bg-2); }
.cx-nav-pill.active { background: var(--ink-btn); color: #fff; font-weight: 600; padding: 11px 22px; }
.cx-nav-pill .cx-nav-dot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--amber-solid); margin-left: 6px; vertical-align: middle;
}

.cx-topbar-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex: 0 0 auto; }
.cx-round-btn {
  width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid var(--line-strong); background: #fff;
  display: grid; place-items: center; color: var(--ink-0);
  position: relative; flex-shrink: 0;
  transition: background 0.12s;
}
.cx-round-btn:hover { background: var(--bg-2); }
.cx-round-btn.sm { width: 40px; height: 40px; }
.cx-round-btn.xs { width: 32px; height: 32px; }
.cx-badge-count {
  position: absolute; top: -2px; right: -4px;
  background: var(--pink-solid); color: #fff;
  font-size: 10px; font-weight: 700;
  border-radius: 999px; padding: 2px 6px;
  border: 2px solid #fff;
  line-height: 1.1;
}
.cx-avatar-btn {
  width: 42px; height: 42px; border-radius: 50%;
  background: linear-gradient(135deg, var(--blue-solid), var(--pink-solid));
  display: grid; place-items: center;
  color: #fff; font-size: 14px; font-weight: 700;
  flex-shrink: 0;
}

/* ---- Secondary nav: the deep screens inside the active section ---- */
.cx-subnav {
  display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap;
  padding: 14px 4px 0;
}
.cx-subnav-item {
  border-radius: 999px;
  padding: 7px 15px;
  font-size: 13px; font-weight: 500;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.cx-subnav-item:hover { background: #fff; color: var(--ink-0); border-color: var(--line-strong); }
.cx-subnav-item.active { background: #fff; color: var(--ink-0); font-weight: 600; border-color: var(--line-strong); box-shadow: var(--shadow-card); }
.cx-subnav-item .count { margin-left: 6px; font-size: 11px; color: var(--ink-3); }

/* ---- Page scaffolding ---- */
.cx-page { max-width: 1400px; margin: 0 auto; padding: 28px 32px 56px; }
.cx-page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; margin-bottom: 24px; flex-wrap: wrap;
}
.cx-eyebrow {
  font-size: 12px; font-weight: 600; letter-spacing: 1px;
  color: var(--ink-3); text-transform: uppercase; margin-bottom: 8px;
}
.cx-h1 { margin: 0; font-size: 34px; font-weight: 600; letter-spacing: -1px; color: var(--ink-0); }
.cx-h2 { margin: 0; font-size: 26px; font-weight: 600; letter-spacing: -0.6px; color: var(--ink-0); }

/* ---- Cards ---- */
.cx-card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 24px;
  box-shadow: var(--shadow-card);
  min-width: 0;
}
.cx-card.tight { padding: 20px 24px; }
.cx-card-title { font-size: 17px; font-weight: 600; color: var(--ink-0); }
.cx-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 16px;
}
.cx-card-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  border-top: 1px solid var(--line-soft);
  padding-top: 14px; margin-top: 16px;
}
.cx-grid { display: grid; gap: 20px; }

/* ---- Stat card (the big-number tile) ---- */
.cx-stat { display: flex; flex-direction: column; gap: 20px; }
.cx-stat-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.cx-stat-icon {
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  flex-shrink: 0;
}
.cx-stat-label { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--ink-0); max-width: 130px; }
.cx-stat-value {
  font-size: 46px; font-weight: 600; letter-spacing: -2px; line-height: 1;
  color: var(--ink-0); font-variant-numeric: tabular-nums;
}
.cx-stat-value.sm { font-size: 30px; letter-spacing: -1px; }
.cx-stat-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.cx-stat-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; }
.cx-delta { font-size: 13px; font-weight: 700; }
.cx-delta.up   { color: var(--green); }
.cx-delta.down { color: var(--red); }
.cx-delta.warn { color: var(--amber); }
.cx-delta.flat { color: var(--ink-3); }

/* ---- Pills used across the cockpit screens ---- */
.cx-pill {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 6px 14px;
  font-size: 13px; font-weight: 500; color: var(--ink-1);
  background: #fff; white-space: nowrap;
}
.cx-pill.tint { border-color: transparent; font-weight: 600; font-size: 12px; padding: 5px 12px; }
.cx-pill.green  { background: var(--green-bg);  color: var(--green); border-color: transparent; }
.cx-pill.amber  { background: var(--amber-bg);  color: var(--amber); border-color: transparent; }
.cx-pill.red    { background: var(--red-bg);    color: var(--red);   border-color: transparent; }
.cx-pill.blue   { background: var(--blue-bg);   color: var(--blue);  border-color: transparent; }
.cx-pill.violet { background: var(--violet-bg); color: var(--violet);border-color: transparent; }
.cx-pill.grey   { background: var(--line-soft); color: var(--ink-2); border-color: transparent; }
.cx-pill.dark   { background: var(--ink-btn); color: #fff; border-color: transparent; font-weight: 600; }

/* ---- Segmented period switch (pill group) ---- */
.cx-seg {
  display: inline-flex; gap: 2px;
  background: #fff; border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 4px;
}
.cx-seg button {
  border: none; border-radius: 999px;
  padding: 9px 18px; font-size: 13px; font-weight: 600;
  background: transparent; color: var(--ink-2);
  white-space: nowrap;
}
.cx-seg button.active { background: var(--ink-btn); color: #fff; }

/* ---- Dark pill button (the reference's primary action) ---- */
.cx-btn-dark {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--ink-btn); color: #fff;
  border: none; border-radius: 999px;
  padding: 13px 22px; font-size: 14px; font-weight: 600;
  white-space: nowrap;
  transition: background 0.12s;
}
.cx-btn-dark:hover { background: var(--ink-btn-hover); }
.cx-btn-dark.sm { padding: 9px 16px; font-size: 13px; }

/* ---- Rows inside cards ---- */
.cx-row {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line-soft);
  min-width: 0;
}
.cx-row:last-child { border-bottom: none; }
.cx-row-title { font-size: 14.5px; font-weight: 600; color: var(--ink-0); }
.cx-row-sub   { font-size: 12.5px; color: var(--ink-3); }
.cx-av {
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.cx-av.sm { width: 38px; height: 38px; font-size: 12px; }
.cx-av.lg { width: 42px; height: 42px; }

/* ---- Meters ---- */
.cx-meter { height: 8px; border-radius: 999px; background: var(--bg-3); overflow: hidden; }
.cx-meter > div { height: 100%; border-radius: 999px; background: var(--blue-solid); }
.cx-meter.thin { height: 6px; }
.cx-meter.xthin { height: 5px; }

/* ---- Inset tile (the grey squares inside cards) ---- */
.cx-tile {
  background: var(--bg-2); border-radius: var(--radius-tile);
  padding: 14px; min-width: 0;
}
.cx-tile-value { font-size: 24px; font-weight: 600; letter-spacing: -1px; color: var(--ink-0); }
.cx-tile-label { font-size: 12px; color: var(--ink-3); }

/* ---- Timeline dot (activity feeds) ---- */
.cx-dot {
  width: 8px; height: 8px; border-radius: 50%;
  margin-top: 6px; flex-shrink: 0;
}

/* ---- Empty / not-connected states, styled like everything else ---- */
.cx-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; text-align: center;
  padding: 34px 24px;
  color: var(--ink-3); font-size: 13px;
}
.cx-empty .cx-empty-mark {
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--bg-2); color: var(--ink-4);
  display: grid; place-items: center;
}
.cx-empty strong { color: var(--ink-1); font-weight: 600; display: block; font-size: 14px; }

.cx-note {
  display: flex; gap: 12px; align-items: center;
  background: var(--bg-2); border-radius: var(--radius-tile);
  padding: 14px 16px;
  font-size: 13px; color: var(--ink-2); line-height: 1.45;
}
.cx-note .cx-note-mark {
  width: 38px; height: 38px; border-radius: 12px;
  background: var(--ink-btn); color: #fff;
  display: grid; place-items: center; flex-shrink: 0;
}

/* ---- Attendance heat-map cell ---- */
.cx-heat { width: 14px; height: 14px; border-radius: 4px; flex-shrink: 0; }
.cx-heat.present    { background: #7ed49b; }
.cx-heat.leave      { background: var(--amber-solid); }
.cx-heat.absent     { background: var(--red-solid); }
.cx-heat.weekend    { background: #efeff2; }
.cx-heat.unrecorded { background: #fff; border: 1px solid var(--line-strong); }
.cx-heat.future     { background: #fff; border: 1px dashed #e3e3e8; }
/* Today's marker. Deliberately a thin ring: at 2px on an unrecorded (white)
   cell it reads as a filled black dot and becomes the loudest thing in a grid
   of hundreds of cells. */
.cx-heat.today      { box-shadow: 0 0 0 1.5px var(--ink-2); }

/* ---- Skeleton grid: three across on desktop, stacked on a phone ---- */
.cx-skelgrid {
  display: grid; gap: 20px;
  grid-template-columns: repeat(var(--cx-skel-cols, 3), minmax(0, 1fr));
}
@media (max-width: 760px) {
  .cx-skelgrid { grid-template-columns: minmax(0, 1fr); gap: 14px; }
}

/* ---- Skeleton shimmer while a screen's data is in flight ---- */
.cx-skel {
  background: linear-gradient(90deg, var(--bg-3) 25%, var(--bg-2) 37%, var(--bg-3) 63%);
  background-size: 400% 100%;
  animation: cx-shimmer 1.3s ease-in-out infinite;
  border-radius: 8px;
}
@keyframes cx-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .cx-skel { animation: none; } }

/* ---- Catalog screens (Classes & Subjects, Chapters, Topics) ----
   A picker column beside a detail pane. The column widths used to be fixed
   px, which is what pushed these three screens past the viewport on a phone;
   `minmax(0, …)` lets both tracks actually shrink, and under 900px the pane
   moves below the picker instead of fighting it for width. */
.cx-catalog-split {
  display: grid;
  grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
  gap: 18px;
}
.cx-catalog-split.narrow { grid-template-columns: minmax(0, 280px) minmax(0, 1fr); }
.cx-catalog-split.even   { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* `flush` is the full-height editor variant (Answer Keys): no gap, the panes
   own their own padding and scroll independently. */
.cx-catalog-split.flush  { gap: 0; }
/* Without this a grid child refuses to shrink below its content, which is the
   whole reason these screens were 180px wider than the phone. */
.cx-catalog-split > * { min-width: 0; }
@media (max-width: 900px) {
  .cx-catalog-split,
  .cx-catalog-split.narrow,
  .cx-catalog-split.even,
  .cx-catalog-split.flush { grid-template-columns: minmax(0, 1fr); }
  /* A pane that was sized to fill a viewport-height column becomes a
     scroll trap once stacked; let both panes size to content instead. */
  .cx-catalog-split.flush > * { min-height: 0; height: auto; overflow: visible; }
}

/* ---- Student roster ----------------------------------------------------
   Six columns totalling 510px of fixed track plus a flexible name column:
   704px of content in a 332px card. On a phone that pushed the whole page
   sideways, which is why the roster looked "shifted" with its left edge cut
   off. Below 760px the row becomes two lines — identity, then the metadata
   that used to be its own columns. */
.cx-roster-split { display: grid; grid-template-columns: minmax(0, 1fr); }
.cx-roster-split.has-detail { grid-template-columns: minmax(0, 1fr) minmax(0, 420px); }
.cx-roster-split > * { min-width: 0; }

.cx-roster-row {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr) 120px 90px 140px 80px;
}
.cx-roster-row > * { min-width: 0; }

/* ---- Generic data rows --------------------------------------------------
   The same fixed-track pattern appears on KinetiX Access and Class Reports.
   One class per column shape on desktop; all of them collapse to the same
   stacked form on a phone: first cell full width, the rest flowing beneath. */
.cx-datarow { display: grid; gap: 12px; align-items: center; }
.cx-datarow.c4  { grid-template-columns: 80px minmax(0,1fr) 110px auto; }
.cx-datarow.c5  { grid-template-columns: 80px minmax(0,1fr) 110px minmax(0,1fr) auto; }
.cx-datarow.c5b { grid-template-columns: 80px minmax(0,1fr) 80px minmax(0,1fr) 100px; }
.cx-datarow.r5  { grid-template-columns: 60px minmax(0,1fr) 110px 130px 100px; }
.cx-datarow > * { min-width: 0; }

/* A row of equal stat tiles (Class Reports header). */
.cx-statrow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }

@media (max-width: 760px) {
  .cx-datarow, .cx-datarow.c4, .cx-datarow.c5, .cx-datarow.c5b, .cx-datarow.r5 {
    display: flex; flex-wrap: wrap; align-items: baseline;
    column-gap: 10px; row-gap: 4px;
  }
  /* The roll/id chip stays inline with the name; everything after wraps. */
  .cx-datarow > :nth-child(2) { flex: 1 1 60%; min-width: 0; }
  .cx-datarow > :nth-child(n + 3) { flex: 0 0 auto; font-size: 11px; text-align: left !important; }
  .cx-statrow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 420px) {
  .cx-statrow { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Teacher roster rows ------------------------------------------------
   name · username (180) · role (80) · scopes (110) · session (80) — about
   450px of fixed track. In a ~334px card on a phone the trailing columns
   simply painted OUTSIDE the card, over the dotted page background, because
   nothing in the chain clipped them.

   (Worth noting for future audits: a document-level scrollWidth check does
   NOT catch this. The pane above these rows carried `overflow: hidden`, so
   the spill was clipped at the pane and never widened the document — it just
   rendered on top of the background. Row-level measurement is what finds it.) */
.cx-tchrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 180px 80px 110px 80px;
}
.cx-tchqueue {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 140px 90px auto;
}
.cx-tchqueue.reset { grid-template-columns: minmax(0, 1fr) 120px auto; }
.cx-tchrow > *, .cx-tchqueue > * { min-width: 0; }

@media (max-width: 760px) {
  /* Identity on the first line; the four metadata cells flow and wrap
     beneath it as one run, same pattern as the activity rows. */
  .cx-tchrow, .cx-tchqueue, .cx-tchqueue.reset {
    display: flex; flex-wrap: wrap; align-items: baseline;
    column-gap: 10px; row-gap: 4px;
  }
  .cx-tchrow > :first-child,
  .cx-tchqueue > :first-child { flex: 1 1 100%; min-width: 0; }
  .cx-tchrow > :not(:first-child),
  .cx-tchqueue > :not(:first-child) {
    flex: 0 0 auto; text-align: left !important; font-size: 11px;
  }
}

/* ---- Student detail pane ------------------------------------------------
   Desktop: a column that TRACKS the viewport while the roster scrolls past.
   Phone:   a bottom sheet, because at ≤1000px the split collapses to one
            column and the pane would otherwise land below 881 rows of roster,
            i.e. somewhere nobody will ever scroll to. */
.cx-roster-detail {
  border-left: 1px solid var(--line);
  background: #fff;
  display: flex; flex-direction: column;
  min-width: 0;
}
.cx-roster-scrim { display: none; }
.cx-sheet-grip { display: none; }

@media (min-width: 1001px) {
  .cx-roster-detail {
    position: sticky;
    top: 16px;
    /* align-self is the load-bearing line. A grid item stretches to the row
       height by default, which leaves a sticky element nothing to travel
       within — it looks like sticky "doesn't work". Shrinking it to its
       content is what lets it stick. */
    align-self: start;
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--line);
    border-radius: 20px;
    box-shadow: var(--shadow-card);
  }
}

@media (max-width: 1000px) {
  .cx-roster-split.has-detail { grid-template-columns: minmax(0, 1fr); }

  .cx-roster-scrim {
    display: block;
    position: fixed; inset: 0; z-index: 245;
    background: rgba(20,20,23,0.34);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    animation: cx-fade-in 0.18s ease-out;
  }
  .cx-roster-detail {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    z-index: 250;
    max-height: 88vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    border: none;
    border-radius: 24px 24px 0 0;
    box-shadow: 0 -14px 44px rgba(20,20,23,0.24);
    padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    animation: cx-sheet-up 0.26s cubic-bezier(.2,.85,.3,1);
  }
  .cx-sheet-grip {
    display: block; flex-shrink: 0;
    width: 40px; height: 4px; border-radius: 999px;
    background: var(--bg-4);
    margin: 10px auto 2px;
  }
  /* The sheet supplies its own top chrome, so the panel header loses the
     border that separated it from a column edge. */
  .cx-roster-detail-head { border-bottom: 1px solid var(--line-soft) !important; }
}

@keyframes cx-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes cx-sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .cx-roster-detail, .cx-roster-scrim { animation: none; }
}
@media (max-width: 760px) {
  /* Two columns, three rows. Source order is:
     1 roll · 2 identity · 3 class · 4 type-pill · 5 login · 6 activity. */
  .cx-roster-row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 4px !important;
    column-gap: 10px !important;
    align-items: center !important;
  }
  .cx-roster-row > :nth-child(1) { grid-column: 1; grid-row: 1; }   /* roll  */
  .cx-roster-row > :nth-child(4) { grid-column: 2; grid-row: 1;     /* type  */
                                   justify-self: end; }
  .cx-roster-row > :nth-child(2) { grid-column: 1 / -1; grid-row: 2; } /* name + email */
  .cx-roster-row > :nth-child(3) { grid-column: 1; grid-row: 3; }   /* class */
  .cx-roster-row > :nth-child(5) { grid-column: 1; grid-row: 4; }   /* login */
  .cx-roster-row > :nth-child(6) { grid-column: 2; grid-row: 3 / span 2;
                                   text-align: right !important; align-self: center; }
}

/* ---- Staff attendance heat-map -----------------------------------------
   Desktop: avatar · name (210px) · the day strip · the figure.
   The strip needs ~820px, so on a phone the whole row used to live inside a
   horizontal scroller and got sliced off at the card edge — a hard vertical
   cut with no hint that anything continued past it.

   Below 760px the row stops being a row: identity on the first line, the day
   strip WRAPPING across the full width beneath it, and the figure pinned to
   the right of the name. Nothing overflows, so nothing is cut. */
.cx-attgrid { min-width: 820px; }
.cx-attrow { display: flex; align-items: center; gap: 16px; }
.cx-attwho { width: 210px; min-width: 0; flex-shrink: 0; }
.cx-attcells { flex: 1; display: flex; align-items: center; gap: 3px; min-width: 0; flex-wrap: wrap; }
.cx-attstat {
  width: 96px; text-align: right; font-size: 13.5px; font-weight: 700;
  color: var(--ink-0); flex-shrink: 0;
}

@media (max-width: 760px) {
  /* No inner min-width: nothing to scroll, so the scroll affordance is
     switched off outright rather than left to the local-gradient trick to
     hide by coincidence. */
  .cx-attgrid { min-width: 0; }
  .cx-attscroll { overflow-x: visible; background: none; }

  .cx-attrow {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 12px; row-gap: 8px;
  }
  /* Explicit placement for all four. Auto-placement put the figure on a
     third line of its own, because the full-width strip above it consumed
     row 2 and the next free cell was row 3. */
  .cx-attrow > .cx-av { grid-column: 1; grid-row: 1; }
  .cx-attwho  { grid-column: 2; grid-row: 1; width: auto; }
  .cx-attstat { grid-column: 3; grid-row: 1; width: auto; }
  /* The strip drops to its own full-width line and wraps there. */
  .cx-attcells {
    grid-column: 1 / -1; grid-row: 2;
    gap: 4px;
  }
  /* Slightly larger cells now that they have the full width to wrap into —
     a 31-day month lands on two comfortable lines instead of one cramped. */
  .cx-attcells .cx-heat { width: 15px; height: 15px; }
}

/* ---- KinetiX Activity rows ---------------------------------------------
   Seven fixed columns (roll · student · class · timestamp · messages ·
   tokens · cost). On a phone that squeezed the student column until Bengali
   test titles wrapped to six lines while the timestamp and cost were cut off
   the right edge. Below 760px the row becomes a stacked block: identity on
   top, then the numbers on one line. */
.cx-actrow {
  display: grid;
  grid-template-columns: 70px minmax(0, 1fr) 130px 110px 90px 80px 80px;
}
.cx-actrow.wide {
  grid-template-columns: 70px minmax(0, 1fr) 110px 110px 110px 110px 130px;
}
.cx-actrow > * { min-width: 0; }

@media (max-width: 760px) {
  /* Flex, not grid: the five trailing values should flow and wrap as one
     run of text. Placing them in grid rows either stacks five near-empty
     lines or overlaps them in a shared cell. */
  .cx-actrow, .cx-actrow.wide {
    display: flex; flex-wrap: wrap; align-items: baseline;
    column-gap: 10px; row-gap: 4px;
  }
  .cx-actrow > :nth-child(1) { order: 0; }                          /* roll */
  .cx-actrow > :nth-child(2) { order: 1; flex: 1 1 auto; min-width: 0; } /* identity */
  /* Zero-height full-width spacer: forces the numbers onto their own line
     without needing a wrapper element in the JSX. */
  .cx-actrow::before { content: ""; order: 2; flex-basis: 100%; height: 0; }
  .cx-actrow > :nth-child(n + 3) {
    order: 3; flex: 0 0 auto; font-size: 11px;
  }
}

/* ---- Liquid glass material --------------------------------------------
   An approximation of Apple's Liquid Glass, built from web primitives.
   Apple ships that material as SwiftUI `.glassEffect()` / UIKit
   `UIGlassEffect` and distributes design resources as Figma kits — there is
   no official CSS to import, so this reconstructs the look from what the
   platform actually gives us.

   Four layers, and all four matter — drop any one and it reads as "blurry
   box" rather than glass:

     1. backdrop-filter   — blur plus a saturation lift, so colour bleeds
                            through instead of going grey
     2. a translucent fill — the body of the material
     3. ::before          — the diffuse sheen, brightest at the top edge
                            where a light source would strike it
     4. ::after           — the specular RIM: a hairline that brightens on
                            two opposing arcs, which is what actually sells
                            the illusion of a thick, refracting edge

   `isolation: isolate` keeps the screen-blended sheen from bleeding onto
   whatever sits behind the element. */
.lg {
  position: relative;
  isolation: isolate;
  background: rgba(255, 255, 255, 0.44);
  -webkit-backdrop-filter: blur(28px) saturate(200%) brightness(1.06);
  backdrop-filter: blur(28px) saturate(200%) brightness(1.06);
  border: 0.5px solid rgba(255, 255, 255, 0.55);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.85),
    inset 0 -1px 1px rgba(255, 255, 255, 0.30),
    inset 0 0 22px rgba(255, 255, 255, 0.16),
    0 10px 30px rgba(20, 20, 23, 0.18),
    0 2px 6px rgba(20, 20, 23, 0.10);
}
.lg > * { position: relative; z-index: 1; }
.lg::before,
.lg::after {
  content: ""; position: absolute; inset: 0;
  border-radius: inherit; pointer-events: none;
}
.lg::before {
  background:
    radial-gradient(125% 85% at 30% -10%, rgba(255,255,255,0.60), rgba(255,255,255,0) 62%),
    linear-gradient(180deg, rgba(255,255,255,0.32), rgba(255,255,255,0) 48%);
  mix-blend-mode: screen;
}
/* The rim. Masked to a 1.5px band at the very edge so the conic gradient
   reads as light travelling around a bevel, not as a tinted fill. */
.lg::after {
  background: conic-gradient(from 200deg,
    rgba(255,255,255,0) 0turn,
    rgba(255,255,255,0.55) 0.10turn,
    rgba(255,255,255,0) 0.30turn,
    rgba(255,255,255,0) 0.62turn,
    rgba(255,255,255,0.42) 0.80turn,
    rgba(255,255,255,0) 1turn);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
          mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
}

/* Dark variant, for the primary control that has to stay legible against a
   bright screen. Same construction, inverted body. */
.lg.dark {
  background: rgba(20, 20, 23, 0.66);
  border-color: rgba(255, 255, 255, 0.22);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.34),
    inset 0 -1px 1px rgba(255, 255, 255, 0.10),
    0 12px 34px rgba(20, 20, 23, 0.40),
    0 3px 8px rgba(20, 20, 23, 0.22);
}
.lg.dark::before {
  background:
    radial-gradient(120% 80% at 32% -8%, rgba(255,255,255,0.42), rgba(255,255,255,0) 58%);
  mix-blend-mode: screen;
}
.lg.dark::after { opacity: 0.65; }

/* Two honest fallbacks. Without backdrop-filter the translucent fill turns
   into unreadable haze, and a user who has asked for less transparency
   should get an opaque surface, not a slightly-less-blurry one. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .lg { background: rgba(255, 255, 255, 0.96); }
  .lg.dark { background: rgba(20, 20, 23, 0.94); }
}
@media (prefers-reduced-transparency: reduce) {
  .lg {
    background: #fff; -webkit-backdrop-filter: none; backdrop-filter: none;
    border-color: var(--line-strong);
  }
  .lg.dark { background: var(--ink-btn); border-color: transparent; }
  .lg::before, .lg::after { display: none; }
}

/* ---- Radial navigation widget (phones + installed PWA) -----------------
   A single floating circle instead of a six-slot bar. Two ways in, sharing
   one pointer pipeline (see CockpitRadialNav in shell.jsx):

     · tap        → the arc opens and stays open; tap a section
     · press-drag → the arc opens under the finger, the section nearest the
                    drag angle highlights, releasing selects it

   Why a widget and not a bar: six labelled slots at 390px were already
   ellipsising ("Academi…") and dropped to icons-only below 360px. An arc
   gives every section a full label at any width, and returns the ~66px the
   bar was permanently taking off the bottom of every screen.

   Hidden by default, revealed under 760px, so desktop never pays for it. */
.cx-radialnav { display: none; }

@media (max-width: 760px) {
  .cx-radialnav {
    display: block;
    position: fixed;
    left: 0; right: 0;
    bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    height: 0;                 /* the children are absolutely placed */
    z-index: 240;
    pointer-events: none;      /* only the button and items are hit-testable */
  }

  /* The trigger — bottom-CENTRE. Centred with left:50% + translateX(-50%),
     which means EVERY other transform on this element must re-state that
     translate or the button jumps to the right on press/open. */
  .cx-fab {
    position: absolute; left: 50%; right: auto; bottom: 0;
    transform: translateX(-50%);
    width: 60px; height: 60px; border-radius: 50%;
    display: grid; place-items: center;
    color: #fff;
    pointer-events: auto;
    touch-action: none;        /* we own the drag; stop the page scrolling */
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.2s cubic-bezier(.2,.8,.3,1),
                background 0.18s, backdrop-filter 0.18s;
  }
  .cx-fab:active { transform: translateX(-50%) scale(0.93); }
  /* Reading downward: the trigger recedes so it stops covering the row under
     it. Scrolling back up restores it immediately. Scale stops at 0.72 (43px)
     rather than going smaller — a transform shrinks the hit area with the
     visual, and below this it stops being reliably tappable. */
  .cx-radialnav.mini .cx-fab {
    transform: translateX(-50%) scale(0.72);
    opacity: 0.62;
  }
  .cx-radialnav.mini .cx-fab:active { transform: translateX(-50%) scale(0.68); }
  /* Glass "flexes" under pressure: the blur deepens as the control is held. */
  .cx-fab.open {
    transform: translateX(-50%) rotate(45deg);
    -webkit-backdrop-filter: blur(36px) saturate(220%);
    backdrop-filter: blur(36px) saturate(220%);
  }
  .cx-fab.open .cx-fab-face { transform: rotate(-45deg); }
  .cx-fab-face { display: grid; place-items: center; transition: transform 0.2s; }
  /* Dimmer behind the open arc — also the tap-to-close target. */
  .cx-radial-veil {
    position: fixed; inset: 0;
    background: rgba(20,20,23,0.22);
    -webkit-backdrop-filter: blur(3px) saturate(115%);
    backdrop-filter: blur(3px) saturate(115%);
    pointer-events: auto;
    animation: cx-veil-in 0.18s ease-out;
  }
  @keyframes cx-veil-in { from { opacity: 0; } to { opacity: 1; } }

  /* Arc items, centred on the trigger and displaced by --x/--y (written per
     item by the component). margin-left/bottom are computed so an item's
     centre coincides with the trigger's centre at rest:
       margin-left = −56/2 = −28     bottom = 30 − 28 = 2 */
  .cx-radial-item {
    position: absolute; left: 50%; right: auto; bottom: 2px;
    margin-left: -28px;
    width: 56px; height: 56px;
    border-radius: 50%;
    color: var(--ink-1);
    display: grid; place-items: center; align-content: center;
    gap: 1px;
    pointer-events: auto;
    transform: translate(var(--x), var(--y)) scale(1);
    transition: transform 0.26s cubic-bezier(.2,.9,.3,1.15),
                opacity 0.18s, background 0.14s, color 0.14s, box-shadow 0.18s;
  }
  /* Denser than the base material: these sit over a dimmed veil, and at the
     base 0.44 fill the 8.5px labels dropped below a comfortable contrast. */
  .cx-radial-item { background: rgba(255, 255, 255, 0.72); }
  .cx-radial-item .cx-radial-label {
    font-size: 8.5px; font-weight: 700; letter-spacing: -0.15px;
    max-width: 52px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Collapsed: every item sits under the trigger, invisible. */
  .cx-radialnav:not(.open) .cx-radial-item {
    transform: translate(0, 0) scale(0.4);
    opacity: 0; pointer-events: none;
  }
  .cx-radial-item.current { color: var(--ink-0); }
  .cx-radial-item.current .cx-radial-label { text-decoration: underline; text-underline-offset: 2px; }
  /* Armed by the drag. The glass thickens and lifts, which is the feedback
     that a release right now will commit to this section. */
  .cx-radial-item.armed {
    background: rgba(20, 20, 23, 0.80);
    border-color: rgba(255,255,255,0.28);
    color: #fff;
    transform: translate(var(--x), var(--y)) scale(1.18);
    box-shadow:
      inset 0 1px 1px rgba(255,255,255,0.34),
      0 14px 34px rgba(20,20,23,0.38);
  }

  /* Content clears the trigger; far less than the old bar needed. */
  .cx-page,
  .content > div:not(.cx-page) {
    padding-bottom: calc(94px + env(safe-area-inset-bottom, 0px)) !important;
  }

  /* The notifications dropdown. As an absolutely-positioned 380px panel it
     was wider than a 390px screen once the bell's offset was added, so it
     hung off the edge and clipped its own text. Pinned to the viewport
     instead, with the inline width/top overridden. */
  .cx-notifpanel {
    position: fixed !important;
    top: calc(72px + env(safe-area-inset-top, 0px)) !important;
    left: 12px !important; right: 12px !important;
    width: auto !important;
    max-height: calc(100vh - 150px - env(safe-area-inset-top, 0px)) !important;
    border-radius: 20px !important;
    overscroll-behavior: contain;   /* stop the page scrolling underneath */
  }
  /* A notification title is a full sentence; it must wrap, not truncate. */
  .cx-notifpanel [style*="font-size: 12px"] { overflow-wrap: anywhere; }

  /* The floating top bar is the same class of chrome as the widget, so it
     takes the same material — otherwise the two read as different systems. */
  .cx-topbar {
    background: rgba(255,255,255,0.62);
    -webkit-backdrop-filter: blur(24px) saturate(190%);
    backdrop-filter: blur(24px) saturate(190%);
    border: 0.5px solid rgba(255,255,255,0.6);
    box-shadow:
      inset 0 1px 1px rgba(255,255,255,0.8),
      0 6px 22px rgba(20,20,23,0.10);
  }

  /* ---- Scrolled: the bar dissolves into four floating discs -------------
     Logo on the left, the three controls on the right, nothing between them.
     Only possible on this breakpoint because the section pills are already
     hidden here (see .cx-nav above) — the bar really is just four elements,
     so removing its chrome costs nothing. On desktop the pills live in the
     bar, so it keeps its condensed form instead.

     The wrapper stops the background band and, critically, stops capturing
     pointer events: an invisible full-width strip across the top of the
     screen that still swallowed taps would be worse than the bar it
     replaced. The two groups switch them back on for themselves. */
  .cx-topwrap.scrolled {
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding-bottom: 0;
    pointer-events: none;
    position: sticky;

    /* Pull the disc row up into the top inset so it flanks the Dynamic
       Island: logo to its left, the three controls to its right. `max()`
       keeps a sane 8px on any device with no inset at all, where there is
       no Island to flank and the discs simply sit near the top edge.
       The left/right insets matter in landscape, where the Island moves to
       the side and the safe area narrows. */
    --cx-disc-top: max(8px, calc(var(--cx-sat) - var(--cx-island-lift)));
    padding-top: var(--cx-disc-top);
    padding-left: calc(10px + env(safe-area-inset-left, 0px));
    padding-right: calc(10px + env(safe-area-inset-right, 0px));
  }
  /* The fade now has to begin at the physical top of the screen, not at the
     padded box — otherwise it starts below the discs and they float on raw
     content. It grows by the same amount it is pulled up by. */
  .cx-topwrap.scrolled::before {
    top: calc(-1 * var(--cx-disc-top));
    height: calc(104px + var(--cx-disc-top));
  }
  /* A short fade, not a bar. Each disc blurs what is directly behind it, but
     the gaps between them do not — without this, headings and figures collide
     with the controls and neither is readable. The gradient is page colour at
     the very top, fully transparent by 74px, so content dissolves upward
     instead of running into the discs.

     It is the ::before of the wrapper, so it paints behind .cx-topbar without
     needing a z-index, and inherits `pointer-events: none` from the wrapper. */
  .cx-topwrap.scrolled::before {
    content: "";
    position: absolute; left: 0; right: 0; top: 0;
    /* Tall enough that the discs (which occupy roughly 10–56px) always sit on
       dissolved ground. At 74px the fade ran out above the first line of card
       text and headings still collided with the logo. */
    height: 104px;
    background: linear-gradient(to bottom,
      var(--bg-0) 0%, var(--bg-0) 38%, rgba(233, 233, 236, 0.55) 68%, rgba(233, 233, 236, 0) 100%);
  }
  .cx-topwrap.scrolled .cx-topbar {
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: none;
    box-shadow: none;
    padding: 0;
  }
  .cx-topwrap.scrolled .cx-brand,
  .cx-topwrap.scrolled .cx-topbar-actions { pointer-events: auto; }
  .cx-topwrap.scrolled .cx-brand { padding-left: 0; gap: 0; }

  /* The logo disc — deliberately the largest of the four. */
  .cx-topwrap.scrolled .cx-brand-mark {
    width: 46px; height: 46px; border-radius: 50%;
    background: rgba(255,255,255,0.74);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    backdrop-filter: blur(20px) saturate(160%);
    border: 0.5px solid rgba(255,255,255,0.62);
    box-shadow:
      inset 0 1px 1px rgba(255,255,255,0.85),
      0 6px 20px rgba(20,20,23,0.18);
  }
  .cx-topwrap.scrolled .cx-brand-mark img { width: 27px; height: 27px; }

  /* The three controls: smaller, and each its own disc.
     The sizes here are not taste, they are the flank arithmetic. The Dynamic
     Island is ~126pt wide and centred, so on the narrowest current Pro
     (393pt) each side has ~133pt, less ~10pt of page padding = ~123pt of
     usable flank. Three 36px discs with 6px gaps is 120px, which clears the
     Island's edge with a little to spare. At 37px/7px it was 125px and the
     first button tucked under the Island. */
  .cx-topwrap.scrolled .cx-topbar-actions { gap: 6px; }
  .cx-topwrap.scrolled .cx-round-btn,
  .cx-topwrap.scrolled .cx-avatar-btn { width: 36px; height: 36px; }
  .cx-topwrap.scrolled .cx-round-btn {
    background: rgba(255,255,255,0.74);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    backdrop-filter: blur(20px) saturate(160%);
    border-color: rgba(255,255,255,0.62);
    box-shadow:
      inset 0 1px 1px rgba(255,255,255,0.85),
      0 6px 20px rgba(20,20,23,0.18);
  }
  .cx-topwrap.scrolled .cx-round-btn svg { width: 16px; height: 16px; }
  .cx-topwrap.scrolled .cx-avatar-btn {
    font-size: 12px;
    box-shadow: 0 6px 20px rgba(20,20,23,0.22);
  }
  .cx-topwrap.scrolled .cx-badge-count {
    font-size: 9px; padding: 1px 5px; border-width: 1.5px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cx-fab, .cx-radial-item, .cx-fab-face { transition: none; }
  .cx-radial-veil { animation: none; }
}

/* ---- Responsive: collapse the multi-column grids before they crush ---- */
@media (max-width: 1180px) {
  .cx-grid.cols-3, .cx-grid.cols-4 { grid-template-columns: repeat(2, 1fr) !important; }
  .cx-grid.split, .cx-grid.split-wide, .cx-grid.split-narrow { grid-template-columns: 1fr !important; }
}

@media (max-width: 760px) {
  .cx-shellpad {
    padding: calc(10px + env(safe-area-inset-top, 0px))
             calc(12px + env(safe-area-inset-right, 0px)) 0
             calc(12px + env(safe-area-inset-left, 0px));
  }
  /* Tightened: the chrome already costs ~126px on a phone once the sub-nav
     is showing, and an 18px page pad on top of the sub-nav's own 14px read as
     dead space before anything useful appeared. */
  .cx-page { padding: 10px 12px 0; }
  .cx-page-head { margin-bottom: 16px; }
  .cx-eyebrow { margin-bottom: 5px; }
  /* Screens that lay themselves out (rosters) rather than using .cx-page. */
  .cx-roster-split > div:first-child { padding: 12px 14px !important; }
  .cx-grid.cols-3, .cx-grid.cols-4 { grid-template-columns: 1fr !important; }
  .cx-grid { gap: 14px; }

  /* The section pills live in the bottom bar now; the top bar keeps only
     identity and the two global actions, so it costs ~64px instead of 324. */
  .cx-nav { display: none; }
  .cx-topbar { padding: 8px 10px 8px 10px; }
  .cx-brand-mark { width: 34px; height: 34px; }
  .cx-brand-mark img { width: 32px; height: 32px; }
  .cx-brand-name { font-size: 15px; }
  .cx-brand-sub { font-size: 9px; letter-spacing: 1.1px; }
  .cx-round-btn { width: 38px; height: 38px; }
  .cx-avatar-btn { width: 38px; height: 38px; font-size: 13px; }
  .cx-topbar-actions { gap: 6px; }

  /* Sub-nav becomes a swipeable strip instead of a block that wraps to five
     rows. The negative margin lets it bleed to the screen edge so the last
     chip doesn't look clipped mid-word. */
  .cx-subnav {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    margin: 0 -12px;
    padding: 8px 12px 2px;
  }
  .cx-subnav::-webkit-scrollbar { display: none; }
  .cx-subnav-item { flex: 0 0 auto; scroll-snap-align: start; min-height: 38px; }

  /* (Bottom clearance for the radial trigger is set with the widget's own
     rules above, so the two can never drift apart.) */

  .cx-h1 { font-size: 25px; letter-spacing: -0.6px; }
  .cx-h2 { font-size: 20px; }
  .cx-eyebrow { font-size: 11px; }
  .cx-page-head { margin-bottom: 18px; gap: 14px; }
  .cx-card { padding: 18px; border-radius: 20px; }
  .cx-card.tight { padding: 16px 18px; }
  .cx-stat-value { font-size: 38px; letter-spacing: -1.5px; }
  .cx-stat-value.sm { font-size: 26px; }
  .cx-stat { gap: 16px; }
  .cx-stat-label { font-size: 15px; max-width: none; }

  /* A stat card's footer is a label and a pill; side by side at 390px they
     collide, so the pill drops to its own line. */
  .cx-stat-foot { flex-direction: column; align-items: flex-start; gap: 8px; }

  /* Full-width primary actions read as buttons on a phone, not as afterthoughts. */
  .cx-page-head .cx-btn-dark { width: 100%; justify-content: center; }
  .cx-seg { width: 100%; overflow-x: auto; scrollbar-width: none; }
  .cx-seg::-webkit-scrollbar { display: none; }
  .cx-seg button { flex: 1 0 auto; }

  /* Tables stay tabular and scroll inside their card. */
  .cx-tablecard { border-radius: 18px; }
  table.cx-table thead th, table.cx-table tbody td { padding: 11px 12px; }

  /* Card headers carry filters, a search box and an action. Buttons no longer
     shrink (see the coarse-pointer rule), so the header must wrap or it is
     the header that pushes the page sideways. */
  .card-head { flex-wrap: wrap; row-gap: 8px; }
  .card-head .input, .card-head .select { min-width: 0; }

  .cx-emptycard { padding: 30px 18px; border-radius: 18px; }
  .cx-note { padding: 12px 14px; }

  /* The A4 answer-key preview shrinks to the screen; its mm paddings would
     otherwise eat most of a 390px width. Print still gets the real thing. */
  .ak-a4 { padding: 18px 16px !important; font-size: 12px !important; }
}

@media print {
  /* Paper is the one place the A4 sheet must keep its true width, and the
     app chrome must not appear at all. */
  .ak-a4 { width: 210mm !important; max-width: none !important; padding: 16mm 14mm !important; }
  .cx-radialnav, .cx-shellpad, .cx-subnav { display: none !important; }
  body { background: #fff !important; }
}

/* ---- Touch input: enforce a real hit area regardless of visual size ----
   The audit found 173 sub-32px targets on one screen alone. Rather than
   resize every button, guarantee the hit area on pointers that need it.
   `pointer: coarse` scopes this to touch, so mouse users keep the compact
   density the dense screens were designed for. */
@media (pointer: coarse) {
  .btn, .cx-pill, .cx-subnav-item, .seg button, .cx-seg button, .cx-round-btn {
    min-height: 36px;
  }
  /* Native form controls default to ~28px here, which is under the touch
     guideline and awkward to hit while holding a phone one-handed.
     16px on inputs also stops iOS Safari zooming the page on focus. */
  .input, .select, .textarea { min-height: 40px; }
  .input, .select { font-size: 16px; }
  .btn.sm, .pill { min-height: 32px; }
  /* Width matters as much as height: the test library rows carry icon-only
     buttons that were 19–25px across even at a 32px height.
     `flex-shrink: 0` is the other half — min-width alone still let a flex
     parent squeeze a text button ("Admit in Ledger ↗") to 33px and clip it. */
  .btn, .btn.sm {
    padding-inline: 12px; min-width: 34px;
    justify-content: center; flex-shrink: 0;
  }
  /* Anything genuinely small (icon-only affordances, chips in dense grids)
     gets an invisible expanded hit area instead of a bigger box. */
  .btn.icon-only, .chk { position: relative; }
  .btn.icon-only::after, .chk::after {
    content: ""; position: absolute;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 44px; height: 44px;
  }
}

/* ---- Small-phone type floor -------------------------------------------
   Screens built for a desktop table drop to 10px and below, which is not
   readable on a handset. This lifts the floor without touching the
   deliberate 11px metadata sizes. */
@media (max-width: 560px) {
  .content { font-size: 13px; }
  .content [style*="font-size: 9"], .content [style*="fontSize: 9"] { font-size: 11px !important; }
  .content [style*="font-size: 10"], .content [style*="fontSize: 10"] { font-size: 11.5px !important; }
  .mono { font-size: inherit; }
  /* Long unbroken strings (emails, usernames, IDs) are the usual cause of a
     table forcing the page wider than the screen. */
  table.cx-table td, table.tests td, .cx-row, .cx-tile { overflow-wrap: anywhere; }
}

/* ============ Legacy shell bits (kept for screens that still use them) ============ */
.topbar {
  height: 52px;
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center;
  padding: 0 20px;
  gap: 16px;
  background: var(--bg-1);
}
.crumbs { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-3); }
.crumbs .sep { color: var(--ink-4); }
.crumbs .here { color: var(--ink-0); }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }

.kbd {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 10px; padding: 2px 5px;
  border: 1px solid var(--line-strong); border-radius: 3px;
  color: var(--ink-2); background: var(--bg-2);
}

/* ============ Buttons ============ */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px;
  border-radius: 999px;
  font-size: 12px; font-weight: 500;
  border: 1px solid var(--line-strong);
  background: #fff;
  color: var(--ink-1);
  transition: background 0.12s, border-color 0.12s;
  white-space: nowrap;
}
.btn:hover { background: var(--bg-2); border-color: #dcdce3; }
/* Near-black, NOT --accent: the accent is user-tintable from the Tweaks panel
   and a pale tint would leave this button unreadable. */
.btn.primary {
  background: var(--ink-btn); color: #fff;
  border-color: transparent; font-weight: 600;
}
.btn.primary:hover { background: var(--ink-btn-hover); }
.btn.success { background: var(--green-solid); color: #fff; border-color: transparent; font-weight: 600; }
.btn.success:hover { background: #29a854; }
.btn.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn.ghost:hover { background: var(--bg-2); color: var(--ink-0); }
.btn.danger { color: var(--red); border-color: rgba(229,72,77,0.3); background: #fff; }
.btn.danger:hover { background: var(--red-bg); }
.btn.sm { padding: 5px 10px; font-size: 11px; }
.btn.lg { padding: 9px 18px; font-size: 13px; }
.btn.icon-only { padding: 6px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ============ Form controls ============ */
.field { display: flex; flex-direction: column; gap: 5px; }
.label {
  font-size: 11px; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.08em;
  font-weight: 600;
}
.input, .select, .textarea {
  background: #fff;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 8px 11px;
  color: var(--ink-0);
  font-size: 13px;
  outline: none;
  transition: border-color 0.12s, box-shadow 0.12s;
  width: 100%;
}
.input::placeholder, .textarea::placeholder { color: var(--ink-4); }
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.textarea { resize: vertical; min-height: 60px; }

/* Native pickers follow the light theme now. */
input[type="date"], input[type="time"], input[type="datetime-local"] { color-scheme: light; }
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.55;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' stroke='%238a8a93' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
}

/* ============ Pills / badges ============ */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  border: 1px solid var(--line-strong);
  background: var(--bg-2);
  color: var(--ink-2);
  white-space: nowrap;
}
.pill .swatch { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.pill.amber { color: var(--amber); border-color: transparent; background: var(--amber-bg); }
.pill.green { color: var(--green); border-color: transparent; background: var(--green-bg); }
.pill.red   { color: var(--red);   border-color: transparent; background: var(--red-bg); }
.pill.blue  { color: var(--blue);  border-color: transparent; background: var(--blue-bg); }
.pill.violet{ color: var(--violet);border-color: transparent; background: var(--violet-bg); }
.pill.solid-amber { background: var(--amber-solid); color: #fff; border-color: transparent; font-weight: 600; }

/* ============ Card ============ */
.card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow-card);
}
.card-head {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-soft);
  display: flex; align-items: center; gap: 10px;
}
.card-title { color: var(--ink-0); font-weight: 600; font-size: 14px; }
.card-body { padding: 16px; }

/* ============ Tables ============
   ONE treatment for every table in the cockpit. `table.tests` is the original
   class (test library, dashboard lists); `table.cx-table` is the same thing
   under a name the newer screens use. They are deliberately a single rule set
   — two table looks in one product is the thing this consolidates away.

   Wrap a table in `.cx-tablecard` when it is not already inside a card: on the
   dotted page background an unwrapped table reads as loose text, which is
   exactly how the Personnel screen looked before this existed. */
.cx-tablecard {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 20px;
  box-shadow: var(--shadow-card);
  overflow: hidden;      /* clips the header's square corners to the radius */
}
.cx-tablecard > .cx-tablecard-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line-soft);
}
.cx-tablecard-title { font-size: 15px; font-weight: 600; color: var(--ink-0); }
/* Horizontal scroll belongs to the wrapper, never the page.
   ------------------------------------------------------------------------
   The four layered backgrounds are the classic scroll-shadow trick, and they
   are why the content no longer just STOPS at the card edge. The two white
   gradients are `background-attachment: local`, so they travel with the
   content and cover the shadow when there is nothing more that way; the two
   radial shadows are `scroll`, so they stay pinned to the edges. Net effect:
   a soft shade appears only on the side that has more content, and vanishes
   when you reach the end.

   Without this a clipped row reads as a rendering fault rather than as
   "there is more, swipe". */
.cx-tablescroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  background:
    linear-gradient(to right, #fff 32%, rgba(255,255,255,0)) left center,
    linear-gradient(to left,  #fff 32%, rgba(255,255,255,0)) right center,
    radial-gradient(farthest-side at 0    50%, rgba(20,20,23,0.13), rgba(20,20,23,0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(20,20,23,0.13), rgba(20,20,23,0)) right center;
  background-repeat: no-repeat;
  background-size: 36px 100%, 36px 100%, 15px 100%, 15px 100%;
  background-attachment: local, local, scroll, scroll;
}

table.tests, table.cx-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 13px;
}
table.tests thead th, table.cx-table thead th {
  text-align: left;
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
  background: #fff;
  position: sticky; top: 0;
  z-index: 1;
  white-space: nowrap;
}
table.tests tbody td, table.cx-table tbody td {
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
  color: var(--ink-1);
}
/* The last row's rule doubles the card's own border. */
table.tests tbody tr:last-child td,
table.cx-table tbody tr:last-child td { border-bottom: none; }
table.tests tbody tr:hover, table.cx-table tbody tr:hover { background: var(--bg-2); }

/* Row-level modifiers */
table.cx-table tbody tr.muted-row { opacity: 0.55; }
table.cx-table td.num, table.cx-table th.num {
  text-align: right; font-variant-numeric: tabular-nums;
}
table.cx-table td.actions, table.cx-table th.actions {
  text-align: right; white-space: nowrap;
}
/* Action buttons sit quiet until the row is hovered, so a table of forty rows
   is not forty competing links. */
table.cx-table td.actions .btn { opacity: 0.75; }
table.cx-table tbody tr:hover td.actions .btn { opacity: 1; }
/* Primary cell: the name/title column. */
table.cx-table td .t-main { color: var(--ink-0); font-weight: 600; font-size: 13.5px; }
table.cx-table td .t-sub  { color: var(--ink-3); font-size: 11.5px; margin-top: 1px; }

/* A compact variant for sub-tables nested inside a detail panel, where the
   card chrome would be a box inside a box. */
table.cx-table.compact { font-size: 12px; }
table.cx-table.compact thead th { padding: 8px 10px; font-size: 10px; }
table.cx-table.compact tbody td { padding: 8px 10px; }

/* The "nothing here yet" panel that stands in for a table. Previously a dashed
   outline, which is invisible against the dotted page — an empty list looked
   like a rendering failure. Same white card as the table it replaces, so the
   page keeps its shape whether or not there are rows. */
.cx-emptycard {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 20px;
  box-shadow: var(--shadow-card);
  padding: 40px 24px;
  text-align: center;
  color: var(--ink-3);
  font-size: 13.5px;
}
.cx-emptycard strong { display: block; color: var(--ink-0); font-size: 15px; font-weight: 600; margin-bottom: 6px; }

/* misc */
.divider { height: 1px; background: var(--line); margin: 0; border: 0; }

.scanline-bg {
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0,
    transparent 6px,
    rgba(20,20,23,0.022) 6px,
    rgba(20,20,23,0.022) 7px
  );
}

/* progress bar */
.progress { height: 5px; background: var(--bg-3); border-radius: 999px; overflow: hidden; }
.progress > div { height: 100%; background: var(--blue-solid); border-radius: 999px; }
.progress.green > div { background: var(--green-solid); }

/* checkbox */
.chk {
  width: 15px; height: 15px;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  background: #fff;
  display: inline-grid; place-items: center;
  flex-shrink: 0;
}
.chk.on { background: var(--accent); border-color: var(--accent); }
.chk.on::after {
  content: "";
  width: 7px; height: 4px;
  border-left: 1.5px solid #fff;
  border-bottom: 1.5px solid #fff;
  transform: rotate(-45deg) translate(0, -1px);
}

.muted { color: var(--ink-3); }
.dim { color: var(--ink-2); }

/* tabs (segmented) */
.seg {
  display: inline-flex;
  background: var(--bg-2);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 3px;
  gap: 2px;
}
.seg button {
  font-size: 11.5px;
  padding: 5px 12px;
  border-radius: 999px;
  color: var(--ink-2);
  font-weight: 500;
}
.seg button.active {
  background: #fff;
  color: var(--ink-0);
  font-weight: 600;
  box-shadow: var(--shadow-card);
}

/* answer-card grid */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 14px;
}

.answer-card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 16px;
  display: flex; flex-direction: column;
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: border-color 0.15s, transform 0.15s;
}
.answer-card:hover { border-color: var(--line-strong); }
.answer-card.pending { border-color: rgba(255,159,10,0.45); }
.answer-card.confirmed { border-color: rgba(47,190,95,0.45); background: linear-gradient(180deg, rgba(47,190,95,0.05), transparent 40%), #fff; }
.answer-card.overridden { border-color: rgba(59,123,246,0.45); background: linear-gradient(180deg, rgba(59,123,246,0.05), transparent 40%), #fff; }
.answer-card.failed { border-color: rgba(229,72,77,0.45); }
.answer-card.flagged { border-color: rgba(255,159,10,0.7); box-shadow: 0 0 0 3px rgba(255,159,10,0.12); }

.answer-card .card-top {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line-soft);
}
.answer-card .student-name { color: var(--ink-0); font-weight: 600; font-size: 13px; }
.answer-card .roll { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--ink-3); }

/* Scanned answer crop — deliberately still "paper", on any theme. */
.crop {
  position: relative;
  background: #f4f1ea;
  color: #1a1a1a;
  font-family: 'Kalam', 'Caveat', cursive;
  padding: 16px;
  min-height: 130px;
  font-size: 15px;
  line-height: 1.4;
  border-bottom: 1px solid var(--line-soft);
}
.crop::before {
  content: "";
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(
    0deg, transparent 0 22px, rgba(0,0,0,0.06) 22px 23px
  );
  pointer-events: none;
}
.crop .corner {
  position: absolute; top: 6px; right: 8px;
  font-size: 10px;
  color: #888;
  font-family: 'IBM Plex Mono', monospace;
}
.crop.bn { font-family: 'Hind Siliguri', 'Noto Serif Bengali', serif; font-size: 17px; }

.transcription {
  padding: 10px 12px;
  font-size: 12px;
  color: var(--ink-2);
  line-height: 1.5;
  border-bottom: 1px solid var(--line-soft);
  background: var(--bg-2);
  position: relative;
}
.transcription .ttl {
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  margin-bottom: 4px;
  display: flex; align-items: center; gap: 6px;
}

.criteria-list { padding: 10px 12px; border-bottom: 1px solid var(--line-soft); }
.criterion {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 4px 0;
  font-size: 11.5px;
  color: var(--ink-2);
}
.criterion .ck {
  width: 15px; height: 15px; flex-shrink: 0; margin-top: 1px;
  border-radius: 4px; display: grid; place-items: center;
  font-size: 9px;
}
.criterion .ck.match { background: var(--green-bg); color: var(--green); border: 1px solid rgba(47,190,95,0.35); }
.criterion .ck.partial { background: var(--amber-bg); color: var(--amber); border: 1px solid rgba(255,159,10,0.35); }
.criterion .ck.miss { background: var(--bg-2); color: var(--ink-3); border: 1px solid var(--line-strong); }
.criterion .marks { margin-left: auto; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--ink-3); }
.criterion .marks.has { color: var(--green); }

.card-bottom { padding: 10px 12px; display: flex; align-items: center; gap: 8px; }

.score-display {
  display: flex; align-items: baseline; gap: 2px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 17px;
  font-weight: 600;
  color: var(--ink-0);
}
.score-display .total { font-size: 11px; color: var(--ink-3); }
.score-display.green { color: var(--green); }
.score-display.amber { color: var(--amber); }
.score-display.red { color: var(--red); }

.confidence-dots { display: inline-flex; gap: 2px; align-items: center; }
.confidence-dots .d { width: 5px; height: 5px; border-radius: 50%; background: var(--bg-4); }
.confidence-dots .d.on.high { background: var(--green-solid); }
.confidence-dots .d.on.mid { background: var(--amber-solid); }
.confidence-dots .d.on.low { background: var(--red-solid); }

/* layout helpers */
.row { display: flex; align-items: center; gap: 8px; }
.col { display: flex; flex-direction: column; gap: 8px; }
.flex1 { flex: 1; min-width: 0; }
.spacer { flex: 1; }

/* PDF mock — a paper page on a neutral mat, so the scan is the brightest thing. */
.pdf-viewer {
  background: #dcdce1;
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  padding: 12px;
  position: relative;
  min-height: 400px;
  overflow: hidden;
}
.pdf-page {
  background: #f7f3ea;
  color: #1a1a1a;
  border-radius: 4px;
  padding: 24px 28px;
  font-family: 'Kalam', 'Caveat', cursive;
  font-size: 16px;
  line-height: 1.7;
  min-height: 440px;
  box-shadow: 0 10px 30px rgba(20,20,23,0.18);
  position: relative;
}
.pdf-page .header-line {
  display: flex; justify-content: space-between;
  font-family: 'DM Sans', sans-serif;
  font-size: 11px; color: #888;
  border-bottom: 1px solid #d8d2c2;
  padding-bottom: 6px;
  margin-bottom: 12px;
}
.bbox {
  position: absolute;
  border: 2px solid var(--blue-solid);
  border-radius: 4px;
  background: rgba(59,123,246,0.10);
  pointer-events: none;
}
.bbox.confirmed { border-color: var(--green-solid); background: rgba(47,190,95,0.10); }
.bbox.active { border-color: var(--pink-solid); box-shadow: 0 0 0 2px rgba(245,49,141,0.25); }
.bbox-label {
  position: absolute;
  top: -10px; left: -2px;
  background: var(--blue-solid); color: #fff;
  font-size: 10px; font-weight: 600;
  padding: 1px 6px; border-radius: 4px;
  font-family: 'IBM Plex Mono', monospace;
}
.bbox.confirmed .bbox-label { background: var(--green-solid); color: #fff; }
.bbox.active .bbox-label { background: var(--pink-solid); color: #fff; }
