/* idetik Faculty Dashboard — design system.

   DERIVED, DATED NOTE (2026-08-18). Palette, type scale and density come from
   ../idetik/docs/ui-guidelines.md ("Developer Dashboard — dense desktop
   console") read on that date, cross-checked against the literal values in
   ../idetik/Developer Dashboard.dc.html. This is the same visual family, not a
   copy of that document's layout: the Developer Dashboard is a platform
   operator's console with a screen registry; this is a faculty's workspace with
   three destinations.

   Two deliberate departures, both because the audience differs:
   · Rows breathe more (10–12px vertical, not 5–6px). A faculty opens this a few
     times a week to answer a question, not for hours of triage; the console's
     density is a professional tool's density and would read as pressure here.
   · Mobile is a first-class layout, not a reflow. The console is desktop-only
     by design; a faculty checks their packs from a phone.
*/

:root {
  /* Surfaces — the console's fixed warm-dark palette. */
  --page:      #191613;
  --panel:     #211D18;
  --rail-bg:   #14110F;      /* the console's sidebar is DARKER than the page */
  --panel-2:   #262119;
  --popover:   #2A251E;
  --selected:  #322B22;

  /* Hairlines. The console runs 0.05–0.14 alpha; these are its two ends. */
  --line:      rgba(237, 230, 217, 0.07);   /* panel + header borders */
  --line-row:  rgba(237, 230, 217, 0.045);  /* table row separators */
  --line-soft: rgba(237, 230, 217, 0.05);   /* list separators */
  --line-2:    rgba(237, 230, 217, 0.12);

  /* Type */
  --text:      #EDE6D9;
  --text-hi:   #E8DCC8;
  --text-2:    #8F8474;
  --text-val:  #D8CFBE;      /* value text in key/value rows */
  --text-mid:  #B5AA97;      /* secondary control labels */
  --text-3:    #6E6559;

  /* Accent + status */
  --accent:      #C9A87A;
  --accent-hi:   #D9BC90;
  --accent-soft: rgba(201, 168, 122, 0.10);
  --green:     #7FB584;
  --green-2:   #9FCB9F;
  --blue:      #8FA8C9;
  --tan:       #C9B28F;
  --danger:    #D64545;

  --radius:    12px;
  --radius-sm: 9px;

  /* One rhythm, referenced everywhere. */
  /* ── SPACING CARRIES THE MINIMALISM, NOT THE TYPE ─────────────────────────
     Larger text on the old rhythm reads as denser, not calmer — the glyphs grow
     and the air around them does not. These moved with the type on 2026-09-10
     so the page ends up quieter rather than fuller. */
  --gap:       14px;
  --pad:       22px;

  /* Widened with the nav type (15px → 16px) so "Student Doubts" still sits on
     one line with its icon and its padding. Widening the rail rather than
     shrinking the label is the point: the brief rules out paying for larger
     type by taking space away somewhere else. */
  --sidebar:   232px;
  --topbar:    56px;
  --tabbar:    62px;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* `hidden` MUST hide. The browser's own `[hidden] { display: none }` lives in
   the user-agent stylesheet, so any author rule that sets `display` — `.btn`'s
   `inline-flex`, for one — silently beats it, and an element the code believes
   is hidden stays on screen. Found on Account ▸ Profile, where the name row's
   Save button was permanently visible instead of appearing when there was
   something to save. One rule, rather than a defensive class on every element
   that is ever toggled. */
[hidden] { display: none !important; }

html {
  background: var(--page);
  /* iOS: keep the fixed bottom tab bar above the home indicator. */
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--page);
  color: var(--text);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  font-size: 16.5px;
  /* A touch looser to match the larger body size — the same ratio at a bigger
     size reads tighter than it did. */
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--text-hi); }
::selection { background: rgba(201, 168, 122, 0.3); }

/* Thin themed scrollbars — the console's, verbatim. */
* { scrollbar-width: thin; scrollbar-color: rgba(237, 230, 217, 0.22) #1C1814; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: #1C1814; border-radius: 999px; }
*::-webkit-scrollbar-thumb { background: rgba(237, 230, 217, 0.18); border-radius: 999px; border: 2px solid #1C1814; }
*::-webkit-scrollbar-thumb:hover { background: rgba(201, 168, 122, 0.45); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

@keyframes fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes shimmer { 0% { background-position: -320px 0; } 100% { background-position: 320px 0; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   SHELL
   Laptop: a fixed left rail, content scrolls independently.
   Mobile: a top bar for identity and context, a bottom tab bar for the three
   destinations — thumb-reachable, which a left rail on a phone is not.
   ══════════════════════════════════════════════════════════════════════════ */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  min-height: 100dvh;
}

/* ══ SWITCHING WORKSPACES ═══════════════════════════════════════════════════
   The outgoing screen is taken out of flow and faded out while the incoming
   one fades in over it, so the window is never empty for a frame. See
   `swapShell()` in app.js for the ordering and for why nothing waits on this.

   Only `opacity` and `transform` are animated — both composited, neither
   causing layout — so the transition cannot make navigation slower than it
   already is. 150ms in, 130ms out: enough to read as a movement, short enough
   that it is over before it is noticed. */
#app { position: relative; }
.shell { position: relative; z-index: 1; }

/* THE ARRIVING SCREEN IS NEVER HIDDEN. IT IS THE OUTGOING ONE THAT DISSOLVES.
   The obvious crossfade — fade the new screen up from nothing — cannot be made
   safe: a browser PAUSES animations in a hidden tab, so a screen whose first
   keyframe is `opacity: 0` renders as an empty window for as long as the tab
   stays in the background. Two earlier versions of this had exactly that bug,
   caught by measuring computed opacity rather than by looking at it.

   So the old screen stays ON TOP and fades out, revealing the new one already
   painted underneath. The same dissolve to look at, and the failure mode
   inverts: if the animation never runs the swap is merely abrupt — which is
   where this started — instead of blank.

   The arriving screen animates its TRANSFORM only. A paused transform leaves
   it four pixels low; a paused opacity would leave it invisible. */
.shell.is-entering { animation: shell-in 150ms ease; }
@keyframes shell-in { from { transform: translateY(4px); } }

/* Out of flow, ABOVE the new screen, and click-through — so the screen the
   faculty can see is never a screen their click could land on. app.js has
   already taken it out of the accessibility tree and the tab order. */
.shell.is-leaving {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  animation: shell-out 130ms ease forwards;
}
@keyframes shell-out { to { opacity: 0; } }

.rail {
  border-right: 1px solid var(--line);
  background: var(--rail-bg);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100dvh;
}

.rail-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 20px var(--pad) 18px;
  border-bottom: 1px solid var(--line);
}
/* The actual brand mark (see lib/ui.js `mark()`) filling a 26px, 8px-radius
   tile — the same square rendering the browser tab icon uses, so the sidebar
   shows the same object the tab does. */
.rail-mark {
  width: 26px; height: 26px;
  border-radius: 8px;
  overflow: hidden;
  display: grid; place-items: center;
  flex: none;
}
.rail-mark img { width: 100%; height: 100%; display: block; }
.rail-wordmark { font-size: 18px; font-weight: 800; letter-spacing: -0.02em; }
.rail-role {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--text-3); margin-top: 1px;
}

.rail-nav { padding: 12px 10px; display: flex; flex-direction: column; gap: 2px; }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  color: var(--text-2);
  font-size: 16px; font-weight: 600;
  min-height: 38px;
  transition: background 0.14s ease, color 0.14s ease;
  cursor: pointer;
}
.nav-item:hover { background: var(--panel-2); color: var(--text); }
.nav-item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--text-hi);
  box-shadow: inset 2px 0 0 var(--accent);
}
.nav-item svg { flex: none; opacity: 0.9; }
.nav-count {
  margin-left: auto;
  font-size: 13.5px; font-weight: 700;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.rail-foot {
  margin-top: auto;
  padding: 14px var(--pad);
  border-top: 1px solid var(--line);
  font-size: 14px;
  color: var(--text-3);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

.main { min-width: 0; display: flex; flex-direction: column; }

/* The two mobile-only chrome elements are always in the DOM (one shell, one
   markup path) and are turned ON by the mobile layout mode. They must default
   to hidden: without this the bottom tab bar renders as an ordinary block in
   the desktop grid — a second, duplicate navigation below the rail. */
.topbar { display: none; }
.tabbar { display: none; }

.view {
  padding: 36px 40px 64px;
  max-width: 1180px;
  width: 100%;
  /* NO ENTRY ANIMATION HERE ANY MORE.
     A view repaints whenever its data lands — Publishing paints a loading state
     and then replaces it with rows — and each repaint built a NEW `.view`
     element, so `fade-up ... both` restarted from opacity 0 every time. That is
     the flash that was visible part-way through mounting a screen, separate
     from the one between screens.

     The entry animation now belongs to the workspace swap (`.shell` below),
     which happens once per navigation, so arriving somewhere animates and
     filling it in does not. */
}

/* ══ Page header ══ */
.page-head { margin-bottom: 30px; }
.eyebrow {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--text-3); margin-bottom: 7px;
}
.page-title {
  font-size: 27.5px; font-weight: 800; letter-spacing: -0.02em;
  margin: 0; line-height: 1.2;
}
.page-sub {
  margin-top: 7px;
  font-size: 16px; color: var(--text-2); line-height: 1.6;
  max-width: 66ch;
}

/* ══ Panels ══ */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.panel-head {
  display: flex; align-items: center; gap: 12px;
  padding: 13px var(--pad);
  border-bottom: 1px solid var(--line);
  min-height: 48px;
}
.panel-title { font-size: 15.5px; font-weight: 700; letter-spacing: -0.005em; }
.panel-note { font-size: 14.5px; color: var(--text-3); margin-left: auto; }
.panel-body { padding: var(--pad); }

.stack { display: flex; flex-direction: column; gap: var(--gap); }
.grid { display: grid; gap: var(--gap); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

/* ══ Stat tiles — real numbers only; "—" where a read failed ══ */
.stat {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 15px 17px 16px;
}
.stat-label {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3);
}
.stat-value {
  font-size: 31.5px; font-weight: 800; letter-spacing: -0.025em;
  margin-top: 8px; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat-value.is-unknown { color: var(--text-3); font-size: 26px; }
.stat-foot { margin-top: 8px; font-size: 14.5px; color: var(--text-2); line-height: 1.5; }

/* ══ Badges ══ */
/* Status pill — the console's exact treatment: 11px/700 uppercase, 0.06em
   tracking, the status colour at full strength on a 12%-alpha bed, fully
   rounded. Deliberately NO leading dot: the console's pills are text only, and
   a dot is decoration this design language does not use. */
.badge {
  display: inline-flex; align-items: center;
  font-size: 13.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(237, 230, 217, 0.07);
  color: var(--text-2);
  white-space: nowrap;
}
.badge.is-published { color: var(--green);  background: rgba(127, 181, 132, 0.12); }
.badge.is-ready     { color: var(--green);  background: rgba(127, 181, 132, 0.12); }
.badge.is-draft     { color: var(--tan);    background: rgba(201, 178, 143, 0.12); }
.badge.is-info      { color: var(--blue);   background: rgba(143, 168, 201, 0.12); }
.badge.is-retired   { color: var(--text-3); background: rgba(237, 230, 217, 0.05); }

/* ══ Buttons ══ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: inherit; font-size: 15.5px; font-weight: 700;
  padding: 0 16px;
  min-height: 38px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-2);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
  transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease;
  white-space: nowrap;
}
.btn:hover { background: var(--popover); border-color: rgba(237, 230, 217, 0.2); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary {
  background: var(--accent); border-color: var(--accent); color: #191613;
}
.btn-primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: #191613; }
.btn-ghost { background: transparent; }
.btn-block { width: 100%; }

/* ══ Lists — the desktop "table" and the mobile card are ONE component.
   Rows are a grid whose template changes at the breakpoint, so there is no
   second markup path to keep in sync and no horizontally-scrolling table on a
   phone. ══ */
.rows { display: flex; flex-direction: column; }

.rows-head {
  display: grid;
  gap: 14px;
  padding: 9px var(--pad);
  border-bottom: 1px solid var(--line);
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-3);
  background: var(--panel);
}

.row {
  display: grid;
  gap: 14px;
  align-items: center;
  padding: 12px var(--pad);
  border-bottom: 1px solid var(--line);
  font-size: 16px;
  text-align: left;
  width: 100%;
  background: transparent;
  border-left: 0; border-right: 0; border-top: 0;
  color: inherit;
  font-family: inherit;
  transition: background 0.12s ease;
}
.rows > .row:last-child { border-bottom: 0; }
button.row { cursor: pointer; }
button.row:hover { background: var(--panel-2); }
button.row:active { background: var(--selected); }

.row-title { font-weight: 650; letter-spacing: -0.005em; }
.row-sub { font-size: 14.5px; color: var(--text-3); margin-top: 2px; }
.num { font-variant-numeric: tabular-nums; }
.dim { color: var(--text-2); }
.dimmer { color: var(--text-3); }
.mono {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 14.5px; letter-spacing: 0.02em;
}
.row-chevron { color: var(--text-3); display: flex; justify-content: flex-end; }

/* Mobile carries the columns it dropped back as an inline meta line, so a
   phone loses the TABLE, not the information. Hidden on laptop, where the
   columns say the same thing better. */
.row-meta { display: none; }

/* ══ Tabs (Course Pack workspace) ══ */
.tabs {
  display: flex; gap: 2px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 22px;
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  appearance: none; border: 0; background: transparent;
  font-family: inherit; font-size: 15.5px; font-weight: 700;
  color: var(--text-3);
  padding: 11px 15px;
  min-height: 44px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
  transition: color 0.14s ease, border-color 0.14s ease;
}
.tab:hover { color: var(--text-2); }
.tab[aria-selected="true"] { color: var(--text-hi); border-bottom-color: var(--accent); }

/* ══ States: loading / empty / error. Every screen uses these three. ══ */
.skeleton {
  background: linear-gradient(90deg, var(--panel-2) 0%, #2E281F 40%, var(--panel-2) 80%);
  background-size: 320px 100%;
  animation: shimmer 1.25s linear infinite;
  border-radius: 6px;
  height: 13px;
}
.skeleton.tall { height: 27px; border-radius: 8px; }

.spinner {
  width: 15px; height: 15px; flex: none;
  border: 2px solid rgba(237, 230, 217, 0.18);
  border-top-color: var(--accent);
  border-radius: 999px;
  animation: spin 0.7s linear infinite;
}

.state {
  padding: 34px var(--pad);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 9px;
}
.state-icon {
  width: 40px; height: 40px; border-radius: 11px;
  background: rgba(237, 230, 217, 0.055);
  display: grid; place-items: center;
  color: var(--text-3);
  margin-bottom: 3px;
}
.state-title { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }
.state-body { font-size: 15.5px; color: var(--text-2); line-height: 1.65; max-width: 46ch; }
.state-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.state.is-error .state-icon { color: var(--danger); background: rgba(214, 69, 69, 0.09); }
.state-detail {
  margin-top: 4px;
  font-size: 14px; color: var(--text-3);
  font-family: ui-monospace, Menlo, Consolas, monospace;
  overflow-wrap: anywhere;
}

/* ══ Banner — the unapplied-migration warning, and nothing else ══ */
.banner {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 12px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(214, 69, 69, 0.32);
  background: rgba(214, 69, 69, 0.08);
  font-size: 15px; line-height: 1.6;
  color: var(--text);
  margin-bottom: 20px;
}
.banner svg { flex: none; color: var(--danger); margin-top: 1px; }
.banner strong { font-weight: 750; }
.banner code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 14px;
  background: rgba(0, 0, 0, 0.28);
  padding: 1px 5px; border-radius: 4px;
}

/* ══════════════════════════════════════════════════════════════════════════
   SIGN-IN — a single centred card, no shell.
   ══════════════════════════════════════════════════════════════════════════ */
.auth {
  min-height: 100dvh;
  display: grid; place-items: center;
  padding: 28px 20px calc(28px + env(safe-area-inset-bottom));
}
.auth-card {
  width: 100%; max-width: 384px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 28px 26px 22px;
  animation: fade-up 0.28s ease both;
}
.auth-mark {
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--accent-soft);
  border: 1px solid rgba(201, 168, 122, 0.22);
  display: grid; place-items: center; color: var(--accent);
  margin-bottom: 14px;
}
.auth-title { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
/* The CODE step's line only — the first step carries no subtitle. */
.auth-sub { margin-top: 6px; font-size: 15.5px; color: var(--text-2); line-height: 1.65; }
.auth-form { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }

/* ── THE ONE PRIMARY ACTION ────────────────────────────────────────────────
   Sizing only, and only on this screen: `.btn` keeps its own 38px everywhere
   else, and the coarse-pointer rule further down already lifts every button to
   44 on touch — this brings the same presence to a mouse, because on this
   screen there is one thing to press. */
.auth-google { min-height: 44px; font-size: 16px; }
/* Google's mark goes on white with clear space around it, never recoloured and
   never dropped straight onto an arbitrary ground — and its blue on our accent
   would be unreadable besides. The tile is that white field. */
.auth-g {
  display: grid; place-items: center;
  width: 22px; height: 22px; flex: none;
  background: #FFFFFF; border-radius: 4px;
}
/* The emailed code, closed. Centred and quiet: an alternative, not a second
   decision — and it is not called a fallback anywhere, because for anyone
   locked out of their Google account it is the way in. */
.auth-alt { margin-top: 14px; display: flex; justify-content: center; }

.field { display: flex; flex-direction: column; gap: 6px; }
.field-label {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3);
}
.input {
  font-family: inherit;
  font-size: 18px;            /* ≥16px avoids iOS zoom-on-focus, which this
                                 comfortably clears since the 2026-09-09
                                 typography pass. Coarse pointers get their own
                                 size below. */
  color: var(--text);
  background: var(--page);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  padding: 0 13px;
  min-height: 44px;
  width: 100%;
  transition: border-color 0.14s ease;
}
.input::placeholder { color: var(--text-3); }
.input:focus { outline: none; border-color: var(--accent); }
.input.code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 25px; letter-spacing: 0.32em; text-align: center;
  padding: 0;
}
.auth-error {
  font-size: 15px; color: #E88C8C; line-height: 1.6;
  background: rgba(214, 69, 69, 0.09);
  border: 1px solid rgba(214, 69, 69, 0.28);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
}
/* The workspace sentence. Its WORDS are unchanged — signing in granting nothing
   on its own is the honest disclosure this screen exists to make, and it is not
   something a visual pass gets to shorten. What it loses is the rule above it:
   with one button and one link on the card there is nothing left to divide it
   from, and the space alone now does that work. */
.auth-foot {
  margin-top: 22px;
  font-size: 13.5px; color: var(--text-3); line-height: 1.6;
}
.link-btn {
  appearance: none; border: 0; background: none; padding: 0;
  font-family: inherit; font-size: 15px; font-weight: 650;
  color: var(--accent); cursor: pointer;
  min-height: 34px;
}
.link-btn:hover { color: var(--text-hi); }
.link-btn:disabled { color: var(--text-3); cursor: not-allowed; }

/* ══ Definition list — Account ══ */
.dl { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 0; }
.dl > dt, .dl > dd {
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  margin: 0;
  font-size: 16px;
}
.dl > dt { color: var(--text-3); font-size: 14.5px; font-weight: 650; }
.dl > dd { overflow-wrap: anywhere; }
.dl > dt:last-of-type, .dl > dd:last-of-type { border-bottom: 0; }

/* ══ Setting rows — Account → Basic Settings ══
   A settings screen is a list of settings, so it is built from rows on
   hairlines rather than from panels: a card around three facts turns a quiet
   screen into a dashboard. Everything else is already in the system — .section
   for the group heading, .input for the one editable field, .btn.btn-sm for the
   actions, .ws-tile for the avatar. */
.set { display: flex; flex-direction: column; }
.set-group { margin-bottom: 34px; }
.set-group:last-child { margin-bottom: 0; }
.set-row {
  display: flex; align-items: center; gap: 16px;
  min-height: 54px; padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
}
.set-row:last-child { border-bottom: 0; }
.set-label { flex: 0 0 128px; font-size: 15px; font-weight: 650; color: var(--text-3); }
.set-value {
  flex: 1 1 auto; min-width: 0;
  font-size: 16px; color: var(--text-val);
  overflow-wrap: anywhere;
}
.set-action { flex: none; margin-left: auto; }
.set-error { margin-top: 6px; font-size: 14.5px; color: var(--danger); line-height: 1.5; }
/* One short line under a value. The ONLY explanatory text a settings row gets:
   if a setting needs a paragraph, the setting needs a rethink. */
.set-hint { margin-top: 5px; font-size: 14.5px; color: var(--text-3); line-height: 1.5; }
/* A form's single commit, at the foot of its group. */
.set-actions { display: flex; justify-content: flex-end; margin-top: 16px; }

/* A setting whose NAME carries the explanation and whose control is a switch.
   The 128px label column would break "Require owner approval" over three lines,
   so this variant gives the text the width and keeps the control on the edge. */
.set-row.is-wide { align-items: flex-start; gap: 24px; }
.set-main { flex: 1 1 auto; min-width: 0; }
.set-title { font-size: 16px; font-weight: 650; color: var(--text); letter-spacing: -0.005em; }
.set-row.is-wide .set-hint { max-width: 48ch; }
.set-row.is-wide .set-action { padding-top: 1px; }

/* ══ Switch ══
   A restyled native checkbox — focusable, announced, Space-operable. The track
   is the label's sibling so `:checked +` does all the work with no script. */
.sw { display: inline-flex; align-items: center; cursor: pointer; }
.sw-input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.sw-track {
  display: block; position: relative;
  width: 40px; height: 23px;
  border-radius: 999px;
  background: var(--panel-2);
  border: 1px solid var(--line-2);
  transition: background 0.16s ease, border-color 0.16s ease;
}
.sw-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 17px; height: 17px; border-radius: 50%;
  background: var(--text-3);
  transition: transform 0.18s cubic-bezier(0.32, 0.72, 0, 1), background 0.16s ease;
}
.sw:hover .sw-track { border-color: rgba(237, 230, 217, 0.2); }
.sw-input:checked + .sw-track { background: var(--accent); border-color: var(--accent); }
.sw-input:checked + .sw-track .sw-thumb { transform: translateX(17px); background: #191613; }
.sw-input:focus-visible + .sw-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.sw-input:disabled + .sw-track { opacity: 0.45; }

/* ══════════════════════════════════════════════════════════════════════════
   ACCOUNT — the area's own frame
   ══════════════════════════════════════════════════════════════════════════
   One title, one tab strip, one column. The area had a labelled navigation
   column down the left on desktop; it does not any more (2026-08-21). The
   dashboard already carries exactly one navigation — the rail — and a second
   one beside it reads as a second product rather than as a section index. The
   four tabs are the dashboard's own `.tabs`, so nothing new was invented for
   them, and they say which page while the title says which area. */
.acct-body { min-width: 0; }
/* The title and the strip belong together — 24px of air between "Account" and
   the tabs that qualify it reads as two separate things. */
.view.is-account > .page-head { margin-bottom: 15px; }
.acct-tabs { margin-bottom: 26px; }

/* The prototype line. A sentence, never a banner: it says the same thing on
   more than one page, and a coloured box on each would drown the design it is
   describing. */
.acct-note {
  margin: -6px 0 22px;
  max-width: 56ch;
  font-size: 15px; line-height: 1.55;
  color: var(--text-3);
}

/* ══ Team ══
   A list of people, not an admin table. Three columns: who, what they may do,
   and the one control that changes it. */
/* The list's own header: the count on the left, Invite member on the right.
   The page title above belongs to the AREA, so this is where a control that
   acts on the list has to live. */
.tm-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  margin-bottom: 10px;
}
.tm-count { font-size: 15px; color: var(--text-3); }
.tm-list { display: flex; flex-direction: column; }
.tm-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 220px) auto;
  align-items: center;
  gap: 20px;
  padding: 15px 0;
  border-bottom: 1px solid var(--line-soft);
}
.tm-row:last-child { border-bottom: 0; }
.tm-who { min-width: 0; }
.tm-name {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  font-size: 16.5px; font-weight: 650; letter-spacing: -0.01em;
  color: var(--text);
}
.tm-you {
  font-size: 11.5px; font-weight: 750;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-3);
}
.tm-mail { margin-top: 3px; font-size: 15px; color: var(--text-3); overflow-wrap: anywhere; }
.tm-perms { font-size: 15px; color: var(--text-2); min-width: 0; overflow-wrap: anywhere; }
.tm-act { justify-self: end; }
.tm-section { margin-top: 30px; }

.badge.is-owner { color: var(--accent); background: var(--accent-soft); }

/* ══ Permissions, inside a dialog ══
   One row per area — its name on the left, its options on the right — which is
   the same geometry as a settings row and a quarter of the height of nine
   headed checkbox stacks. */
.dlg-stack { display: flex; flex-direction: column; gap: 14px; }
.dlg-sub {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  margin-top: -4px;
  font-size: 15px; color: var(--text-3);
  overflow-wrap: anywhere;
}
.dlg-foot-note { font-size: 14.5px; line-height: 1.55; color: var(--text-3); }

.perm-title {
  font-size: 12px; font-weight: 750;
  letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 2px;
}
.perms { display: flex; flex-direction: column; }
.perm-row {
  display: flex; align-items: center; gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
}
.perm-row:last-child { border-bottom: 0; }
.perm-label { flex: 0 0 104px; font-size: 15px; font-weight: 650; color: var(--text-2); }
.perm-opts { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; gap: 7px 16px; }
.perm-opt {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 15px; color: var(--text-val);
  cursor: pointer;
}
.perm-opt input:disabled { cursor: default; }
.perm-opt input:disabled + span { color: var(--text-3); }
.perm-owner { font-size: 15px; line-height: 1.6; color: var(--text-2); }
.perm-owner strong { color: var(--text-hi); font-weight: 700; }

.link-btn.is-danger { color: var(--danger); align-self: flex-start; }
.link-btn.is-danger:hover { color: #E86A6A; }

/* A dialog that carries a permission matrix can outgrow a short window, and a
   modal that scrolls the page behind it is worse than one that scrolls itself.
   Declared here rather than on `.dialog` above so the two properties sit with
   the content that made them necessary. */
.dialog { max-height: calc(100dvh - 40px); overflow-y: auto; }

/* ══════════════════════════════════════════════════════════════════════════
   LAYOUT MODES
   ══════════════════════════════════════════════════════════════════════════

   Layout is driven by `data-layout` on <html> — set BEFORE first paint by
   lib/layout-boot.js — and never by a bare media query. That is what lets
   /faculty/desktop/ and /faculty/mobile/ compose two different experiences
   from one codebase: the route pins the mode, and /faculty/ resolves it from
   the viewport and keeps resolving it as the window changes.

   Every rule below therefore keys off [data-layout="…"]. There is exactly one
   copy of the mobile composition; nothing is duplicated per route.
   ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
   DESKTOP — horizontal space, persistent rail, real tables, multi-column
   ══════════════════════════════════════════════════════════════════════════ */

/* Narrow laptop: the rail gives up width before the content does. */
@media (max-width: 1080px) {
  /* Both moved with the type pass (2026-09-10). The rail still gives up width
     before the content does — it just starts from a wider rail and a bigger nav
     label, so the same intent needs a slightly larger number. */
  [data-layout="desktop"] { --sidebar: 214px; }
  [data-layout="desktop"] .view { padding: 30px 28px 58px; }
}

/* Wide desktop earns a second column rather than a longer measure — a 1600px
   window should show more AT ONCE, not the same column with more air. */
[data-layout="desktop"] .view { max-width: 1180px; }
/* A settings screen keeps a narrow measure at any window width — four labels
   and four values stretched across 1180px read as a report, not a form. Account
   takes a little more than the 560px a plain settings page does: its Team tab
   is a three-column list, and all four tab labels have to fit across the top
   without scrolling. Still nowhere near the 1180px the data screens take,
   because nothing on it is a table. */
[data-layout="desktop"] .view.is-account { max-width: 660px; }
[data-layout="desktop"] .split {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(300px, 1fr);
  gap: var(--gap);
  align-items: start;
}
/* The secondary column tracks the reader down a long primary column. */
[data-layout="desktop"] .split > .split-side {
  position: sticky;
  top: 24px;
  display: flex; flex-direction: column; gap: var(--gap);
}

@media (max-width: 1240px) {
  [data-layout="desktop"] .split { grid-template-columns: minmax(0, 1fr); }
  [data-layout="desktop"] .split > .split-side { position: static; }
}

@media (min-width: 1500px) {
  [data-layout="desktop"] .view { max-width: 1420px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   MOBILE — vertical, thumb-first, stacked. Not the desktop layout scaled down:
   different navigation (bottom bar), different density, and tables replaced by
   cards rather than squeezed into columns.
   ══════════════════════════════════════════════════════════════════════════ */

[data-layout="mobile"] .shell {
  grid-template-columns: minmax(0, 1fr);
  /* Room for the fixed bars at both ends. */
  padding-top: var(--topbar);
  padding-bottom: calc(var(--tabbar) + env(safe-area-inset-bottom));
}
[data-layout="mobile"] .rail { display: none; }
[data-layout="mobile"] .split { display: block; }
[data-layout="mobile"] .split > .split-side {
  display: flex; flex-direction: column; gap: var(--gap);
  margin-top: var(--gap);
  position: static;
}

[data-layout="mobile"] .topbar {
  display: flex; align-items: center; gap: 10px;
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  height: var(--topbar);
  padding: 0 16px;
  background: rgba(25, 22, 19, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
[data-layout="mobile"] .topbar-mark {
  width: 26px; height: 26px; border-radius: 7px;
  overflow: hidden;
  display: grid; place-items: center;
  flex: none;
}
[data-layout="mobile"] .topbar-mark img { width: 100%; height: 100%; display: block; }
[data-layout="mobile"] .topbar-title {
  font-size: 16.5px; font-weight: 750; letter-spacing: -0.015em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
[data-layout="mobile"] .topbar-sub {
  font-size: 13px; color: var(--text-3); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A back affordance, because on a phone the rail is not there to go back to. */
[data-layout="mobile"] .topbar-back {
  appearance: none; background: none; border: 0; padding: 0;
  width: 44px; height: 44px; margin-left: -10px;
  display: grid; place-items: center;
  color: var(--text-2); cursor: pointer; flex: none;
}

[data-layout="mobile"] .tabbar {
  display: grid;
  grid-auto-flow: column; grid-auto-columns: 1fr;
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
  background: rgba(25, 22, 19, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
[data-layout="mobile"] .tabbar .nav-item {
  flex-direction: column; gap: 3px;
  /* VERTICAL: NOT centered. A wrapped label must not move its own icon —
     every icon sits at the same fixed distance from the top of the bar,
     whether its label takes one line or two, so the row of icons stays
     level. */
  justify-content: flex-start;
  /* HORIZONTAL: the label span, once it has a two-word name to wrap
     ("Create Decks", "Student Doubts"), renders BOTH lines flush against the
     span's own left edge — text-align defaults to start, and it is the TEXT
     that needs centring, not the span (the span is already centred as a
     block; measured: its box sits exactly on the icon's own centre line,
     133-189px for a tab centred at 161px). Audited 2026-09-10 by reading the
     rendered line boxes directly: "Create" and "Decks" both started at the
     SAME x as each other and as the span's left edge, 43px and 40px wide in
     a 56px box — flush left, not centred, which is what read as "offset".
     `align-items: center` on the flex column centres the SPAN; it was never
     going to reach inside and centre the two lines of text painted within
     it. */
  text-align: center;
  padding: 8px 2px 6px;
  min-height: var(--tabbar);
  /* Kept at the typography-pass size (2026-09-09) rather than shrunk to fit
     six items — the icon-alignment fix above is what makes two-line labels
     safe now, so the label can simply wrap instead of the type getting smaller. */
  font-size: 13px; letter-spacing: 0.005em; line-height: 1.15;
  background: none;
  box-shadow: none;
}
/* The old inline style, unchanged for desktop: fixed to the very top,
   centred. Only the MOBILE override below differs from what preview.js used
   to set inline. */
.preview-tag {
  position: fixed; z-index: 99; top: 0; left: 50%; transform: translateX(-50%);
}
/* Pushed below the topbar instead of sitting on top of it — the topbar is
   ALSO position:fixed;top:0 on a phone, so the two overlapped exactly, and the
   badge covered "idetik Faculty" mid-word. */
[data-layout="mobile"] .preview-tag { top: var(--topbar); border-radius: 0 0 7px 7px; }

[data-layout="mobile"] .tabbar .nav-item[aria-current="page"] {
  background: none;
  color: var(--accent);
  box-shadow: inset 0 2px 0 var(--accent);
}
/* ── SIX WORKSPACES, ZERO WRAPPING ───────────────────────────────────────
   Six full workspace names do not fit unwrapped at 375px however the column
   padding is tuned — "Create Decks" and "Student Doubts" wrapped to two
   lines while their siblings stayed on one, which is the uneven-baseline
   problem the icon-alignment fix above patches but cannot fully hide.

   The label now shows ONLY on the current tab. Every icon still sits at the
   SAME fixed offset from the top either way — an inactive label is invisible
   via `opacity`, never removed from layout, so it goes on occupying exactly
   the line height a visible label would have and no icon has to move when
   its neighbour becomes active. `opacity`, not `visibility`/`display`, for
   the reason those two both take an element out of the accessibility tree:
   this keeps every tab announcing its real name to a screen reader, which is
   the one thing this technique must not cost. */
[data-layout="mobile"] .tabbar .nav-item:not([aria-current="page"]) span {
  opacity: 0;
}
[data-layout="mobile"] .tabbar .nav-item[aria-current="page"] {
  font-weight: 700;
}
[data-layout="mobile"] .tabbar .nav-count { display: none; }

[data-layout="mobile"] .view { padding: 18px 15px 34px; max-width: none; }
[data-layout="mobile"] .page-head { margin-bottom: 18px; }
[data-layout="mobile"] .page-title { font-size: 24px; }
[data-layout="mobile"] .page-sub { font-size: 15.5px; }

/* Rows become stacked cards: the header row is meaningless without columns,
   and a 5-column grid on a 375px screen is not a table, it is a mess. */
[data-layout="mobile"] .rows-head { display: none; }
[data-layout="mobile"] .row {
  grid-template-columns: minmax(0, 1fr) auto !important;
  gap: 4px 12px;
  padding: 14px 16px;
  min-height: 60px;
}
[data-layout="mobile"] .row > .col-hide-mobile { display: none; }
[data-layout="mobile"] .row > .col-span-mobile { grid-column: 1 / -1; }
[data-layout="mobile"] .row-meta {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 7px;
  font-size: 14.5px; color: var(--text-3);
}
[data-layout="mobile"] .row-meta .badge { font-size: 11.5px; padding: 2px 7px; }

[data-layout="mobile"] .dl { grid-template-columns: minmax(0, 1fr); }

/* Settings: the label sits above its value, and an action is a full-width
   thumb target on its own line — the same rule the Student Doubts panel actions
   follow, for the same reason. */
[data-layout="mobile"] .set-row { flex-wrap: wrap; align-items: flex-start; gap: 8px; padding: 14px 0; }
[data-layout="mobile"] .set-label { flex: 1 1 100%; }
[data-layout="mobile"] .set-value:empty { display: none; }
[data-layout="mobile"] .set-action { flex: 1 1 100%; margin-left: 0; }
[data-layout="mobile"] .set-action .btn { width: 100%; }
/* …except a switch, which must stay BESIDE the label it toggles. Stacking it
   full-width under two lines of explanation would leave a 40px control adrift
   in a 375px row with nothing to say what it belongs to. */
[data-layout="mobile"] .set-row.is-wide { flex-wrap: nowrap; gap: 16px; }
[data-layout="mobile"] .set-row.is-wide .set-action { flex: none; width: auto; margin-left: auto; }
[data-layout="mobile"] .dl > dt { padding-bottom: 0; border-bottom: 0; }
[data-layout="mobile"] .dl > dd { padding-top: 3px; }

/* ── Account, on a phone ────────────────────────────────────────────────────
   Same four tabs as the laptop, one label shortened. `.tabs` already scrolls
   horizontally when it has to (`overflow-x: auto`, scrollbar hidden), so a
   narrower phone or a longer translation degrades by scrolling rather than by
   wrapping the strip into two rows. */
[data-layout="mobile"] .acct-tabs { margin-bottom: 20px; }
[data-layout="mobile"] .acct-note { margin: -4px 0 20px; }

/* A team row drops to two lines: who on the left with Manage on the right, and
   what they may do across the full width beneath. The permission summary is
   the longest string on the row and the one that must not be truncated. */
[data-layout="mobile"] .tm-row {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 10px 14px;
  padding: 16px 0;
}
/* Placed explicitly, all three. Auto-placement puts the full-width summary in
   its own row and then pushes Manage onto a THIRD row in column one — a 160px
   row with an empty second column, which is what this did before the rows were
   measured. */
[data-layout="mobile"] .tm-who { grid-column: 1; grid-row: 1; }
[data-layout="mobile"] .tm-act { grid-column: 2; grid-row: 1; }
[data-layout="mobile"] .tm-perms { grid-column: 1 / -1; grid-row: 2; }

/* Permission areas stack: the label above its options, the same way every
   other settings row on a phone does. */
[data-layout="mobile"] .perm-row { flex-wrap: wrap; align-items: flex-start; gap: 8px; }
[data-layout="mobile"] .perm-label { flex: 1 1 100%; }
/* The sheet scrolls itself; the page behind it must not move. */
[data-layout="mobile"] .dialog { max-height: 86dvh; overflow-y: auto; }

[data-layout="mobile"] .stat { padding: 13px 15px 14px; }
[data-layout="mobile"] .stat-value { font-size: 28.5px; }
[data-layout="mobile"] .grid-3 { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }

/* The workspace tabs stick under the top bar: on a phone the tab strip scrolls
   out of reach almost immediately, and losing your place in a three-tab
   workspace is the whole navigation. */
[data-layout="mobile"] .tabs {
  position: sticky;
  top: calc(var(--topbar) - 18px);
  z-index: 10;
  margin: 0 -15px 18px;
  padding: 0 15px;
  background: var(--page);
}

/* Touch is first-class in mobile mode regardless of the pointing device, so
   the composition can be inspected honestly on a laptop at /faculty/mobile/. */
[data-layout="mobile"] .nav-item,
[data-layout="mobile"] .btn,
[data-layout="mobile"] .tab,
[data-layout="mobile"] .link-btn { min-height: 44px; }
/* Inline panel-head links ("See all") are a 14px-tall tap target at their
   natural size — the one thing in the composition a thumb cannot reliably
   hit. Padded out to 44px without changing where the text sits. */
[data-layout="mobile"] .panel-note a,
[data-layout="mobile"] .eyebrow a {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 4px; margin: -12px -4px;
}
[data-layout="mobile"] button.row { min-height: 60px; }
[data-layout="mobile"] .input { font-size: 19px; }

@media (max-width: 380px) {
  [data-layout="mobile"] .view { padding: 16px 12px 30px; }
  [data-layout="mobile"] .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The full-bleed tab strip cancels the view's side padding with a negative
     margin, so it has to be re-cancelled when that padding changes. Left at
     -15px against a 12px padding it hangs 3px off each edge and gives the whole
     document a horizontal scrollbar — measured at 375px: `scrollWidth` 378 vs
     `clientWidth` 375. Found on Account, which is the tab strip's only mobile
     user, but the rule is the shared one. */
  [data-layout="mobile"] .tabs { margin-left: -12px; margin-right: -12px; padding-left: 12px; padding-right: 12px; }
}

/* Real touch hardware: drop hover styling in either mode (a sticky :hover
   after a tap reads as a stuck selection). */
@media (pointer: coarse) {
  .nav-item, .btn, .tab, .link-btn { min-height: 44px; }
  button.row { min-height: 60px; }
  .input { font-size: 19px; }
  .btn:hover, button.row:hover, .nav-item:hover { background: inherit; }
  button.row:active { background: var(--selected); }
  .btn:active { filter: brightness(1.15); }
}

/* ══════════════════════════════════════════════════════════════════════════
   MOBILE COMPOSITION ON A LARGE SCREEN — /faculty/mobile/ on a laptop.
   The phone layout stretched across 1280px is not the phone experience, so on
   a wide viewport the mobile composition is framed at phone width and centred.
   This is an INSPECTION affordance for the two dedicated dev routes; on a real
   phone the viewport is narrow, none of it applies, and the layout is
   full-bleed exactly as before.
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 900px) {
  [data-layout="mobile"] body {
    background:
      radial-gradient(1200px 600px at 50% -10%, rgba(201, 168, 122, 0.05), transparent 70%),
      var(--page);
  }
  [data-layout="mobile"] .shell,
  [data-layout="mobile"] .auth {
    width: 100%;
    max-width: 420px;
    margin: 0 auto;
    min-height: 100dvh;
    border-left: 1px solid var(--line);
    border-right: 1px solid var(--line);
    background: var(--page);
  }
  /* The fixed bars are viewport-anchored, so they have to be re-anchored to
     the frame rather than the window. */
  [data-layout="mobile"] .topbar,
  [data-layout="mobile"] .tabbar {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 100%;
    max-width: 420px;
  }
  [data-layout="mobile"] .topbar { border-left: 1px solid var(--line); border-right: 1px solid var(--line); }
  [data-layout="mobile"] .tabbar { border-left: 1px solid var(--line); border-right: 1px solid var(--line); }

  /* The sheet belongs to the phone frame too. Left unconstrained it spans the
     whole laptop window, which is not what the composition being inspected
     actually does on a phone. */
  [data-layout="mobile"] .scrim {
    /* calc, not a translate: the sheet itself animates with a transform, and
       two transform owners on one axis is how the last fix broke. */
    left: calc(50% - 210px); right: auto;
    width: 420px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   HOME
   Every measurement below is taken from ../idetik/Developer Dashboard.dc.html
   (read 2026-08-18), not approximated: panel 12px radius on a 0.07 hairline,
   12px grid gap, 9px/16px table header, 10px/16px rows on a 0.045 separator,
   0.03 hover, .12s ease. Where a number here differs from the console it is
   because the element does not exist there, never because it was rounded.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Greeting ── */
.greet { margin-bottom: 22px; }
.greet-title {
  /* 23px = 21px +10%. The subtitle below is deliberately NOT scaled with it —
     widening the gap between the two is the point of the change. */
  font-size: 27.5px; font-weight: 800; letter-spacing: -0.022em;
  margin: 0; line-height: 1.25;
}
.greet-sub { margin-top: 5px; font-size: 16px; color: var(--text-2); }

/* ── Overview metrics: ONE box holding three numbers, not three cards.
      The container treatment is the Analytics hero's, reused rather than
      re-invented (`.hero` / `.hero-stat`, below): a single border and radius
      around the whole row, and the dividers drawn by a 1px grid gap over a
      `--line` background instead of per-child borders — which is what keeps
      them full-bleed to the box edges and free of the double-hairline a
      border-left produces next to the container's own border.

      The type stays Home's, not Analytics'. Home's numbers are an overview,
      not the subject of the page, so the 26px value is unchanged; only the
      surface around them is borrowed. Horizontal padding is 16px to match
      `.tbl-row`, so the metric values and the Subjects table's names below
      start on the same vertical line. ── */
.metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: var(--gap);
}
.metric {
  background: var(--panel);
  padding: 18px 16px 16px;
  min-width: 0;
  border: 0;
  font-family: inherit; text-align: left; color: inherit;
  transition: background 0.12s ease;
}
button.metric { cursor: pointer; }
/* `--panel-2` is the console's own hover surface (nav items, table rows). An
   rgba wash would let the `--line` backdrop show through the gap seams. */
button.metric:hover { background: var(--panel-2); }
.metric-value { font-size: 30.5px; font-weight: 800; letter-spacing: -0.025em; line-height: 1; font-variant-numeric: tabular-nums; }
.metric-label { display: flex; align-items: center; gap: 5px; font-size: 15px; color: var(--text-2); margin-top: 6px; }
button.metric .metric-label { color: var(--accent); transition: color 0.12s ease; }
button.metric:hover .metric-label { color: var(--accent-hi); }
.metric-arrow { transition: transform 0.14s ease; }
button.metric:hover .metric-arrow { transform: translateX(2px); }
/* Same stack the Analytics hero makes on a phone: one column, the 1px gap now
   drawing horizontal rules between the three. */
[data-layout="mobile"] .metrics { grid-template-columns: minmax(0, 1fr); }

/* ── each metric's hover explanation ──
   The BUBBLE is the design system's own `.tip-bubble` (see the `?` tooltip
   below) — same surface, border, radius, type and shadow, nothing new drawn.
   Only its positioning is replaced, and for one specific reason: `.metrics`
   sets `overflow: hidden`, which is what rounds the grid's corners, and it
   would equally clip a bubble reaching outside the box. A FIXED element's
   containing block is the viewport rather than that ancestor, so it escapes
   the clip without the container giving up its corners — and being out of
   flow, it cannot move the hero by a pixel. Coordinates are measured on
   reveal, above or below; see `withTip()` in views/home.js.

   COMPOUNDED WITH `.tip-bubble` ON PURPOSE. `.tip-bubble` is declared further
   down this file and carries its own `position`, `bottom`, `left` and
   `transform`; at equal specificity the later rule wins, so a lone
   `.metric-tip` lost every one of them and the bubble stayed absolute — and
   therefore clipped. Compounding both classes settles it by specificity rather
   than by where in the file this block happens to sit. */
.tip-bubble.metric-tip { position: fixed; top: 0; left: 0; bottom: auto; transform: none; }
.tip-bubble.metric-tip.is-shown { display: block; }
/* The type `.tip-bubble` INTENDS. Its own `font: 500 12.5px/1.55 inherit` is
   invalid CSS — the shorthand's family slot rejects the `inherit` keyword — so
   the whole declaration is dropped and every bubble on the dashboard inherits
   its host's type instead. Here that is visible rather than merely wrong: two
   of these three hang off divs (14px) and one off a button (13.3px), so the
   row's three explanations would not even match each other. Longhands, so they
   apply. (The shorthand itself is left alone: fixing it would change the
   Analytics tooltips too, which is not this change's business.) */
.tip-bubble.metric-tip { font-size: 15px; font-weight: 500; line-height: 1.5; }
/* SMALL. One short line settling one ambiguity — so the box is tightened from
   the `?`-beside-a-heading version: less padding, a smaller radius and a
   shorter shadow throw. The surface, border and colour are `.tip-bubble`'s own
   and are deliberately untouched — this is the same object, quieter. */
.tip-bubble.metric-tip {
  max-width: min(232px, calc(100vw - 32px));
  padding: 7px 10px;
  border-radius: 8px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.42);
}
/* THE `?` — the design system's own `.tip-btn`, one size down. Beside a 12.5px
   label a 17px mark competes with it; 14px sits under the cap height and reads
   as secondary, which is the whole brief for it. Nothing else about it changes,
   so it is the same glyph, border and hover the Analytics headings carry.

   Type is spelled out because `.tip-btn`'s own `font: 700 10.5px/1 inherit` is
   invalid CSS and is dropped whole — the shorthand's family slot rejects the
   `inherit` keyword — so the glyph inherits whatever its host gives it. That is
   invisible where every `?` is a button, and visible here, where one of the
   three is a span: 12.5px Inter beside 13.3px system UI, one bold and one not. */
.metric-label .tip-btn {
  width: 14px; height: 14px; margin-left: 1px;
  font-family: inherit; font-size: 11px; font-weight: 700; line-height: 1;
}
/* Inside Student Doubts the mark is a decorative span, not a button, and the
   cell around it navigates. `cursor: help` there would promise an explanation
   to a click that actually leaves the page, so it inherits the cell's pointer. */
.metric-label .tip-btn.is-static { cursor: inherit; }
/* The arrow ends the row, the `?` explains the label, so the `?` goes first and
   the flex `gap` keeps them from crowding. */
.metric-label .metric-arrow { order: 1; }
/* A 14px mark is the right SIZE and the wrong TARGET: this dashboard's mobile
   composition holds every target to 44px, and a finger does not get smaller
   because a glyph did. The hit area is grown with an absolutely-positioned
   pseudo-element — 14 + 2×15 = 44 — so nothing about the layout or the drawn
   mark changes, only what counts as touching it. */
[data-layout="mobile"] .metric-label .tip-btn { position: relative; }
[data-layout="mobile"] .metric-label .tip-btn::after { content: ''; position: absolute; inset: -15px; }
/* The global ring is `outline-offset: 2px`, which `overflow: hidden` clips to
   an L-shape on the outer two metrics. Inset it instead — same ring, same
   width, drawn inside the cell, so focus stays visible and nothing resizes. */
.metric:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 0; }

/* ── Section heading, used where a block earns page-level weight rather than
      being one more titled card. Subjects is the only one on Home. ── */
.section {
  display: flex; align-items: baseline; gap: 12px;
  margin: 0 0 10px;
}
.section-title { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }
.section-note { font-size: 15px; color: var(--text-3); margin-left: auto; }

/* ── Table (the console's data list) ── */
.tbl {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.tbl-head {
  display: grid; align-items: center;
  padding: 9px 16px;
  border-bottom: 1px solid var(--line);
  font-size: 14px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-2);
}
.tbl-row {
  display: grid; align-items: center;
  width: 100%;
  padding: 10px 16px;
  border: 0; border-bottom: 1px solid var(--line-row);
  background: transparent; color: inherit;
  font-family: inherit; font-size: 16px; text-align: left;
  transition: background 0.12s ease;
}
button.tbl-row { cursor: pointer; }
button.tbl-row:hover { background: rgba(237, 230, 217, 0.03); }
button.tbl-row:active { background: rgba(237, 230, 217, 0.055); }
.tbl-name { font-weight: 650; letter-spacing: -0.005em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tbl-num { font-variant-numeric: tabular-nums; color: var(--text-val); }
.tbl-empty { padding: 26px 16px; text-align: center; font-size: 16px; color: var(--text-3); line-height: 1.6; }
.tbl-empty strong { display: block; color: var(--text); font-weight: 700; font-size: 16.5px; margin-bottom: 5px; }

/* The table's own footer action — part of the list, not a floating button. */
.tbl-foot {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 11px 16px;
  border: 0; background: transparent;
  font-family: inherit; font-size: 16px; font-weight: 650;
  color: var(--accent); cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.tbl-foot:hover { background: rgba(237, 230, 217, 0.03); color: var(--accent-hi); }
.tbl-foot svg { flex: none; }

/* ── Two-up region (Students | Needs attention) ── */
.duo { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); gap: var(--gap); align-items: start; }

/* ── Card: the console's titled panel ── */
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
}
.card-title { font-size: 16.5px; font-weight: 700; margin-bottom: 12px; }
.card-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
.card-head .card-title { margin-bottom: 0; }
.card.is-flush { padding: 16px 16px 4px; }

/* ── Student figures. Two numbers, no chart — the brief and the product
      principles both refuse decorative analytics here. ── */
.figures { display: flex; gap: 28px; }
.figure-value { font-size: 30.5px; font-weight: 800; letter-spacing: -0.025em; line-height: 1; font-variant-numeric: tabular-nums; }
.figure-label { font-size: 15px; color: var(--text-2); margin-top: 6px; }

/* ── A quiet trailing action ── */
.more {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 16px;
  padding: 0; border: 0; background: none;
  font-family: inherit; font-size: 15.5px; font-weight: 650;
  color: var(--accent); cursor: pointer;
  transition: color 0.12s ease;
}
.more:hover { color: var(--accent-hi); }
.more-arrow { transition: transform 0.14s ease; }
.more:hover .more-arrow { transform: translateX(2px); }

/* ── Needs attention ── */
.attn { display: flex; flex-direction: column; margin: -4px -6px; }
.attn-item {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 10px 6px;
  border: 0; border-radius: 8px; background: transparent;
  font-family: inherit; text-align: left; color: inherit;
  cursor: pointer;
  transition: background 0.12s ease;
}
.attn-item:hover { background: rgba(237, 230, 217, 0.04); }
.attn-item + .attn-item { box-shadow: inset 0 1px 0 var(--line-soft); }
.attn-label { font-size: 16px; line-height: 1.45; }
.attn-meta { font-size: 14.5px; color: var(--text-3); margin-top: 2px; }
.attn-go { margin-left: auto; color: var(--text-3); flex: none; }
.attn-item:hover .attn-go { color: var(--accent); }

/* All caught up — a real, common state, given the same calm as everything else. */
.caught-up { display: flex; align-items: center; gap: 10px; padding: 4px 0 2px; }
.caught-up-icon { color: var(--green); flex: none; }
.caught-up-text { font-size: 16px; color: var(--text-val); }
.caught-up-sub { font-size: 15px; color: var(--text-3); margin-top: 2px; }

/* ── Recent activity — the console's own row: 7px dot, 7px/0 padding, a 0.05
      separator, 13.5px/1.45 text over a 12px muted timestamp. ── */
.feed { display: flex; flex-direction: column; }
.feed-row {
  display: flex; gap: 10px; align-items: flex-start;
  width: 100%;
  padding: 8px 6px; margin: 0 -6px;
  border: 0; border-radius: 8px; background: transparent;
  font-family: inherit; text-align: left; color: inherit;
  cursor: pointer;
  transition: background 0.12s ease;
}
.feed-row:hover { background: rgba(237, 230, 217, 0.03); }
.feed-row + .feed-row { box-shadow: inset 0 1px 0 var(--line-soft); }
.feed-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex: none; background: var(--text-3); }
.feed-dot.is-publish { background: var(--green); }
.feed-dot.is-activation { background: var(--accent); }
.feed-dot.is-create { background: var(--blue); }
.feed-dot.is-question { background: var(--tan); }
.feed-text { font-size: 16px; line-height: 1.45; }
.feed-time { font-size: 14.5px; color: var(--text-3); margin-top: 1px; }

/* ── Skeletons that match the real rows, so nothing jumps on arrival ── */
.sk { background: rgba(237, 230, 217, 0.06); border-radius: 5px; height: 10px; }
.sk.tall { height: 22px; border-radius: 6px; }

/* ══ Dialog — the console's overlay, verbatim ══ */
.scrim {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(10, 8, 6, 0.62);
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  /* Opacity only — `fade-up` also animates `transform`, which silently
     cancelled the centring translate used to pin the sheet to the phone frame
     on the /faculty/mobile/ route. A scrim has nothing to slide anyway. */
  animation: scrim-in 0.16s ease both;
}
@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }
.dialog {
  background: var(--panel);
  border: 1px solid var(--line-2);
  border-radius: 14px;
  padding: 22px;
  width: 420px; max-width: 100%;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.dialog-title { font-size: 18px; font-weight: 800; letter-spacing: -0.01em; color: var(--text); }
.dialog-body { font-size: 15.5px; color: var(--text-2); margin: 4px 0 12px; line-height: 1.55; }
.dialog-actions { display: flex; gap: 9px; margin-top: 16px; }
.dialog-actions .btn { flex: 1; }
.dialog-actions .btn-primary { flex: 1.4; }

/* ══════════════════════════════════════════════════════════════════════════
   HOME — MOBILE COMPOSITION
   Not the desktop table narrowed: the columns are dropped and the row becomes
   a two-line card, because three columns inside 375px is a compromise nobody
   asked for. Order is set in views/home.js, not here.
   ══════════════════════════════════════════════════════════════════════════ */

[data-layout="mobile"] .greet { margin-bottom: 18px; }
[data-layout="mobile"] .greet-title { font-size: 26px; }

[data-layout="mobile"] .duo { grid-template-columns: minmax(0, 1fr); }

/* Table → stacked rows. The header row is meaningless with one column. */
[data-layout="mobile"] .tbl-head { display: none; }
[data-layout="mobile"] .tbl-row {
  grid-template-columns: minmax(0, 1fr) auto !important;
  gap: 3px 12px;
  padding: 13px 16px;
  min-height: 60px;
}
.tbl-sub {
  margin-top: 2px;
  font-size: 15px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
[data-layout="mobile"] .tbl-foot { padding: 14px 16px; min-height: 52px; }

/* Every tappable row on Home clears 44px, on any pointer, because the mobile
   composition is inspected on a laptop at /faculty/mobile/. */
[data-layout="mobile"] .attn-item { min-height: 56px; padding: 11px 6px; }
[data-layout="mobile"] .feed-row { min-height: 48px; padding: 10px 6px; }
[data-layout="mobile"] .more { min-height: 44px; margin-top: 10px; }

[data-layout="mobile"] .figures { gap: 0; }
[data-layout="mobile"] .figures > * { flex: 1; }
[data-layout="mobile"] .figure-value { font-size: 28.5px; }

/* A dialog on a phone is a bottom sheet — reachable, and it does not float in
   the middle of a screen the thumb cannot comfortably reach. */
[data-layout="mobile"] .scrim { align-items: flex-end; padding: 0; }
[data-layout="mobile"] .dialog {
  width: 100%;
  border-radius: 16px 16px 0 0;
  border-bottom: 0;
  padding: 20px 18px calc(20px + env(safe-area-inset-bottom));
  animation: sheet-up 0.22s cubic-bezier(0.32, 0.72, 0, 1) both;
}
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
[data-layout="mobile"] .dialog-actions .btn { min-height: 46px; }

/* ── Student Doubts ────────────────────────────────────────────────────────
   A DENSE TABLE, NOT A STACK OF CARDS. The previous Questions screen was a
   list of expandable panels because it answered one doubt at a time; this one
   is a workspace for answering many, so it borrows the ANALYTICS table's
   idiom — `.an-tr` / `.an-thead` / `.an-th` / `.an-td` — rather than inventing
   a third table in this stylesheet. Same sticky header, same uppercase 10.5px
   column labels, same hairline rows, same one-inset rule.

   It reuses that page's spacing scale for the same reason: two spreadsheets in
   one product that pad differently read as two products.

   THE TWO ROW TEMPLATES ARE SET IN JS, not here (`GROUPED_COLS` / `FLAT_COLS`
   in views/doubts.js), because the child row deliberately reuses the parent's
   trailing columns and the two must be written next to each other to stay in
   step. */
.view.is-doubts {
  max-width: 1440px;          /* eight columns and a 420px panel need it */
  --s1: 9px;
  --s2: 16px;
  --s3: 24px;
  --s4: 40px;
}

/* ── the summary line: four facts, each a control ── */
.dbt-summary {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 2px;
  min-height: 34px;
  margin-bottom: var(--s2);
}
/* AUDITED 2026-09-10, mobile: `views/doubts.js` falls back to an EMPTY
   `.dbt-summary` (no chips, nothing) whenever there is nothing to
   summarise — zero doubts, or the current filter matches none. The 34px
   floor above exists to keep a REAL summary from looking cramped; applied
   to a genuinely empty one it became 34px of blank, unexplained space
   between the title and the tabs below it, with nothing painted in it to
   justify the gap. `:empty` is exactly the DOM state the fallback already
   produces (no children passed to el(...)), so this needs no JS change: a
   populated summary (chip buttons as children) is untouched, an empty one
   collapses to nothing. */
.dbt-summary:empty { min-height: 0; margin-bottom: 0; }
.dbt-dot { color: var(--text-3); padding: 0 2px; }
.dbt-chip {
  border: 0; background: transparent;
  font-family: inherit; font-size: 15.5px; font-weight: 600;
  color: var(--text-2);
  padding: 5px 8px; border-radius: 7px;
  cursor: pointer;
  transition: color 0.12s ease, background 0.12s ease;
}
.dbt-chip:hover { color: var(--text); background: rgba(237, 230, 217, 0.045); }
.dbt-chip.is-on { color: var(--accent); }

/* ── the selection bar: same row, same height, so nothing jumps ── */
.dbt-selbar {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  min-height: 34px;
  margin-bottom: var(--s2);
  padding: 0 2px;
}
.dbt-selcount { font-size: 15.5px; color: var(--text-2); }
.dbt-selcount strong { color: var(--text); font-weight: 700; }
.dbt-selactions { display: flex; gap: 8px; margin-left: auto; }
.btn-sm { min-height: 32px; padding: 0 12px; font-size: 15px; }

/* ── controls ── */
.dbt-controls {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap;
  margin-bottom: var(--s2);
}
.dbt-controls .tabs { margin-bottom: 0; border-bottom: 0; flex: none; }
.dbt-controls-right {
  display: flex; align-items: center; gap: var(--s1);
  margin-left: auto; flex-wrap: wrap;
}
.dbt-search {
  min-height: 34px; width: 300px; max-width: 100%;
  font-size: 15.5px; padding: 0 12px;
}
.dbt-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 15px; font-weight: 600; color: var(--text-2);
  cursor: pointer; white-space: nowrap;
  padding: 0 4px;
}
.dbt-toggle:hover { color: var(--text); }

/* ── checkboxes. One accent, square, no custom SVG: the native control is
      focusable and announces itself, and restyling it into a div loses both.
      `.check` is the same control under a name that is not this screen's — the
      Account area's permission matrix uses it, and a second implementation of a
      checkbox is how one of them ends up a different size. ── */
.dbt-check, .check {
  appearance: none;
  width: 15px; height: 15px; flex: none;
  margin: 0;
  border: 1px solid var(--line-2);
  border-radius: 4px;
  background: var(--page);
  cursor: pointer;
  display: grid; place-content: center;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.dbt-check:hover, .check:hover { border-color: var(--accent); }
.dbt-check:checked, .dbt-check:indeterminate,
.check:checked, .check:indeterminate {
  background: var(--accent); border-color: var(--accent);
}
.dbt-check:checked::after, .check:checked::after {
  content: ''; width: 8px; height: 4.5px;
  border-left: 2px solid #191613; border-bottom: 2px solid #191613;
  transform: translateY(-1px) rotate(-45deg);
}
.dbt-check:indeterminate::after, .check:indeterminate::after {
  content: ''; width: 8px; height: 2px; background: #191613; border-radius: 1px;
}
.dbt-check:focus-visible, .check:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.check:disabled { opacity: 0.45; }

/* ── the workspace: table left, panel docked right ── */
/* The panel gives ground before the spreadsheet does. It holds one answer box
   and a readable list; the table holds eight columns, and squeezing those is
   the more expensive loss — so the panel is elastic and the table is not. */
.dbt-work {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(320px, 26vw, 420px);
  gap: var(--s3);
  align-items: start;
}

.dbt-table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  overflow: auto;
  /* The table scrolls inside itself so the sticky header and the panel stay
     put. The subtrahend is the page header plus the two control rows above it. */
  max-height: calc(100dvh - 198px);
  min-height: 240px;
}
/* The floor the columns need to stay legible. Between roughly 1160px and
   1480px of window the docked panel leaves less than this, and the table SCROLLS
   sideways inside its own wrap rather than crushing its columns — the same
   choice this stylesheet already makes for the Analytics table and for every
   table on a phone. Losing the panel would cost more than a scrollbar does. */
.dbt-table { min-width: 860px; }

.dbt-tr {
  display: grid;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s3);
  border-bottom: 1px solid var(--line-row);
}
.dbt-tr:last-child { border-bottom: 0; }

.dbt-thead {
  position: sticky; top: 0; z-index: 2;
  background: var(--panel);
  border-bottom: 1px solid var(--line-2);
}
.dbt-th {
  font: 750 10.5px/1 inherit;
  text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-3);
  padding: 12px 0;
}
.dbt-th.is-num { text-align: right; }

.dbt-cell { min-width: 0; font-size: 16px; padding: 0; }
.dbt-num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 650; color: var(--text-val); }
.dbt-num.is-late { color: var(--accent); }

/* Everything after FLASHCARD is centred in its column, header and cell on the
   same axis. Scoped to the grouped table: the flat list is a different set of
   columns and keeps its own alignment. */
.dbt-thead.is-grouped .dbt-th:not(:first-child),
.dbt-group .dbt-cell:not(:first-child) { text-align: center; }

/* ── the flashcard row ── */
.dbt-group {
  min-height: 46px;
  background: rgba(237, 230, 217, 0.014);
  transition: background 0.12s ease;
}
.dbt-group:hover { background: rgba(237, 230, 217, 0.032); }
.dbt-group.is-active { background: var(--selected); }
.dbt-groupblock + .dbt-groupblock .dbt-group { border-top: 1px solid var(--line); }

.dbt-cardcell { display: flex; align-items: center; gap: 9px; min-width: 0; }
.dbt-cardlink {
  border: 0; background: transparent; padding: 0;
  font-family: inherit; font-size: 16px; font-weight: 650;
  letter-spacing: -0.005em;
  color: var(--text); text-align: left;
  cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0;
  border-bottom: 1px solid transparent;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.dbt-cardlink:hover { color: var(--accent-hi); border-bottom-color: rgba(201, 168, 122, 0.45); }
.dbt-cardlink.is-inline { font-weight: 500; font-size: 15px; color: var(--text-2); }

/* The one hint that a cluster is folded inside. Quiet — it is a proposal. */
.dbt-alike {
  flex: none;
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--tan);
  background: rgba(201, 178, 143, 0.12);
  padding: 2px 7px; border-radius: 999px;
  white-space: nowrap;
}
.dbt-chapter {
  font-size: 15px; color: var(--text-mid);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── the doubt row ── */
.dbt-doubt { min-height: 42px; transition: background 0.12s ease; }
.dbt-doubt:hover { background: rgba(237, 230, 217, 0.022); }
.dbt-doubt.is-active { background: var(--selected); }
.dbt-text.is-span { grid-column: 3 / 6; }
/* The doubt and its "Not sent" mark sit on ONE line: the text flexes and
   ellipsises, the mark keeps its width. `width: 100%` on the button pushed the
   mark onto a second line and doubled the row's height on exactly the rows that
   had gone wrong. */
.dbt-text { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dbt-open {
  border: 0; background: transparent; padding: 0;
  font-family: inherit; font-size: 15.5px; color: var(--text-val);
  text-align: left; cursor: pointer;
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color 0.12s ease;
}
.dbt-open:hover { color: var(--text); }
.dbt-student {
  font-size: 15px; color: var(--text-mid);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dbt-age { font-weight: 500; color: var(--text-3); font-size: 15px; }
.dbt-failed {
  flex: none;
  font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--danger);
}

/* Status and the hover actions share one cell: the actions REPLACE the badge
   on hover rather than sitting in a column of their own, so scanning the table
   never means scanning past thirty buttons. */
.dbt-status { position: relative; display: flex; align-items: center; }
/* The actions REPLACE the badge; they must therefore paint over it opaquely.
   `background: inherit` inherits the *value* `transparent` from the row, not the
   row's rendered colour, so the badge showed straight through the buttons. The
   panel colour is the row's own ground, and the two row tints are re-stated on
   hover so the strip matches whichever row it is covering. */
.dbt-rowactions {
  position: absolute; inset: 0;
  display: flex; align-items: center; gap: 3px;
  background: var(--panel);
  opacity: 0; pointer-events: none;
  transition: opacity 0.1s ease;
}
.dbt-doubt:hover .dbt-rowactions { background: #262119; }
.dbt-doubt.is-active .dbt-rowactions { background: var(--selected); }
.dbt-doubt:hover .dbt-rowactions,
.dbt-rowactions:focus-within { opacity: 1; pointer-events: auto; }
.dbt-act {
  border: 1px solid transparent; background: transparent;
  color: var(--text-3);
  width: 26px; height: 26px; border-radius: 6px;
  display: grid; place-items: center; cursor: pointer;
  transition: color 0.12s ease, background 0.12s ease;
}
.dbt-act:hover { color: var(--text); background: rgba(237, 230, 217, 0.07); }
.dbt-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── a proposed cluster ──
   An inline rule and a label, not a nested table. It says "look alike", and
   the accent bar down the left is the only thing marking its members. */
.dbt-cluster {
  display: flex; align-items: center; gap: 10px;
  padding: 7px var(--s3) 7px calc(var(--s3) + 50px);
  border-bottom: 1px solid var(--line-row);
  background: rgba(201, 178, 143, 0.045);
}
.dbt-cluster-label {
  display: flex; align-items: center; gap: 9px;
  font-size: 14.5px; font-weight: 700; color: var(--tan);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.dbt-cluster-note {
  text-transform: none; letter-spacing: 0; font-weight: 500;
  color: var(--text-3); font-size: 14px;
}
.dbt-cluster .btn { margin-left: auto; }
.dbt-doubt.in-cluster { box-shadow: inset 2px 0 0 rgba(201, 178, 143, 0.42); }

/* ── the docked panel ── */
.dbt-panel {
  position: sticky; top: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  max-height: calc(100dvh - 248px);
  overflow: auto;
  min-height: 240px;
}
.dbt-panel-empty { padding: var(--s3); color: var(--text-3); }
.dbt-panel-empty p { font-size: 15px; line-height: 1.65; margin: 0 0 10px; }
.dbt-panel-body { padding: var(--s3); display: flex; flex-direction: column; gap: var(--s2); }
.dbt-panel-head { display: flex; align-items: center; gap: 10px; }
.dbt-panel-title {
  font-size: 17px; font-weight: 750; letter-spacing: -0.01em;
  color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dbt-panel-head .badge { margin-left: auto; flex: none; }
.dbt-panel-sub { font-size: 15px; color: var(--text-3); margin-top: -8px; }

.dbt-quote {
  font-size: 16px; line-height: 1.6; color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 13px;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

.dbt-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px var(--s2); }
.dbt-fact { min-width: 0; }
.dbt-fact-k {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-3);
}
.dbt-fact-v { font-size: 15px; color: var(--text-val); margin-top: 3px; overflow-wrap: anywhere; }

.dbt-cardbox { display: flex; flex-direction: column; gap: 5px; }
.dbt-cardbox .dbt-cardlink { white-space: normal; font-size: 15.5px; line-height: 1.5; }
.dbt-cardstats { font-size: 15px; color: var(--text-2); line-height: 1.6; }
.dbt-cardstats strong { color: var(--text); font-weight: 700; }

.dbt-answered {
  border-left: 2px solid var(--green);
  padding-left: 11px;
}
.dbt-answer-text {
  font-size: 15px; color: var(--text-val); line-height: 1.6; margin-top: 4px;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Every selected doubt, readable, each removable. This list IS the guard
   against a careless bulk send, so it is never collapsed to a count. */
.dbt-sellist {
  display: flex; flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  max-height: 260px; overflow: auto;
}
.dbt-selitem {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 10px 11px;
  border-bottom: 1px solid var(--line-row);
}
.dbt-selitem:last-child { border-bottom: 0; }
.dbt-selitem-main { min-width: 0; flex: 1; }
.dbt-selitem-q { font-size: 15px; line-height: 1.5; color: var(--text-val); overflow-wrap: anywhere; }
.dbt-selitem-meta { font-size: 13.5px; color: var(--text-3); margin-top: 3px; }

.dbt-draft { width: 100%; resize: vertical; min-height: 120px; line-height: 1.55; padding: 11px 13px; font-size: 16px; }
.dbt-panel-actions { display: flex; gap: 9px; flex-wrap: wrap; }
.dbt-panel-actions .btn { flex: 1; }
.dbt-panel-note { font-size: 14px; color: var(--text-3); line-height: 1.6; margin: 0; }
.dbt-panel-note.is-warn { color: var(--tan); }

.dbt-error {
  font-size: 15px; color: #E88C8C; line-height: 1.6;
  background: rgba(214, 69, 69, 0.09);
  border: 1px solid rgba(214, 69, 69, 0.28);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  margin-bottom: var(--s2);
}

/* ── narrow desktop: the panel goes under the table rather than squeezing the
      spreadsheet below the width its columns need. ── */
@media (max-width: 1160px) {
  .dbt-work { grid-template-columns: minmax(0, 1fr); }
  .dbt-panel { position: static; max-height: none; }
  .dbt-table-wrap { max-height: calc(100dvh - 250px); }
}

/* ── mobile: not the desktop table narrowed. The table keeps its columns and
      scrolls sideways like a spreadsheet — collapsing Doubts/Students into a
      card would destroy the one thing this screen is for — and the panel
      becomes a block underneath it. ── */
[data-layout="mobile"] .view.is-doubts { max-width: 100%; }
[data-layout="mobile"] .dbt-work { grid-template-columns: minmax(0, 1fr); }
[data-layout="mobile"] .dbt-panel { position: static; max-height: none; }
[data-layout="mobile"] .dbt-table-wrap { max-height: 62dvh; }
[data-layout="mobile"] .dbt-controls-right { width: 100%; }
[data-layout="mobile"] .dbt-search { width: 100%; }
[data-layout="mobile"] .dbt-selactions { margin-left: 0; width: 100%; }
[data-layout="mobile"] .dbt-selactions .btn { flex: 1; min-height: 40px; }
[data-layout="mobile"] .dbt-facts { grid-template-columns: minmax(0, 1fr); }
[data-layout="mobile"] .dbt-panel-actions .btn { min-height: 44px; }
/* Touch has no hover, so the row actions would never appear. They become part
   of the row instead of an overlay — and the badge STAYS: a column headed
   "Status" whose cells contain no status is worse than a slightly wider column.
   The cell gets the room by widening, not by dropping the thing it is named for. */
[data-layout="mobile"] .dbt-status {
  position: static;
  gap: 6px;
  justify-content: flex-start;
}
[data-layout="mobile"] .dbt-rowactions {
  position: static; inset: auto;
  opacity: 1; pointer-events: auto;
  background: transparent;
}
/* Every target a thumb has to hit clears 44px, the rule this stylesheet already
   enforces for every other control in the mobile composition. */
[data-layout="mobile"] .dbt-act { width: 40px; height: 40px; }
[data-layout="mobile"] .dbt-check, [data-layout="mobile"] .check { width: 20px; height: 20px; }
[data-layout="mobile"] .perm-opt { min-height: 36px; }
/* ── THE INTENTIONAL SIDEWAYS SCROLL, MADE TO FEEL INTENTIONAL ─────────────
   `.dbt-table { min-width: 860px }` (above) is a deliberate choice already
   made for this table — sideways scroll over crushed columns, the same call
   this stylesheet makes for the Analytics table too. What was missing on a
   phone was the part that makes a deliberate choice READ as one: nothing
   marked the boundary, so the flashcard name simply vanished under the
   fingertip mid-scroll, indistinguishable from a layout bug.

   Pinning the identifying column FIXES that — the doubt being read never
   scrolls out of view no matter how far right the rest of the row goes — and
   the seam is the visible edge a sticky element already has to have. */
[data-layout="mobile"] .dbt-tr .dbt-cell:first-child {
  display: flex; align-items: center;
  min-height: 44px;
  position: sticky; left: 0; z-index: 1;
  /* OPAQUE, deliberately, not `inherit`. A default row has NO background
     of its own (transparent, so the panel shows through) — inheriting that
     let the columns scrolled underneath show through the "pinned" cell,
     which defeated the entire point of pinning it. `var(--panel)` matches
     the table wrap it sits in, so a resting row is seamless; the two states
     that paint the ROW a different colour re-paint this cell to match,
     directly below. */
  background: var(--panel);
  box-shadow: 2px 0 6px -2px rgba(0, 0, 0, 0.35);
  padding-right: var(--s2);
}
[data-layout="mobile"] .dbt-table-wrap {
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
/* The two row states that paint a background of their own repaint the
   pinned cell to match — an opaque default (above) would otherwise show as a
   seam of the wrong colour across an active or grouped row. */
[data-layout="mobile"] .dbt-group.is-active .dbt-cell:first-child,
[data-layout="mobile"] .dbt-doubt.is-active .dbt-cell:first-child {
  background: var(--selected);
}
/* The header row uses `.dbt-th`, not `.dbt-cell` (see views/doubts.js), so it
   needs its own pin — otherwise "Flashcard"/"Doubt" scrolls away while the
   data beneath it stays put, and a column would end up unlabelled mid-scroll.
   Both axes declared explicitly: `.dbt-thead` is already sticky to the top of
   the vertical scroll, and this corner cell has to hold that same top AND
   pin to the left of the horizontal scroll at once. */
[data-layout="mobile"] .dbt-thead .dbt-th:first-child {
  position: sticky; left: 0; top: 0; z-index: 3;
  background: var(--panel);
  box-shadow: 2px 0 6px -2px rgba(0, 0, 0, 0.35);
  padding-right: var(--s2);
}
[data-layout="mobile"] .dbt-doubt, [data-layout="mobile"] .dbt-group { min-height: 48px; }


/* ── Analytics (Content Intelligence) ───────────────────────────────────────
   ONE SPACING SCALE, used everywhere on this page. Before this pass the page
   ran on one-off values — 52px between sections, plus a 56px divider margin
   that stacked into a 109px trench between the charts and the table, so the
   sections read as separate screens rather than one continuous page.

     --s1  8px   inside a row
     --s2 14px   heading → its body, and between controls
     --s3 20px   panel padding, and the inset of every panel's content
     --s4 34px   between sections — the page's whole vertical rhythm

   --s3 is the load-bearing one: every panel pads by it, so chart content,
   bar labels, table cells and list rows all start on the same vertical line.
   Reach for a new number only with a reason worth writing down. */
.view.is-analytics {
  max-width: 1180px;
  --s1: 9px;
  --s2: 16px;
  --s3: 24px;
  --s4: 40px;
}

/* ── section rhythm ── */
.an-section { margin-top: var(--s4); }
.an-section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin-bottom: var(--s2);
  flex-wrap: wrap;
  min-height: 30px;
}
.sec-title {
  font-size: 21.5px;
  font-weight: 750;
  letter-spacing: -0.02em;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--text);
  line-height: 1.3;
}
.an-section-head > :last-child:not(.sec-title) { margin-left: auto; }

/* Two side-by-side insight blocks. The sections inside carry no top margin of
   their own — the row owns the rhythm, so a two-column row and a full-width
   section sit exactly the same distance apart. */
.an-cols { display: grid; gap: var(--s3); margin-top: var(--s4); }
.an-cols.is-two { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
.an-cols.is-three { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
.an-cols > .an-section { margin-top: 0; }

/* ── the ? tooltip ── */
.tip { position: relative; display: inline-flex; }
.tip-btn {
  width: 17px; height: 17px;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--text-3);
  font: 700 10.5px/1 inherit;
  cursor: help;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  flex: none;
}
.tip-btn:hover, .tip-btn:focus-visible { color: var(--text); border-color: var(--accent); outline: none; }
.tip-bubble {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(268px, calc(100vw - 32px));
  background: var(--popover);
  border: 1px solid var(--line-2);
  border-radius: 10px;
  padding: 10px 12px;
  font: 500 12.5px/1.55 inherit;
  color: var(--text-val);
  letter-spacing: 0;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  /* display, not visibility: a hidden-but-laid-out bubble still counts toward
     the page scroll width, which widened the whole page on mobile. */
  display: none;
  z-index: 60;
  text-align: left;
  pointer-events: none;
}
.tip:hover .tip-bubble,
.tip.is-open .tip-bubble,
.tip-btn:focus-visible + .tip-bubble { display: block; }

/* ── hero ── */
.hero {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
/* Three rows shared by all three tiles, so the value, the label and the note
   sit on the same lines across the row however long any one label runs. */
.hero-stat {
  background: var(--panel);
  padding: 26px var(--s3) 22px;
  min-width: 0;
  display: grid;
  grid-template-rows: auto auto auto;
  align-content: start;
  /* 4px between label and note; the value gets its own 12px via .hero-label,
     so the tighter pairing needs no negative margin to claw space back. */
  row-gap: 4px;
}
.hero-value {
  font-size: 44px;
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  min-height: 42px;
}
.hero-of { font-size: 27.5px; font-weight: 700; color: var(--text-3); letter-spacing: -0.02em; }
.hero-unknown { color: var(--text-3); }
.hero-label {
  margin-top: 8px;
  font-size: 16px;
  font-weight: 650;
  color: var(--text-mid);
  display: flex;
  align-items: center;
  gap: 7px;
  row-gap: 0;
}
.hero-sub { font-size: 14.5px; color: var(--text-3); min-height: 16px; }

/* ── panels: one inset for everything ── */
.an-bars,
.an-pane-body,
.an-list,
.an-filters,
.an-pending {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

/* ── revision by topic ── */
.an-bars { padding: var(--s2) var(--s3); }
/* Fixed label column, so every bar begins on the same x whatever the topic is
   called, and a long name truncates instead of shifting the chart. */
.an-bar-row {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr) 64px;
  align-items: center;
  gap: var(--s2);
  padding: 7px 0;
}
.an-bar-label { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.an-bar-track { height: 9px; border-radius: 999px; background: rgba(237, 230, 217, 0.06); overflow: hidden; }
.an-bar-fill { height: 100%; background: var(--accent); border-radius: 999px; opacity: 0.85; }
.an-bar-value { font-size: 15.5px; font-variant-numeric: tabular-nums; color: var(--text-mid); text-align: right; }

/* ── toolbar + filters ── */
.an-toolbar { display: flex; align-items: center; gap: var(--s1); margin-bottom: var(--s2); flex-wrap: wrap; }
.an-search { flex: 1 1 220px; max-width: 300px; padding: 8px 12px; font-size: 15.5px; }
.an-chip {
  margin-left: 6px; background: var(--accent); color: #191613;
  border-radius: 999px; padding: 1px 6px; font-size: 13px; font-weight: 800;
}
/* Compact and comfortable, not large and empty: the panel held four controls
   in 222px of height before this pass. */
.an-filters {
  padding: var(--s3);
  margin-bottom: var(--s2);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
/* ONE grid, fixed track width. Previously each control sized itself to its own
   longest option — 138 / 114 / 105 / 113px — so the labels above them never
   lined up and the row read as unrelated elements. */
.an-field-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, 172px);
  gap: var(--s2) var(--s2);
  align-items: end;
}
.an-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.an-field-label {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-3);
  min-height: 13px;
}
.an-field select { width: 100%; }
.an-select.is-sm { padding: 7px 10px; font-size: 15px; font-weight: 600; }
.an-topics { display: flex; flex-wrap: wrap; gap: 6px; }
.an-topic {
  border: 1px solid var(--line-2); background: transparent; color: var(--text-mid);
  border-radius: 999px; padding: 5px 11px; font: 600 12.5px/1.35 inherit; cursor: pointer;
}
.an-topic:hover { color: var(--text); border-color: var(--accent); }
.an-topic.is-on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-hi); }

/* ── Cards needing attention: collapsed by default ──────────────────────────
   The summary keeps the finding visible while the spreadsheet stays folded
   away, so the page answers "is anything wrong?" without becoming a workspace
   until asked. Compact by construction: one description line and one row. */
.an-collapsed {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 16px var(--s3);
}
.an-collapsed-desc { margin: 0 0 12px; font-size: 15.5px; color: var(--text-3); line-height: 1.5; }
.an-collapsed-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  flex-wrap: wrap;
}
.an-collapsed-count { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }

/* Understated, not a CTA: the same small button the rest of the console uses,
   with a chevron carrying the state. */
.an-disclose { flex: none; gap: 7px; }
.an-disclose-chev { font-size: 11px; line-height: 1; color: var(--text-3); }
.an-disclose:hover .an-disclose-chev { color: var(--text); }

/* One short, subtle reveal. Height cannot be animated to `auto` without
   measuring, and a measured animation on a table this size is jank for no
   gain — a fade with a few pixels of travel reads as "this opened" and costs
   nothing. Honoured for users who have not asked for reduced motion. */
.an-expanded { animation: an-reveal 0.16s ease both; }
@keyframes an-reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .an-expanded { animation: none; }
}

/* ── the card table ── */
.an-pane-body { overflow: hidden; }
.an-table-scroll { overflow-x: auto; }
.an-table { min-width: 940px; }
.an-tr {
  display: grid;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s3);
  border-bottom: 1px solid var(--line-row);
}
.an-tr:last-child { border-bottom: 0; }
.an-thead { border-bottom: 1px solid var(--line-2); position: sticky; top: 0; background: var(--panel); z-index: 2; }
.an-th {
  border: 0; background: transparent; padding: 12px 0;
  font: 750 10.5px/1 inherit; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-3); cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: 5px;
}
/* Caret to the LEFT of the label on numeric columns. With it trailing, the
   sorted column's heading sat 14px inside its own numbers — the one column a
   faculty is looking at was the one that came adrift. Reversing the row keeps
   the label's right edge on the column edge whether the caret is there or not. */
.an-th.is-num { flex-direction: row-reverse; justify-content: flex-start; }
.an-th.is-plain { cursor: default; justify-content: flex-start; }
.an-th:hover { color: var(--text-mid); }
.an-th.is-on { color: var(--accent); }
.an-caret { font-size: 10px; }
.an-td { font-size: 16px; padding: 14px 0; min-width: 0; }
.an-td.is-num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 650; }
.an-tr:not(.an-thead):hover { background: rgba(237, 230, 217, 0.022); }
.an-card-front { font-weight: 650; letter-spacing: -0.005em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.an-card-sub { font-size: 14px; color: var(--text-3); margin-top: 3px; }
.an-topic-cell { color: var(--text-mid); font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The lightest possible semantic tint — enough to scan a row, nowhere near a
   traffic light. Good and Easy stay neutral: most reviews land there, and
   colouring the common case only makes the table louder. */
.rt-again { color: #C98F8F; }
.rt-hard { color: var(--tan); }
.rt-good, .rt-easy { color: var(--text-val); }
.is-total { color: var(--text); }
.an-recall { font-size: 16.5px; font-weight: 750; letter-spacing: -0.01em; }

.an-verdict { font-size: 14px; font-weight: 700; white-space: nowrap; }
.an-verdict.is-attention { color: var(--accent); }
.an-verdict.is-steady { color: var(--text-2); }
.an-verdict.is-well { color: var(--green); }
.an-verdict.is-quiet { color: var(--text-3); font-weight: 600; }

.an-table-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: 12px var(--s3); font-size: 15px; color: var(--text-3);
  border-top: 1px solid var(--line);
}

/* ── insight lists ── */
.an-list { overflow: hidden; }
.an-list-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  width: 100%; padding: 14px var(--s3); text-align: left;
  background: transparent; border: 0; border-bottom: 1px solid var(--line-row);
  font: inherit; color: inherit; cursor: pointer;
}
.an-list-row:last-child { border-bottom: 0; }
.an-list-row.is-static { cursor: default; }
button.an-list-row:hover { background: rgba(237, 230, 217, 0.03); }
.an-list-title { font-size: 16px; font-weight: 650; letter-spacing: -0.005em; }
.an-list-sub { font-size: 14px; color: var(--text-3); margin-top: 4px; }
.an-list-sub.is-good { color: var(--green); font-weight: 650; }
.an-list-metric { text-align: right; flex: none; }
.an-metric-strong { font-size: 19px; font-weight: 750; font-variant-numeric: tabular-nums; letter-spacing: -0.015em; }
.an-metric-word { font-size: 14.5px; color: var(--text-2); }
.an-legend { padding: 9px var(--s3) 11px; font-size: 14px; color: var(--text-3); text-align: right; }
.an-note { font-size: 14.5px; color: var(--text-3); flex: none; font-variant-numeric: tabular-nums; }

/* ── shared states, on the same inset as everything else ── */
.an-pending { padding: 22px var(--s3); }
.an-pending-title { font-size: 16px; font-weight: 700; color: var(--text-2); margin-bottom: 6px; }
.an-pending-body { font-size: 15.5px; color: var(--text-3); line-height: 1.65; max-width: 66ch; }
.an-pane-body .tbl-empty,
.an-list .tbl-empty { padding: 26px var(--s3); }

.an-method { margin-top: var(--s4); border-top: 1px solid var(--line); padding-top: 18px; }
.an-method p { font-size: 15px; color: var(--text-3); line-height: 1.7; max-width: 84ch; margin: 0 0 8px; }
.an-method p:last-child { margin-bottom: 0; }

.an-select {
  border: 1px solid var(--line-2); background: var(--panel); color: var(--text);
  border-radius: var(--radius-sm); padding: 8px 12px;
  font-family: inherit; font-size: 15.5px; font-weight: 650; cursor: pointer;
  outline: none; max-width: 320px;
}
.an-select:focus { border-color: var(--accent); }

/* ── narrow desktop / tablet ── */
@media (max-width: 1100px) {
  .an-bar-row { grid-template-columns: minmax(0, 160px) minmax(0, 1fr) 60px; }
}
@media (max-width: 1080px) {
  .an-cols.is-three { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 820px) {
  .an-cols.is-two,
  .an-cols.is-three { grid-template-columns: minmax(0, 1fr); }
}

/* ── mobile ── */
[data-layout="mobile"] .hero { grid-template-columns: minmax(0, 1fr); }
[data-layout="mobile"] .hero-stat { padding: 20px var(--s3); }
[data-layout="mobile"] .hero-value { font-size: 37.5px; min-height: 36px; }
[data-layout="mobile"] .sec-title { font-size: 19.5px; }
[data-layout="mobile"] .an-cols.is-two,
[data-layout="mobile"] .an-cols.is-three { grid-template-columns: minmax(0, 1fr); }
[data-layout="mobile"] .an-select,
[data-layout="mobile"] .an-search { max-width: 100%; width: 100%; }
[data-layout="mobile"] .an-field-row { grid-template-columns: minmax(0, 1fr); }
[data-layout="mobile"] .an-bar-row { grid-template-columns: minmax(0, 116px) minmax(0, 1fr) 48px; gap: 10px; }
[data-layout="mobile"] .an-legend { text-align: left; }
[data-layout="mobile"] .tip-bubble { max-width: 220px; }
[data-layout="mobile"] .an-collapsed-row { align-items: flex-start; flex-direction: column; gap: 12px; }
[data-layout="mobile"] .an-disclose { width: 100%; justify-content: center; }
/* The table keeps every column and scrolls, like a spreadsheet. Collapsing
   Again/Hard/Good/Easy into a card would destroy the one thing it is for. */
[data-layout="mobile"] .an-table-foot { flex-direction: column; align-items: flex-start; gap: var(--s1); }

/* ── Browse Doubt Mode ──────────────────────────────────────────────────────
   A SPLIT, NOT A SCREEN. The left column is this dashboard's doubt panel; the
   right column is the studio iframe, unchanged. Nothing here styles anything
   inside the frame — it cannot, and that is the point: the card and its editor
   are the app's, drawn by the app, in the app's own design language.

   The panel takes a fixed column so the card never has to shrink to make room
   for text that wraps anyway. */
.studio-wrap.is-doubtmode { display: flex; flex-direction: column; min-height: 0; }
/* Three tracks: panel, divider, card. The widths are written inline by
   views/create-decks.js during the drag — this is only the fallback for the
   frame before the stored width is restored. */
.dm-split {
  display: grid;
  grid-template-columns: clamp(280px, 30vw, 460px) 11px minmax(0, 1fr);
  min-height: 0;
  flex: 1;
}

/* The divider, built like the app's own Browse divider: an 11px hit area that
   is easy to grab, with a small centred grip that is what you actually see. */
.dm-divider {
  position: relative;
  cursor: col-resize;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel);
  border-right: 1px solid var(--line);
  touch-action: none;   /* the pointer events are ours, not the scroller's */
}
.dm-divider::after {
  content: '';
  width: 3px; height: 38px;
  border-radius: 3px;
  background: var(--line-2);
  transition: background 0.12s ease;
}
.dm-divider:hover::after,
.dm-divider:focus-visible::after { background: var(--accent); }
.dm-divider:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* While dragging, the iframe must not eat the pointer and nothing should
   select. Pointer capture already routes the events here; this stops the frame
   reacting to a pointer that is only passing over it. */
.dm-split.is-dragging { user-select: none; }
.dm-split.is-dragging .studio-frame { pointer-events: none; }
.dm-panel {
  min-width: 0;
  overflow: auto;
  background: var(--panel);
}
.dm-split > .studio-frame { border: 0; width: 100%; height: 100%; min-height: 0; }

.dp { display: flex; flex-direction: column; }

/* ── the panel's own header ── */
.dp-head {
  padding: 16px 18px 14px;
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 3;
  background: var(--panel);
}
.dp-card {
  font-size: 16.5px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--text); line-height: 1.4;
}
.dp-counts { margin-top: 6px; font-size: 14.5px; color: var(--text-2); }

.dp-error {
  margin: 12px 18px 0;
  font-size: 15px; color: #E88C8C; line-height: 1.6;
  background: rgba(214, 69, 69, 0.09);
  border: 1px solid rgba(214, 69, 69, 0.28);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
}

/* ── the bundle bar ── */
.dp-selbar {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 18px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
}
.dp-selcount { font-size: 15px; color: var(--text-2); }
.dp-selcount strong { color: var(--text); font-weight: 700; }
.dp-selactions { display: flex; gap: 7px; margin-left: auto; }

/* ── the bundle compose block — inline, never a modal ── */
.dp-bundle {
  margin: 14px 18px;
  border: 1px solid var(--accent-soft);
  border-color: rgba(201, 168, 122, 0.32);
  border-radius: var(--radius-sm);
  background: rgba(201, 168, 122, 0.045);
  padding: 13px;
  display: flex; flex-direction: column; gap: 10px;
}
.dp-bundle-head {
  font-size: 13.5px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--accent);
}
/* Every student who will receive this text, readable. The faculty confirms by
   reading, not by trusting a count. */
.dp-bundle-list {
  display: flex; flex-direction: column;
  max-height: 190px; overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--page);
}
.dp-bundle-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 26px;
  gap: 2px 8px;
  padding: 9px 10px;
  border-bottom: 1px solid var(--line-row);
}
.dp-bundle-item:last-child { border-bottom: 0; }
.dp-bundle-who { font-size: 14.5px; font-weight: 700; color: var(--text); }
.dp-bundle-q {
  grid-column: 1;
  font-size: 14.5px; color: var(--text-2); line-height: 1.5;
  overflow-wrap: anywhere;
}
.dp-x {
  grid-row: 1 / 3; grid-column: 2;
  align-self: start;
  border: 0; background: transparent; color: var(--text-3);
  width: 24px; height: 24px; border-radius: 6px;
  display: grid; place-items: center; cursor: pointer;
}
.dp-x:hover { color: var(--danger); background: rgba(214, 69, 69, 0.1); }

/* ── the similarity suggestion. A proposal that ticks boxes, and nothing else. ── */
.dp-hint { padding: 10px 18px 0; }
.dp-hint-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 14px; color: var(--tan);
  background: rgba(201, 178, 143, 0.1);
  border-radius: 7px; padding: 6px 10px;
}
.dp-hint-row .link-btn { margin-left: auto; font-size: 14px; }

/* ── one doubt ── */
.dp-list { display: flex; flex-direction: column; }
.dp-doubt {
  padding: 14px 18px;
  border-bottom: 1px solid var(--line-row);
  display: flex; flex-direction: column; gap: 9px;
}
.dp-doubt.is-picked { background: rgba(201, 168, 122, 0.055); box-shadow: inset 2px 0 0 var(--accent); }
.dp-doubt-head { display: flex; align-items: center; gap: 9px; }
.dp-who {
  font-size: 15.5px; font-weight: 700; letter-spacing: -0.005em;
  color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dp-age { margin-left: auto; font-size: 14px; color: var(--text-3); flex: none; }
.dp-doubt-head .badge { flex: none; }
.dp-q {
  font-size: 15.5px; line-height: 1.6; color: var(--text-val);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.dp-failed {
  font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--danger);
}
.dp-answered { border-left: 2px solid var(--green); padding-left: 10px; }
.dp-answered-k {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-3);
}
.dp-answered-t {
  font-size: 15px; color: var(--text-val); line-height: 1.6; margin-top: 3px;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.dp-draft {
  width: 100%; resize: vertical; min-height: 62px;
  line-height: 1.55; padding: 9px 11px; font-size: 15.5px;
}
.dp-actions { display: flex; gap: 7px; flex-wrap: wrap; }
.dp-empty { padding: 26px 18px; font-size: 15.5px; color: var(--text-3); text-align: center; }

/* ── narrow: the panel goes above the card rather than squeezing it ── */
/* Stacked: the panel sits above the card, so there is no vertical divider to
   drag. It is hidden rather than removed so the inline width the drag wrote
   does not have to be unwound — `grid-template-columns` is overridden here and
   the inline style only applies again when the query stops matching. */
@media (max-width: 1080px) {
  .dm-split {
    grid-template-columns: minmax(0, 1fr) !important;
    grid-template-rows: minmax(240px, 42vh) minmax(0, 1fr);
  }
  .dm-divider { display: none; }
  .dm-panel { border-bottom: 1px solid var(--line); }
}
[data-layout="mobile"] .dm-split {
  grid-template-columns: minmax(0, 1fr) !important;
  grid-template-rows: minmax(240px, 46vh) minmax(0, 1fr);
}
[data-layout="mobile"] .dm-divider { display: none; }
[data-layout="mobile"] .dm-panel { border-bottom: 1px solid var(--line); }
[data-layout="mobile"] .dp-selactions { width: 100%; margin-left: 0; }
[data-layout="mobile"] .dp-actions .btn { flex: 1; min-height: 42px; }

/* ── Recall Score (Course Pack → Content) ────────────────────────────────────
   Additive only. Every rule below either reuses an existing token or styles a
   node that did not exist before, so nothing already on the page moves. */
.rs-head { display: inline-flex; align-items: center; gap: 6px; }
/* Tabular figures already come from .tbl-num; this only adds the weight that
   makes the score read as the row's headline rather than a third count. */
.rs-score { font-weight: 700; color: var(--text); }
.rs-cell { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.rs-chev { font-size: 11px; line-height: 1; color: var(--text-3); transition: transform 0.12s ease; }
.rs-chev.is-up { transform: rotate(180deg); }
button.tbl-row:hover .rs-chev { color: var(--text); }

/* Child card rows. Indented by the same 16px the table already pads with, so
   the nesting lines up with the row above instead of inventing a scale. */
.tbl-row.is-child { padding-left: 32px; background: rgba(237, 230, 217, 0.014); }
.tbl-row.is-child .tbl-name { font-weight: 550; }
.rs-dist { font-variant-numeric: tabular-nums; }
.rs-retired {
  margin-left: 8px; padding: 1px 6px; border-radius: 4px;
  border: 1px solid var(--line); color: var(--text-3);
  font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
}
[data-layout="mobile"] .tbl-row.is-child { padding-left: 26px; }
/* The Subject figure, on the section heading line. Not a hero tile — Recall
   Score is intelligence about the content below it, not a fourth headline. */
.rs-headline { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
.rs-headline-value { font-size: 30.5px; font-weight: 700; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }
.rs-headline-sub { font-size: 15px; color: var(--text-3); }

/* Recall Score — the em-dash states and the explanatory note. The column and
   the hierarchy are always rendered; only the numbers can be missing. */
.rs-none { opacity: 0.55; }
.rs-headline-value.is-none { color: var(--text-3); }
.rs-cell-v { display: flex; justify-content: flex-end; }
.rs-note {
  display: flex; align-items: flex-start; gap: 9px;
  margin: 0 0 12px; padding: 11px 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel);
  font-size: 15px; line-height: 1.55; color: var(--text-3);
}
.rs-note strong { color: var(--text-2); font-weight: 700; }
.rs-note .btn { margin-left: auto; flex: none; }

/* ── Recall Score workspace (Analytics) ─────────────────────────────────────
   Built on the .dbt-* system (table wrap, sticky head, docked panel, search),
   because Doubts already translated Study Filters into this design language
   and two translations would drift. Additive only. */

/* The headline band. The score is the page's dominant element; everything
   else in the band is deliberately quiet. */
.rsx-band {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap;
  padding: 20px var(--s3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel);
  margin-bottom: var(--s2);
}
.rsx-band-main { display: flex; align-items: center; gap: 18px; min-width: 0; }
.rsx-big {
  font-size: 51px; font-weight: 750; letter-spacing: -0.03em;
  color: var(--text-hi); font-variant-numeric: tabular-nums; line-height: 1;
}
.rsx-big.is-none { color: var(--text-3); }
.rsx-band-label { font-size: 15.5px; font-weight: 650; color: var(--text); }
.rsx-band-context { font-size: 15px; color: var(--text-3); margin-top: 4px; }
.rsx-band-flag { color: var(--tan); }
.rsx-band-mix { display: flex; flex-direction: column; gap: 7px; align-items: flex-end; }

/* The rating mix: four tints of ONE ink — Again darkest through Easy
   lightest — so "how much dark is in the bar" is the reading, not a traffic
   light. Monochrome is a product rule (decision log 2026-08-20). */
.rsx-mix {
  display: inline-flex; width: 92px; height: 6px;
  border-radius: 3px; overflow: hidden;
  background: rgba(237, 230, 217, 0.06);
  flex: none;
}
.rsx-mix.is-wide { width: 200px; height: 7px; }
.rsx-mix.is-empty { visibility: hidden; }
.rsx-mix-seg { height: 100%; }
.rsx-mix-seg.is-again { background: rgba(237, 230, 217, 0.82); }
.rsx-mix-seg.is-hard  { background: rgba(237, 230, 217, 0.52); }
.rsx-mix-seg.is-good  { background: rgba(237, 230, 217, 0.26); }
.rsx-mix-seg.is-easy  { background: rgba(237, 230, 217, 0.11); }
.rsx-mixline { font-size: 14px; color: var(--text-3); white-space: nowrap; }

/* Summary strip: same voice as .dbt-summary, inline in the controls row. */
.rsx-summary {
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
  font-size: 15.5px; color: var(--text-2); min-height: 34px;
}
.rsx-sum-strong { font-weight: 700; color: var(--text); }

/* Table rows. .dbt-tr supplies the grid/border; these add the two-level
   hierarchy: chapter rows carry the weight, card rows sit indented on a
   barely-tinted ground so nesting reads without another border. */
.rsx-table { min-width: 0; }
.rsx-table-wrap { max-height: calc(100dvh - 260px); }
button.rsx-row {
  width: 100%; background: transparent; border-left: 0; border-right: 0; border-top: 0;
  font-family: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: background 0.12s ease;
}
button.rsx-row:hover { background: rgba(237, 230, 217, 0.03); }
.rsx-row { padding-top: 10px; padding-bottom: 10px; }
.rsx-chrow.is-static { cursor: default; }
.rsx-namecell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.rsx-nametext { min-width: 0; }
.rsx-chev-slot { width: 9px; flex: none; }
.rsx-namecell .rs-chev { flex: none; }
.rsx-namecell.is-card { padding-left: 27px; }
.rsx-cardrow { background: rgba(237, 230, 217, 0.014); }
.rsx-cardrow .tbl-name { font-weight: 550; font-size: 15.5px; white-space: normal; }
.rsx-cardrow.is-selected {
  background: rgba(237, 230, 217, 0.05);
  box-shadow: inset 2px 0 0 var(--accent);
}
.rsx-num { text-align: right; }
.rsx-mixcell { display: flex; justify-content: flex-end; }
button.rsx-more {
  width: 100%; border-left: 0; border-right: 0; border-top: 0; background: rgba(237, 230, 217, 0.014);
  font-family: inherit; font-size: 15px; color: var(--text-3);
  padding: 9px var(--s3) 9px 53px; text-align: left; cursor: pointer;
}
button.rsx-more:hover { color: var(--text); }

/* Sortable headers — Study Filters' pattern: the header IS the control. */
button.rsx-th {
  background: transparent; border: 0; font-family: inherit;
  text-align: left; cursor: pointer; padding: 0;
}
button.rsx-th.is-num { text-align: right; }
button.rsx-th:hover { color: var(--text); }
button.rsx-th.is-on { color: var(--accent); }

/* Panel additions. */
.rsx-back { color: var(--text-2); }
.rsx-panel-score { display: flex; align-items: baseline; gap: 10px; }
.rsx-panel-big {
  font-size: 35px; font-weight: 750; letter-spacing: -0.02em;
  color: var(--text-hi); font-variant-numeric: tabular-nums;
}
.rsx-panel-big.is-none { color: var(--text-3); }
.rsx-panel-note { font-size: 15px; color: var(--text-3); }
.rsx-versions { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.rsx-version-row {
  display: grid; grid-template-columns: 64px minmax(0,1fr) 56px;
  gap: 10px; align-items: center;
  padding: 8px 11px; font-size: 15px;
  border-bottom: 1px solid var(--line-row);
}
.rsx-version-row:last-child { border-bottom: 0; }
.rsx-version-row .tbl-num:last-child { text-align: right; }

[data-layout="mobile"] .rsx-band-main { gap: 14px; }
[data-layout="mobile"] .rsx-big { font-size: 39.5px; }
[data-layout="mobile"] .rsx-band-mix { align-items: flex-start; }
[data-layout="mobile"] .rsx-namecell.is-card { padding-left: 19px; }
[data-layout="mobile"] .rsx-table-wrap { max-height: 62dvh; }

/* ── Recall Score v2: the compact Analytics overview + the workspace ─────────
   The overview is one clickable card; the full experience lives at
   #/analytics/recall on the .dbt-* workspace system. */
.rsx-overview {
  display: block; width: 100%; text-align: left;
  padding: 20px var(--s3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: inherit; font-family: inherit;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.rsx-overview:hover { border-color: var(--accent); background: rgba(237, 230, 217, 0.02); }
.rsx-overview-head {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 15.5px; font-weight: 650; color: var(--text);
}
.rsx-overview .rsx-big { margin-top: 8px; }
.rsx-overview-sub { font-size: 15px; color: var(--text-3); margin-top: 6px; min-height: 16px; }
.rsx-overview-go {
  margin-top: 12px;
  font-size: 15px; font-weight: 650; color: var(--accent);
}
.rsx-overview-arrow { display: inline-block; transition: transform 0.12s ease; }
.rsx-overview:hover .rsx-overview-arrow { transform: translateX(3px); }

/* Workspace-only pieces. */
.rsx-bandbar { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.rsx-band-chip { border: 1px solid var(--line); border-radius: 999px; padding: 5px 13px; }
.rsx-band-chip.is-on { border-color: var(--accent); }
.rsx-chip-n { margin-left: 6px; color: var(--text-3); font-weight: 600; }
.rsx-sort { max-width: 170px; }
.rsx-strip { margin-bottom: var(--s2); }
.rsx-kicker {
  font-size: 12px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3); margin-bottom: 2px;
}
.rsx-subjrow { background: rgba(237, 230, 217, 0.03); }
.rsx-subjrow .tbl-name { font-size: 17px; font-weight: 750; white-space: normal; }
.rsx-subject-score { font-size: 19px; font-weight: 750; }
.rsx-namecell.is-chapter { padding-left: 14px; }
.rsx-namecell.is-card { padding-left: 41px; }

/* The flashcard faces in the preview panel: the note's own sanitised markup,
   in the same quote frame the Doubts panel uses for student text. */
.rsx-card { display: flex; flex-direction: column; gap: 6px; }
.rsx-card-side {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-3);
}
.rsx-card-face {
  font-size: 16px; line-height: 1.6; color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 13px;
  overflow-wrap: anywhere;
}
.rsx-card-face u { text-decoration-color: var(--accent); text-underline-offset: 3px; }
.rsx-card-face b, .rsx-card-face strong { color: var(--text-hi); }
.rsx-card-face ul, .rsx-card-face ol { margin: 6px 0 6px 18px; }
.rsx-card-face p { margin: 0 0 6px; }

[data-layout="mobile"] .rsx-namecell.is-card { padding-left: 27px; }
[data-layout="mobile"] .rsx-bandbar { width: 100%; }
[data-layout="mobile"] .rsx-sort { max-width: none; flex: 1; }

/* ── Recall Score v3 — the flat Study-Filters-shaped workspace ───────────────
   The rsx2-* system. Geometry lifted from the audited student workspace:
   one shared gutter (here 18px against the panel edge — the console's own
   --pad — where the student app uses 26 against the viewport), 12px rows,
   10.5px/700 uppercase sortable headers, sticky header on the panel colour,
   warm result strip, focus bar `inset 2.5px 0 0 var(--accent)`. */

.rsx2-controls { row-gap: var(--s1); }
.rsx2-scope { max-width: 240px; }
.rsx2-sort { max-width: 160px; }

/* The result-summary strip: the warm band with the matching count up front
   and one pill per chapter, dimmed at zero, never removed — the strip must
   not reflow as filters change. */
.rsx2-strip {
  display: flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgba(237, 230, 217, 0.028);
  padding: 8px 14px;
  margin-bottom: var(--s2);
}
.rsx2-strip-inner {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  min-width: 0;
}
.rsx2-total { font-size: 15.5px; font-weight: 800; color: var(--text); white-space: nowrap; }
.rsx2-substat { font-size: 14.5px; font-weight: 600; color: var(--text-2); white-space: nowrap; }
.rsx2-vr { width: 1px; height: 16px; background: var(--line-2); margin: 0 3px; flex: none; }
.rsx2-pill {
  border: 1px solid var(--line); background: var(--panel);
  color: var(--text-2);
  font-family: inherit; font-size: 14.5px; font-weight: 600;
  border-radius: 999px; padding: 4px 11px;
  cursor: pointer; white-space: nowrap;
  transition: border-color 0.12s ease, color 0.12s ease, opacity 0.12s ease;
}
.rsx2-pill:hover { border-color: var(--accent); color: var(--text); }
.rsx2-pill.is-on { border-color: var(--accent); color: var(--accent); }

/* The two-region workspace. Flex, not grid: the panel carries an explicit
   width (the ONE clamp, applied from JS) and the list yields the rest,
   scrolling INSIDE itself — sideways too, past its 760px floor. */
.rsx2-work {
  display: flex; align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
  /* Page header + note + controls + strip above; the workspace gets the rest. */
  height: calc(100dvh - 232px);
  min-height: 320px;
}
.rsx2-listwrap { flex: 1; min-width: 0; overflow: auto; }
.rsx2-list { min-width: 760px; }

.rsx2-tr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 210px 76px 88px 110px;
  gap: 14px; align-items: center;
  padding: 0 var(--pad);
  border-bottom: 1px solid var(--line-row);
}
.rsx2-thead {
  position: sticky; top: 0; z-index: 2;
  background: var(--panel);
  border-bottom: 1px solid var(--line-2);
}
.rsx2-th {
  border: 0; background: transparent;
  font-family: inherit; text-align: left;
  padding: 10px 0;
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-2);
  cursor: pointer; user-select: none;
  transition: color 0.12s ease;
}
.rsx2-th:hover, .rsx2-th.is-active { color: var(--text); }
.rsx2-th.is-num { text-align: right; }
div.rsx2-th { cursor: default; }

.rsx2-row {
  width: 100%;
  border-left: 0; border-right: 0; border-top: 0;
  background: transparent; color: inherit;
  font-family: inherit; text-align: left;
  cursor: pointer;
  min-height: 44px;
  transition: background 0.12s ease;
}
.rsx2-row:hover { background: rgba(237, 230, 217, 0.035); }
.rsx2-row.is-focused {
  background: var(--panel-2);
  box-shadow: inset 2.5px 0 0 var(--accent);
}
.rsx2-row.is-static { cursor: default; }
.rsx2-cell { min-width: 0; font-size: 16px; }
.rsx2-cardcell { display: flex; align-items: center; gap: 8px; min-width: 0; }
.rsx2-preview {
  font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rsx2-chapter {
  font-size: 14.5px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rsx2-num { text-align: right; font-variant-numeric: tabular-nums; }
.rsx2-score { font-weight: 700; color: var(--text-val); }
.rsx2-mixcell { display: flex; justify-content: flex-end; }

.rsx2-overflow {
  display: block; width: 100%;
  border: 0; background: transparent;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--text-2); text-align: center;
  padding: 13px var(--pad);
  cursor: pointer;
}
.rsx2-overflow:hover { color: var(--text); }

/* The docked inspector. Width is set from JS (the one clamp); the divider is
   absolutely positioned on its left edge with an always-visible grip. */
.rsx2-panel {
  position: relative; flex: none;
  border-left: 1px solid var(--line);
  background: var(--panel);
  overflow-y: auto; overflow-x: hidden;
}
.rsx2-divider {
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 11px; z-index: 6;
  cursor: col-resize;
  display: flex; align-items: center; justify-content: center;
}
.rsx2-divider:hover { background: rgba(237, 230, 217, 0.04); }
.rsx2-grip { width: 3px; height: 38px; border-radius: 3px; background: var(--line-2); }
.rsx2-panel-x {
  margin-left: auto;
  border: 0; background: transparent; color: var(--text-2);
  width: 28px; height: 28px; border-radius: 8px;
  font-family: inherit; font-size: 15.5px; cursor: pointer;
}
.rsx2-panel-x:hover { background: var(--panel-2); color: var(--text); }

/* Mobile: the workspace stacks — list, then inspector — and the divider goes
   away; internal horizontal scrolling keeps the columns legible, the same
   choice the student workspace makes instead of crushing them. */
[data-layout="mobile"] .rsx2-work { flex-direction: column; height: auto; }
[data-layout="mobile"] .rsx2-listwrap { max-height: 52dvh; }
[data-layout="mobile"] .rsx2-list { min-width: 0; }
[data-layout="mobile"] .rsx2-tr { grid-template-columns: minmax(0, 1fr) 64px 70px; gap: 10px; }
[data-layout="mobile"] .rsx2-panel { width: auto !important; border-left: 0; border-top: 1px solid var(--line); }
[data-layout="mobile"] .rsx2-scope, [data-layout="mobile"] .rsx2-sort { max-width: none; flex: 1; }

/* ══ Recall Score workspace (rsw-*) — FULL BLEED ═══════════════════════════
   Every other faculty screen is a document: `.view` gives it 34px of padding
   and a 1180px ceiling. A workspace is not a document. The student app's
   Create Decks and Study Filters are `position:fixed; inset:0` — edge to edge
   is what makes them read as an application rather than a card on a page.
   This class removes the padding and the ceiling and claims the full height of
   the content area; the body row then fills whatever the header and controls
   leave and scrolls INSIDE itself. The page never scrolls. */
.view.is-recallws {
  max-width: none;
  padding: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Nothing may grow the page: the body row is the only scroller, and without
     this a tall list pushes the workspace past the viewport and the window
     itself starts scrolling — which is exactly what a workspace must not do. */
  overflow: hidden;
  animation: none;
}

/* ── header row ── */
.rsw-head {
  display: flex; align-items: center; gap: 16px;
  padding: 16px 26px 14px;
  border-bottom: 1px solid var(--line);
  flex: none;
}
.rsw-back {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line); background: var(--panel-2);
  color: var(--text-2);
  font-family: inherit; font-size: 15px; font-weight: 650;
  height: 32px; padding: 0 12px 0 10px; border-radius: var(--radius-sm);
  cursor: pointer; flex: none; white-space: nowrap;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.rsw-back:hover { color: var(--text); border-color: var(--accent); }
.rsw-titlewrap { flex: 1; min-width: 0; }
.rsw-title {
  display: flex; align-items: center; gap: 7px;
  font-size: 20.5px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--text-hi); margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rsw-sub { font-size: 15px; color: var(--text-3); margin-top: 2px; }
.rsw-headscore { text-align: right; flex: none; }
.rsw-headscore-v {
  font-size: 26px; font-weight: 800; letter-spacing: -0.02em;
  color: var(--text-hi); font-variant-numeric: tabular-nums; line-height: 1.1;
}
.rsw-headscore-v.is-none { color: var(--text-3); }
.rsw-headscore-s { font-size: 14px; color: var(--text-3); margin-top: 3px; }

.rsw-note {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 10px 26px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  font-size: 15px; line-height: 1.55; color: var(--text-3);
  flex: none;
}
.rsw-note strong { color: var(--text-2); font-weight: 700; }

/* ── controls row ── */
.rsw-controls {
  display: flex; align-items: center; gap: 14px;
  padding: 11px 26px;
  border-bottom: 1px solid var(--line);
  flex: none; flex-wrap: wrap;
}
.rsw-chips { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.rsw-chip {
  border: 1px solid var(--line); background: var(--panel);
  color: var(--text-2);
  font-family: inherit; font-size: 14.5px; font-weight: 650;
  border-radius: 999px; padding: 5px 13px;
  cursor: pointer; white-space: nowrap;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.rsw-chip:hover { color: var(--text); border-color: var(--accent); }
.rsw-chip.is-on { color: var(--accent); border-color: var(--accent); }
.rsw-controls-right { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.rsw-sort { max-width: 160px; }
.rsw-search { min-height: 34px; width: 280px; max-width: 100%; font-size: 15.5px; padding: 0 12px; }

/* ── the body: list left, inspector right, both scrolling internally ── */
.rsw-body { flex: 1; min-height: 0; display: flex; align-items: stretch; }
.rsw-listwrap { flex: 1; min-width: 0; overflow: auto; }
.rsw-list { min-width: 620px; }

.rsw-tr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 104px 92px 112px;
  gap: 16px; align-items: center;
  width: 100%;
  padding: 0 26px;
  border: 0; border-bottom: 1px solid var(--line-row);
  background: transparent; color: inherit;
  font-family: inherit; font-size: 16px; text-align: left;
}
.rsw-thead {
  position: sticky; top: 0; z-index: 2;
  background: var(--page);
  border-bottom: 1px solid var(--line-2);
}
/* Same missing-clamp bug as `.rsw-nametext` (this file, audited 2026-09-10):
   a grid item defaults to `min-width: auto`, sized to fit its content, so a
   header label overflowed its own column into the next one — "STUDENT" and
   "EMAIL" rendering as one run-together "STUDENEMAIL" at 375px. Headers are
   short, known strings (never a faculty's own data), so ellipsis is a purely
   defensive floor, not something expected to fire in the ordinary case. */
.rsw-th {
  padding: 10px 0;
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-2);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rsw-th.is-num { text-align: right; }

.rsw-row { min-height: 48px; cursor: pointer; transition: background 0.12s ease; }
.rsw-row:hover { background: var(--panel); }
.rsw-row:hover .rsw-go { opacity: 1; transform: translateX(2px); }
.rsw-row.is-focused { background: var(--panel-2); box-shadow: inset 2.5px 0 0 var(--accent); }
.rsw-row.is-static { cursor: default; }
.rsw-cell { min-width: 0; }
.rsw-name { display: flex; align-items: center; gap: 10px; min-width: 0; }
/* AUDITED 2026-09-10, mobile Leaderboard: `overflow`/`text-overflow` on a bare
   inline element (the default here) is a no-op — inline boxes size to their
   own content and ignore a container's width, so the ellipsis rule never
   fired and a long email simply rendered past its 56px cell, overlapping the
   numbers beside it. `.rsw-name`'s flex wrapper happened to blockify its own
   nametext span and never showed the bug; the bare `.rsw-cell` around the
   email column has no such wrapper. `display: block` is the actual fix —
   harmless where a flex parent already forced it, load-bearing where one
   does not. */
.rsw-nametext {
  display: block;
  font-weight: 550; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rsw-meta { font-size: 14px; color: var(--text-3); white-space: nowrap; flex: none; }
.rsw-go {
  margin-left: auto; flex: none;
  color: var(--text-3); font-size: 18px;
  opacity: 0; transition: opacity 0.12s ease, transform 0.12s ease;
}
.rsw-num { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-val); }
.rsw-score { font-size: 18px; font-weight: 750; color: var(--text-hi); }
.rsw-score.dimmer { font-size: 16px; font-weight: 600; }
.rsw-mixcell { display: flex; justify-content: flex-end; }
.rsw-overflow {
  display: block; width: 100%; border: 0; background: transparent;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--text-2); text-align: center; padding: 14px 26px; cursor: pointer;
}
.rsw-overflow:hover { color: var(--text); }
.rsw-empty { padding: 56px 26px; text-align: center; color: var(--text-3); font-size: 16px; line-height: 1.65; }
.rsw-empty strong { display: block; color: var(--text); font-weight: 700; font-size: 17px; margin-bottom: 6px; }

/* ── the inspector ── */
.rsw-panel {
  position: relative; flex: none;
  border-left: 1px solid var(--line);
  background: var(--panel);
  overflow-y: auto; overflow-x: hidden;
}
.rsw-divider {
  position: absolute; left: 0; top: 0; bottom: 0; width: 11px; z-index: 6;
  cursor: col-resize; display: flex; align-items: center; justify-content: center;
}
.rsw-divider:hover { background: rgba(237, 230, 217, 0.04); }
.rsw-grip { width: 3px; height: 38px; border-radius: 3px; background: var(--line-2); }
.rsw-panel-empty {
  padding: 60px 30px; text-align: center;
  color: var(--text-3); font-size: 15.5px; line-height: 1.7;
}
.rsw-panel-emptymark {
  width: 46px; height: 46px; margin: 0 auto 16px;
  border-radius: 50%; display: grid; place-items: center;
  background: var(--panel-2); color: var(--text-3);
}
.rsw-panel-body { padding: 18px 22px 30px; }
.rsw-panel-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.rsw-panel-title {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3);
}
.rsw-panel-x {
  margin-left: auto; border: 0; background: transparent; color: var(--text-2);
  width: 28px; height: 28px; border-radius: 8px;
  font-family: inherit; font-size: 15.5px; cursor: pointer;
}
.rsw-panel-x:hover { background: var(--panel-2); color: var(--text); }

/* THE CARD. Given the room — generous type, real line height, media allowed
   its natural size up to the panel width. The analytics sit below it. */
.rsw-card { display: flex; flex-direction: column; gap: 7px; }
.rsw-face-label {
  font-size: 12px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3); margin-top: 4px;
}
.rsw-face {
  font-size: 18px; line-height: 1.65; color: var(--text-hi);
  background: var(--page);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px 17px;
  overflow-wrap: anywhere;
}
.rsw-face img, .rsw-face svg, .rsw-face video { max-width: 100%; height: auto; border-radius: var(--radius-sm); margin: 6px 0; }
.rsw-face b, .rsw-face strong { color: #F3ECDF; font-weight: 700; }
.rsw-face p { margin: 0 0 8px; }
.rsw-face p:last-child { margin-bottom: 0; }
.rsw-face ul, .rsw-face ol { margin: 8px 0 8px 20px; }
.rsw-face li { margin-bottom: 4px; }
.rsw-face hr { border: 0; border-top: 1px solid var(--line); margin: 12px 0; }
.rsw-face table { border-collapse: collapse; width: 100%; font-size: 16px; }
.rsw-face td, .rsw-face th { border: 1px solid var(--line); padding: 6px 8px; }
.cp-cloze { text-decoration-color: var(--accent); text-underline-offset: 3px; color: var(--accent-hi); }

.rsw-panel-div { height: 1px; background: var(--line); margin: 22px 0 18px; }
.rsw-panel-score { margin-bottom: 16px; }
.rsw-panel-big {
  font-size: 39.5px; font-weight: 800; letter-spacing: -0.025em;
  color: var(--text-hi); font-variant-numeric: tabular-nums; line-height: 1;
}
.rsw-panel-big.is-none { color: var(--text-3); }
.rsw-panel-label {
  font-size: 13.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-2); margin-top: 7px;
}
.rsw-panel-sub { font-size: 15px; color: var(--text-3); margin-top: 3px; }
.rsw-ratings { display: flex; flex-direction: column; gap: 1px; }
.rsw-rating {
  display: grid; grid-template-columns: minmax(0, 1fr) 60px 48px;
  align-items: center; gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-row);
  font-size: 15.5px;
}
.rsw-rating-k { color: var(--text-2); font-weight: 600; }
.rsw-rating-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-val); font-weight: 650; }
.rsw-rating-p { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-3); }
.rsw-versions { margin-top: 18px; }
.rsw-versions-k {
  font-size: 13px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-3); margin-bottom: 7px;
}
.rsw-version-row {
  display: grid; grid-template-columns: 56px minmax(0, 1fr) 52px;
  gap: 10px; padding: 6px 0; font-size: 15px;
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--line-row);
}
.rsw-version-row span:last-child { text-align: right; color: var(--text-val); font-weight: 650; }
.rsw-panel-actions { margin-top: 20px; display: flex; gap: 8px; }
.rsw-panel-note { font-size: 14.5px; color: var(--text-3); line-height: 1.6; margin-top: 14px; }

/* ── mobile: the workspace stacks, the divider goes away ── */
[data-layout="mobile"] .view.is-recallws { height: auto; min-height: 0; }
/* ── THE HEADER THAT WAS CRUSHING SUBJECT NAMES TO ~13 CHARACTERS ──────────
   Audited at 375px: back button (~109px) + score block (~87px) left barely
   128px for `.rsw-titlewrap`, because that element carries `min-width: 0`
   (styles.css, desktop rule) so it shrinks instead of wrapping to a new line.
   `flex-wrap: wrap` alone could not fix this: nothing forced a break.

   Stacked into three rows instead: back button alone, then the title, full
   width and free to wrap across two lines when the Subject's name needs it,
   then the score, left-aligned under the subtitle rather than pinned to the
   right — a position this narrow could never actually support. */
[data-layout="mobile"] .rsw-head { padding: 14px 16px 12px; flex-wrap: wrap; gap: 6px 10px; }
[data-layout="mobile"] .rsw-titlewrap { flex: 1 1 100%; }
[data-layout="mobile"] .rsw-title {
  white-space: normal; overflow: visible; text-overflow: clip;
  font-size: 19px; line-height: 1.25;
}
[data-layout="mobile"] .rsw-headscore {
  flex: 1 1 100%; text-align: left; margin-top: 2px;
  display: flex; align-items: baseline; gap: 8px;
}
[data-layout="mobile"] .rsw-headscore-s { margin-top: 0; }
/* The Leaderboard variant carries a FIXED short title ("Student Leaderboard")
   through a different element (.lb-title, not .rsw-titlewrap) and was never
   part of this crowding — excluded so its own compact single-row header,
   already correct, is not restacked into something it does not need. */
[data-layout="mobile"] .rsw-head.lb-bar .rsw-titlewrap { flex: 0 1 auto; }
[data-layout="mobile"] .rsw-head.lb-bar .rsw-title { font-size: 19px; white-space: nowrap; }
[data-layout="mobile"] .rsw-controls,
[data-layout="mobile"] .rsw-note { padding-left: 16px; padding-right: 16px; }
[data-layout="mobile"] .rsw-controls-right { width: 100%; margin-left: 0; }
[data-layout="mobile"] .rsw-search { flex: 1; width: auto; }
[data-layout="mobile"] .rsw-body { flex-direction: column; }
[data-layout="mobile"] .rsw-listwrap { max-height: 56dvh; }
[data-layout="mobile"] .rsw-list { min-width: 0; }
[data-layout="mobile"] .rsw-tr { grid-template-columns: minmax(0, 1fr) 68px 62px; gap: 10px; padding: 0 16px; }
[data-layout="mobile"] .rsw-panel { width: auto !important; border-left: 0; border-top: 1px solid var(--line); }


/* ══ Student Leaderboard (lb-*) ════════════════════════════════════════════
   The workspace reuses the Recall Score system wholesale — .view.is-recallws,
   .rsw-head, .rsw-controls, .rsw-body, .rsw-tr, .rsw-thead, .rsw-row — because
   a faculty who has used one of these workspaces has used all of them. What is
   below is only what genuinely differs: a four-column grid instead of the
   name/score/recalls/mix one, an email column, and the sort caret on the
   Study Filters-style header buttons. Nothing here restyles an .rsw-* rule.

   THE FEATURE CARD gets its own host, `.lb-host`, so views/analytics.js has a
   stable node to mount into — see `.an-cols.is-two` below for how it and the
   Recall Score card are actually laid out. */

/* # · Student · Email · Total recalls · Recall score.
   The two text columns share the flexible space rather than one taking it all:
   a long email is as likely to be the thing that truncates as a long name, and
   a fixed email column would truncate every address at the same point
   regardless of how much room the names actually left.

   The rank column is fixed and narrow — it holds at most three digits and is
   the one column whose content cannot grow with the data. */
.rsw-tr.lb-tr { grid-template-columns: 44px minmax(0, 1.1fr) minmax(0, 1.4fr) 116px 116px; }
.lb-list { min-width: 764px; }
.lb-email { font-weight: 450; color: var(--text-2); }

/* Position in the current table, not a score — so it is deliberately quieter
   than the two numeric columns it sits opposite. Tabular figures keep the
   column edge straight as it crosses from 9 to 10 to 100. */
.lb-rank { font-variant-numeric: tabular-nums; font-size: 15px; }

/* The caret shows the direction the ACTIVE column is sorted, and — dimmed —
   the direction a first click on an inactive one would choose. A header whose
   caret only appears once it is active makes every other header look inert. */
.lb-sort { font-size: 10.5px; opacity: 0; transition: opacity 0.12s ease; }
button.rsx-th:hover .lb-sort { opacity: 0.45; }
.lb-sort.is-on { opacity: 1; }

/* Rows are not clickable here: there is no student detail page, by design.
   The hover tint stays — it is what keeps the eye on one row across four
   columns — but the pointer must not promise a destination. */
.rsw-row.lb-tr { cursor: default; }

/* ── the single top bar ──
   Back, title+tip, Subject, search and the count on ONE row. Built on
   .rsw-head, so the padding, the border-bottom and the mobile wrap all come
   from the workspace system rather than from here; .lb-bar only adds what a
   one-row layout genuinely needs. */
.rsw-head.lb-bar { gap: 14px; padding: 11px 26px; flex-wrap: wrap; }

/* The title sizes to its text instead of claiming the row, so the right-hand
   group's margin-left:auto is what positions everything — one rule deciding
   the layout rather than two pushing against each other. min-width lets a long
   title ellipsis instead of forcing the controls off the bar. */
.rsw-title.lb-title { flex: 0 1 auto; min-width: 0; font-size: 19px; }

/* Baseline-aligned so the number and its word read as one phrase, and pushed
   off the search by a little more than the group's own gap — it is a reading
   of the table, not another control. */
.lb-count {
  display: flex; align-items: baseline; gap: 5px;
  flex: none; white-space: nowrap; margin-left: 4px;
}
.lb-count-n {
  font-size: 18px; font-weight: 800; color: var(--text-hi);
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.lb-count-w { font-size: 14px; color: var(--text-3); }

/* The group must be able to SHRINK, not just wrap. .rsw-search is a fixed
   280px and .lb-count is flex:none, so between the desktop rail (226px) and a
   narrow window the row wrapped and then ran off the right edge — the page does
   not scroll horizontally, so the count was simply clipped. Basis 280 with
   shrink allowed keeps the field at its intended width wherever there is room
   and gives it back when there is not. Scoped to .lb-bar: the Recall Score
   workspace's own control row is untouched. */
.lb-bar .rsw-controls-right { flex-wrap: nowrap; min-width: 0; max-width: 100%; }
.lb-bar .rsw-search { flex: 0 1 280px; min-width: 150px; width: auto; }

/* Export Excel sits between the search field and the count, taking exactly the
   width its icon and label need — `flex: none` so the SEARCH FIELD is what
   narrows as the bar tightens, never this button losing its label mid-word. */
.lb-export { flex: none; }

/* At mobile .rsw-head already wraps and .rsw-controls-right already goes full
   width, so the group drops to its own line with the search flexing into it.
   The count moves to the end of that line rather than getting a third row. */
[data-layout="mobile"] .rsw-head.lb-bar { padding: 12px 16px; gap: 10px; }
[data-layout="mobile"] .lb-count { margin-left: auto; }
/* The Leaderboard packs FOUR controls into `.rsw-controls-right` — a sort
   picker, search, Export Excel, and the active-student count — where the
   Recall Score workspace this row is shared with has only two. At 375px all
   four on one line ran the count clean off the right edge (measured: cut to
   "2" of "24 active students"). Wrapped into two rows instead: search gets
   the full-width row it already has elsewhere, export and the count share
   the second row exactly as they already do beside each other on desktop. */
[data-layout="mobile"] .lb-bar .rsw-controls-right {
  flex-wrap: wrap; row-gap: 10px;
}
[data-layout="mobile"] .lb-bar .rsw-sort { max-width: none; flex: 1 1 100%; }
[data-layout="mobile"] .lb-bar .rsw-search { flex: 1 1 100%; width: auto; }
[data-layout="mobile"] .lb-bar .lb-export,
[data-layout="mobile"] .lb-bar .lb-count { flex: none; }

[data-layout="mobile"] .rsw-tr.lb-tr { grid-template-columns: 32px minmax(0, 1.2fr) minmax(0, 1.4fr) 84px 84px; }

/* ── LOCKED CONTROLS ────────────────────────────────────────────────────────
   A control the member lacks permission for. Shown, greyed, padlocked, and
   explained on hover — never removed. See lockedIf() in lib/ui.js for why, and
   for the standing reminder that this is a courtesy over a server-side refusal
   rather than a control in its own right.

   The bubble is the design system's own `.tip-bubble`, reused verbatim so a
   locked control explains itself in exactly the voice the `?` tooltips already
   use. The only difference is the trigger: those open on click, this one on
   hover and focus, because there is no `?` to press — the control IS the
   thing being asked about. */
.locked { position: relative; display: inline-flex; }
/* `.is-locked` rather than `:disabled`, because lockedIf() also wraps controls
   that are not form elements and cannot carry the attribute. Matches the
   existing `.btn:disabled` treatment so a locked button and a busy one look
   like the same product. */
.locked .is-locked {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;   /* the wrapper handles the hover, and swallows clicks */
}
.locked .is-locked svg:first-child { flex: 0 0 auto; }
.lock-tip { display: none; z-index: 60; }
/* Focusable so the explanation is reachable by keyboard: a member tabbing
   through the page must be able to find out why an action is unavailable,
   which a hover-only bubble would deny them. */
.locked:hover .lock-tip,
.locked:focus-visible .lock-tip { display: block; }
.locked:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 12px; }
/* The one line that keeps a grantable-but-unenforced permission honest. Sits
   under its group's checkboxes in the Manage dialog — see permissionEditor(). */
.perm-pending {
  flex: 1 0 100%;
  margin-top: 2px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-3);
}
