/* Study Pill — primitives layer.
 *
 * Reusable components built from tokens.css. Once a screen migrates to these,
 * its bespoke `<style>` block can shrink to layout-only rules. Existing
 * screens keep working unchanged because every primitive uses a unique
 * `.sp-…` prefix that doesn't collide with the legacy class names.
 *
 * SEE THEM ALL AT /ui — every component, every state, both themes.
 * That page is the point. This file's first eight components were documented
 * here in exactly this comment and had ZERO adopters for months, because a
 * list of class names tells you nothing about what they look like or which
 * states exist. Before writing any CSS, open /ui and check whether the thing
 * already exists. (Enable with SP_UI_GALLERY=1; off in production by default.)
 *
 * Chrome
 *   .sp-app-bar           sticky top app bar (back · title · actions)
 *   .sp-bottom-nav        thumb-zone tab bar (safe-area aware)
 * Controls
 *   .sp-btn               primary · ghost · icon · sm · lg · [disabled]
 *   .sp-field             label · control · hint · .is-invalid + error
 *   .sp-tabs / .sp-tab    segmented control
 * Surfaces
 *   .sp-card              elevated surface (--interactive, --flat)
 *   .sp-row / .sp-list    tappable list line (lead · body · meta · trail)
 *   .sp-sheet             bottom sheet on phone, dialog on desktop
 * Status
 *   .sp-chip              neutral · accent · success · warn · danger · mono
 *   .sp-alert             error · warn · success — what happened + what to do
 *   .sp-meter             label · count · bar, with threshold states
 *   .sp-toast             ephemeral notification
 * States
 *   .sp-skeleton          loading placeholder (title/line/block/circle)
 *   .sp-empty             empty state — icon · title · body · actions
 * Type & layout
 *   .sp-kicker            the uppercase line above a title
 *   .sp-stack             vertical spacing scale
 */
@import url("./tokens.css");

/* ---------- Reset patches that the design wants everywhere ---------- */
/* Owner, 2026-09-04, on the deployed bar: "when i tap on sth in the buttom bar
   it has some big feel around it same as the serach which is bad". That is the
   platform's own tap highlight - a translucent block the size of the element -
   and this list is what turns it off. It named .sp-bottom-nav and the bar
   became .ga-bottomnav, so every new surface fell straight back to the
   default. A reset that enumerates class names has to be extended with them. */
.sp-app-bar *,
.sp-bottom-nav *,
.ga-bottomnav *,
.ga-subbar *,
.ga-playlist *,
.ga-orb,
.sp-card * { -webkit-tap-highlight-color: transparent; }

/* HIDDEN MEANS HIDDEN.

   `[hidden]` is only `display: none` in the user-agent sheet, so ANY `display`
   rule on the element beats it. Every author who has hit this in Study Pill has
   patched it locally - `.sp-ra-launch__btn[hidden]`, `.sp-ra-voice[hidden]`,
   `.sp-ra-btn[hidden]` - three copies of one rule, each written after the same
   surprise, and each covering only the class in front of that author.

   The fourth cost more. `.resume-ask__confirm { display: flex }` beat the
   `hidden` attribute on the pack page, so the two-step confirm behind "Start
   afresh" was never two steps: **"Clear my progress" - which wipes a student's
   answers on every device with no undo - sat permanently on screen, one tap
   away, under a warning explaining a question nobody had been asked.** The JS
   was correct the whole time; it toggled an attribute that could not win.

   One rule, `!important`, because the whole point is that it must beat a
   `display` an author did not know they were fighting. Anything that needs to
   animate out uses a class, not `hidden`. */
[hidden] { display: none !important; }

/* ---------- Focus (WCAG 2.2 SC 2.4.7, 2.4.11, 2.4.13) ----------
 *
 * The entire app had ONE :focus-visible rule before this. A student using a
 * keyboard, a switch, or a Bluetooth keyboard on a tablet had no reliable way
 * to see where they were — on a timed mock exam.
 *
 * :focus-visible, not :focus, so a mouse/touch press doesn't leave a ring
 * behind; the browser decides when the ring is warranted. `outline` rather
 * than box-shadow so it follows the border radius and survives on Windows
 * High Contrast / forced-colors, where box-shadow is dropped entirely.
 */
.sp-btn:focus-visible,
.sp-chip:focus-visible,
.sp-card--interactive:focus-visible,
.sp-app-bar__back:focus-visible,
.sp-bottom-nav__btn:focus-visible,
.theme-toggle:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}

/* Where a control sits flush against the viewport edge (the tab bar), an
   outward offset would be clipped. Pull it inside instead. */
.sp-bottom-nav__btn:focus-visible { outline-offset: -3px; }

/* ---------- App bar ---------- */
/* The bar used to WRAP: `flex-wrap: wrap` plus a rule giving the actions their
   own full-width row under 500px. Every phone is under 500px, so every phone
   got a two-row bar ~69px tall carrying a page name and a boxed back button,
   and no product name anywhere on it. gluearrow.com's bar is 55px, one row,
   wordmark left and plain icons right. This is that bar. */
.sp-app-bar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--sp-space-2);
  padding: 0 var(--sp-space-4);
  padding-top: env(safe-area-inset-top, 0);
  min-height: calc(56px + env(safe-area-inset-top, 0));
  background: var(--sp-paper);
  background: color-mix(in oklch, var(--sp-paper) 88%, transparent);
  backdrop-filter: saturate(1.2) blur(16px);
  -webkit-backdrop-filter: saturate(1.2) blur(16px);
  /* A plain bar with one rule under it, like the GlueArrow app's thread
     header (ruling 362): no tab hangs from it. */
  border-bottom: 1px solid var(--sp-rule-soft);
}
/* A chevron, not a button. Boxed, filled and bordered, it read as a control of
   equal weight to the brand beside it, which it is not. The 44px tap target is
   kept — that is a thumb requirement, not a visual one — by padding rather than
   by drawing a box. */
.sp-app-bar__back {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-left: calc(var(--sp-space-3) * -1);
  background: none; color: var(--sp-ink);
  border: 0; border-radius: var(--sp-radius-sm);
  text-decoration: none; cursor: pointer;
  font-size: 20px; line-height: 1;
  transition: background var(--sp-dur-fast) ease;
}
.sp-app-bar__back:hover { background: var(--sp-paper-2); }
.sp-app-bar__back:active { transform: scale(.94); }

.sp-app-bar__titles { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* THE WORDMARK SITS IN THE BAR, LEFT (ruling 362, 14 October). The tab that
   hung from the bar's middle (ruling 296) is gone: the owner asked for a
   plain bar like the app's thread header - back, the name, the controls on
   the right, one rule under it. Weight 800 (the Heavy face), the treatment
   gluearrow.com gives its own wordmark. `--sp-coral` is the text coral: 4.9:1
   on paper in light and 7:1 in dark. The link is still the way home, and its
   box is 44px tall for a thumb. */
.sp-app-bar__brand {
  display: inline-flex; align-items: center; align-self: flex-start;
  min-height: 44px;
  font-family: var(--sp-font-display);
  font-weight: 800; font-size: 18px; letter-spacing: -.02em; line-height: 1;
  color: var(--sp-coral);
  text-decoration: none;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.sp-app-bar__brand:hover { color: var(--sp-coral); }
.sp-app-bar__brand:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: -3px;
}

/* Where you are: quiet, one line, never competing with the wordmark. */
.sp-app-bar__title {
  min-width: 0; margin: 0;
  font-family: var(--sp-font-ui); font-weight: 500; font-size: 13px;
  color: var(--sp-ink-soft); letter-spacing: 0; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sp-app-bar__sub { color: var(--sp-ink-soft); font-weight: 400; }

.sp-app-bar__actions {
  flex: 0 0 auto; display: flex; gap: var(--sp-space-1); align-items: center;
}

/* ---------- Bottom nav ---------- */
.sp-bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid; grid-template-columns: repeat(var(--sp-bnav-count, 5), 1fr);
  padding: 6px 4px;
  padding-bottom: calc(6px + env(safe-area-inset-bottom, 0));
  background: color-mix(in oklch, var(--sp-paper) 92%, transparent);
  backdrop-filter: saturate(1.4) blur(20px);
  -webkit-backdrop-filter: saturate(1.4) blur(20px);
  border-top: 1px solid var(--sp-rule-soft);
  transition: transform var(--sp-dur-base) var(--sp-ease-out), opacity var(--sp-dur-base) ease;
}
.sp-bottom-nav.is-hidden { transform: translateY(120%); opacity: 0; pointer-events: none; }
.sp-bottom-nav__btn {
  background: transparent; border: 0; cursor: pointer;
  min-height: 56px; padding: 6px 4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; border-radius: var(--sp-radius-sm);
  font-family: var(--sp-font-ui); font-size: 11px; font-weight: 600;
  color: var(--sp-ink-soft);
  transition: color var(--sp-dur-fast) ease, background var(--sp-dur-fast) ease, transform 80ms ease;
  position: relative;
}
.sp-bottom-nav__btn:active { transform: scale(.94); }
.sp-bottom-nav__btn[aria-current="page"], .sp-bottom-nav__btn.is-active { color: var(--sp-coral); }
.sp-bottom-nav__btn .ico { font-size: 20px; line-height: 1; }
.sp-bottom-nav__btn .badge {
  position: absolute; top: 6px; right: 22%;
  background: var(--sp-coral); color: var(--sp-on-coral);
  font-size: 10px; font-weight: 700;
  min-width: 16px; height: 16px; padding: 0 5px;
  border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
}
@media (min-width: 768px) {
  .sp-bottom-nav { max-width: 560px; left: 50%; transform: translateX(-50%); border-radius: 18px 18px 0 0; border: 1px solid var(--sp-rule-soft); border-bottom: 0; }
  .sp-bottom-nav.is-hidden { transform: translate(-50%, 120%); }
}

/* ---------- Buttons ---------- */
.sp-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 20px; border-radius: var(--sp-radius-sm);
  font-family: var(--sp-font-ui); font-size: 14px; font-weight: 600;
  letter-spacing: .005em; line-height: 1;
  border: 1px solid var(--sp-rule); background: var(--sp-paper); color: var(--sp-ink);
  cursor: pointer; text-decoration: none;
  transition: background var(--sp-dur-fast) ease, border-color var(--sp-dur-fast) ease,
              transform 80ms ease, box-shadow var(--sp-dur-fast) ease;
  min-height: 44px;
}
.sp-btn:hover { border-color: var(--sp-ink-soft); }
.sp-btn:active { transform: translateY(1px); }
.sp-btn[disabled], .sp-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }

/* --sp-on-coral, not #fff. On the dark theme coral is a LIGHT colour, so a
   white label sat at 2.87:1 — a hard-coded #fff cannot flip with the theme,
   which is precisely how that shipped. */
.sp-btn--primary {
  background: var(--sp-coral); color: var(--sp-on-coral); border-color: var(--sp-coral);
  box-shadow: var(--sp-lift-1);
}
/* Hover darkens toward coral-deep, so the label keeps 4.5:1: coral-2 under
   white measured 2.97:1 (ruling 362). */
.sp-btn--primary:hover { background: color-mix(in oklch, var(--sp-coral) 82%, var(--sp-coral-deep)); border-color: color-mix(in oklch, var(--sp-coral) 82%, var(--sp-coral-deep)); transform: translateY(-1px); box-shadow: var(--sp-lift-2); }
.sp-btn--primary:active { transform: translateY(0); }

.sp-btn--ghost { background: transparent; }
.sp-btn--ghost:hover { background: var(--sp-paper-2); }

/* Full width. The action in a screen's thumb zone spans it, and `width: 100%`
   typed inline a dozen times is a modifier being reinvented one page at a
   time — which is the habit that produced thirteen page-level <style> blocks. */
.sp-btn--block { display: flex; width: 100%; }

/* Destructive — OUTLINED, deliberately, and never `--primary`.
 *
 * Coral is Study Pill's brand colour AND the only red it has. So a filled
 * coral "Delete pack" is the biggest, warmest, most inviting thing on the
 * screen: the styling says "recommended" while the label says "destroy your
 * notes". DESIGN_STANDARD §10 calls that out — visual weight must match which
 * option is actually better for the reader.
 *
 * Outlined keeps it clearly available and clearly not the default. On a delete
 * confirmation the SAFE choice takes `--primary`; this takes the risk.
 * Caught by opening /ui and looking at the sheet demo, which had it backwards.
 */
.sp-btn--danger {
  background: transparent;
  color: var(--sp-coral);
  border-color: color-mix(in oklch, var(--sp-coral) 45%, var(--sp-rule));
}
.sp-btn--danger:hover {
  background: color-mix(in oklch, var(--sp-coral) 10%, var(--sp-paper));
  border-color: var(--sp-coral);
}

.sp-btn--icon {
  width: 44px; height: 44px; padding: 0;
  border-radius: 50%;
}

.sp-btn--lg { padding: 14px 26px; font-size: 15px; min-height: 52px; border-radius: var(--sp-radius-md); }
.sp-btn--sm { padding: 8px 14px; font-size: 13px; min-height: 36px; border-radius: var(--sp-radius-xs); }

/* ---------- Card ---------- */
.sp-card {
  background: var(--sp-paper); color: var(--sp-ink);
  border: 1px solid var(--sp-rule-soft);
  border-radius: var(--sp-radius-md);
  padding: var(--sp-space-5) var(--sp-space-6);
  box-shadow: var(--sp-lift-1);
  transition: transform var(--sp-dur-base) var(--sp-ease-out),
              box-shadow var(--sp-dur-base) var(--sp-ease-out),
              border-color var(--sp-dur-base) var(--sp-ease-out);
}
.sp-card--interactive { cursor: pointer; }
.sp-card--interactive:hover { transform: translateY(-2px); box-shadow: var(--sp-lift-2); border-color: var(--sp-rule); }
.sp-card--flat { box-shadow: none; }

/* ---------- Chip ---------- */
.sp-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 12px; font-weight: 500;
  letter-spacing: .01em;
  background: var(--sp-paper-2); color: var(--sp-ink-2);
  border: 1px solid var(--sp-rule-soft);
  white-space: nowrap;
}
.sp-chip--accent  { background: color-mix(in oklch, var(--sp-coral) 12%, var(--sp-paper)); color: var(--sp-coral-deep); border-color: color-mix(in oklch, var(--sp-coral) 30%, var(--sp-rule)); }
/* In dark, coral-deep on its tint measured 4.01:1 (ruling 362); the dark
   coral is the light one, and it clears 4.5:1 there. */
html[data-theme="dark"] .sp-chip--accent { color: var(--sp-coral); }
/* --sp-pine-text, not --sp-pine: this is TYPE on a tinted surface, and the
   fill-weight green measures 2.96:1 against pine-soft. Same split for warn. */
.sp-chip--success { background: var(--sp-pine-soft); color: var(--sp-pine-text); border-color: color-mix(in oklch, var(--sp-pine) 35%, var(--sp-rule)); }
.sp-chip--warn    { background: var(--sp-coral-soft); color: var(--sp-coral); border-color: color-mix(in oklch, var(--sp-coral) 35%, var(--sp-rule)); }
.sp-chip--danger  { background: color-mix(in oklch, var(--sp-coral) 14%, var(--sp-paper)); color: var(--sp-coral); border-color: color-mix(in oklch, var(--sp-coral) 30%, var(--sp-rule)); }
.sp-chip--mono    { font-family: var(--sp-font-mono); font-size: 11px;   }

/* ---------- Toast ---------- */
.sp-toast {
  position: fixed; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom, 0));
  transform: translateX(-50%) translateY(120%);
  background: var(--sp-ink); color: var(--sp-paper);
  padding: 10px 16px; border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 13px; font-weight: 500;
  box-shadow: var(--sp-lift-3); z-index: 1100;
  opacity: 0;
  transition: transform var(--sp-dur-slow) var(--sp-ease-out), opacity var(--sp-dur-base) ease;
  max-width: calc(100vw - 32px);
}
.sp-toast.is-show { transform: translateX(-50%) translateY(0); opacity: 1; }

/* ---------- Empty state ---------- */
.sp-empty {
  padding: var(--sp-space-9) var(--sp-space-5);
  text-align: center;
  color: var(--sp-ink-soft);
  font-family: var(--sp-font-display);
  font-size: 17px; line-height: 1.45;
}
.sp-empty .sp-empty__hint {
  display: block; margin-top: var(--sp-space-2);
  font-family: var(--sp-font-mono); font-size: 11px;
}

/* ---------- Vertical stack helper ---------- */
.sp-stack { display: flex; flex-direction: column; }
.sp-stack--1 > * + * { margin-top: var(--sp-space-1); }
.sp-stack--2 > * + * { margin-top: var(--sp-space-2); }
.sp-stack--3 > * + * { margin-top: var(--sp-space-3); }
.sp-stack--4 > * + * { margin-top: var(--sp-space-4); }
.sp-stack--5 > * + * { margin-top: var(--sp-space-5); }
.sp-stack--6 > * + * { margin-top: var(--sp-space-6); }

/* ---------- Reduced motion respect ---------- */
@media (prefers-reduced-motion: reduce) {
  .sp-app-bar, .sp-bottom-nav, .sp-btn, .sp-card, .sp-chip, .sp-toast {
    transition: opacity .12s ease;
  }
}

/* ---- Inline SVG icons (studypill/templates/partials/_icons.html) ----------
   Study Pill uses real stroke icons, never emoji: emoji render differently on
   every device/OS, can't take brand colour, and look like clip-art next to the
   serif type. These inherit `currentColor` and size in `em`, so an icon always
   matches the text beside it. Class is `sp-ico` (NOT `.ico` — that name is
   already taken by the resume-hero's 50px badge and the drop-zone glyph). */
.sp-ico {
  display: inline-block;
  vertical-align: -0.14em;   /* optical centre against lowercase text */
  flex-shrink: 0;
  stroke: currentColor;
}

/* Theme toggle shows the theme you'd switch TO: a moon while light, a sun while
   dark. Both icons ship in the button and CSS picks — the toggle's JS used to
   swap an emoji via textContent, which would wipe inline SVG children. */
.theme-toggle .sp-ico { display: none; }
html[data-theme="light"] .theme-toggle .sp-ico-moon { display: inline-block; }
html[data-theme="dark"]  .theme-toggle .sp-ico-sun  { display: inline-block; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SECOND WAVE — the components the pages were already re-implementing.
   ═══════════════════════════════════════════════════════════════════════════

   Not picked off a generic design-system checklist. Every one below was counted
   across the thirteen page <style> blocks first, so this builds what the
   product already needs rather than what a component library usually ships:

     card/panel   10 pages      loading / skeleton   7 pages
     button       10 pages      empty state          7 pages
     list row      9 pages      progress meter       5 pages
     kicker        9 pages      toast                4 pages
     field         9 pages      tabs / segmented     4 pages
     alert         8 pages      sheet / modal        4 pages
     chip          8 pages

   Each carries the states DESIGN_STANDARD §7 asks for — loading, empty, error,
   disabled, dense. That is not thoroughness for its own sake: a missing state
   is precisely why a page gives up and writes its own CSS, which is how the
   first wave of primitives ended up with zero adopters.
   ========================================================================= */

/* ---------- Kicker ----------
   The small uppercase line above a title. Nine pages drew their own, each with
   its own dot, letter-spacing and colour. */
/* Sentence case, soft ink, no dot (ruling 296). It was 11.5px uppercase
   coral with letter-spacing and a glowing dot - the "uppercase letter-spaced
   kicker" the standard lists among the tells, and a second coral object on
   every screen that had one. A line above a title says it is the line above
   the title by being smaller and quieter, nothing more. */
.sp-kicker {
  display: inline-flex; align-items: center; gap: var(--sp-space-2);
  font-family: var(--sp-font-ui); font-size: 13px; font-weight: 600;
  letter-spacing: 0; text-transform: none;
  color: var(--sp-ink-soft);
  margin-bottom: var(--sp-space-1);
}
.sp-kicker::before { content: none; }
.sp-kicker--quiet { color: var(--sp-ink-soft); }
.sp-kicker--bare::before { display: none; }

/* ---------- Field ----------
   Label, control, hint — and an error state that belongs to the component
   instead of being reinvented by every form. */
.sp-field { display: flex; flex-direction: column; gap: 6px; }
.sp-field__label {
  font-family: var(--sp-font-ui); font-size: 13px; font-weight: 600;
  color: var(--sp-ink-2);
}
.sp-field__control,
.sp-field input[type="text"],
.sp-field input[type="email"],
.sp-field input[type="tel"],
.sp-field input[type="number"],
.sp-field input[type="date"],
/* A timetable asks for a time and a Teams link, and a school calendar arrives
   as a file. Those three types were missing, so every form that needed one
   either looked unstyled or grew its own copy of the rules above — which is
   how thirteen page-level <style> blocks started. */
.sp-field input[type="time"],
.sp-field input[type="url"],
.sp-field input[type="file"],
.sp-field input[type="search"],
.sp-field select,
.sp-field textarea {
  width: 100%;
  min-height: 44px;
  padding: 11px 14px;
  /* 16px, not 15: iOS zooms the viewport when a field under 16px takes focus,
     which on a phone throws the whole layout sideways mid-form. */
  font-family: var(--sp-font-ui); font-size: 16px;
  color: var(--sp-ink);
  background: var(--sp-paper);
  border: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius-sm);
  transition: border-color var(--sp-dur-fast) ease;
}
.sp-field textarea { min-height: 120px; resize: vertical; line-height: 1.55; }
/* A file input is a control wrapped around another control. Left alone the
   inner button keeps the browser's grey, which is the one piece of chrome on
   the page that never matches the product. */
.sp-field input[type="file"] { padding: 9px 12px; line-height: 24px; }
.sp-field input[type="file"]::file-selector-button {
  font: inherit; font-size: 13px; font-weight: 600;
  margin-right: 10px; padding: 6px 12px; min-height: 32px; cursor: pointer;
  color: var(--sp-ink); background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule); border-radius: var(--sp-radius-xs);
}
.sp-field__control:hover,
.sp-field input:hover, .sp-field select:hover, .sp-field textarea:hover {
  border-color: var(--sp-ink-soft);
}
.sp-field__control:focus-visible,
.sp-field input:focus-visible,
.sp-field select:focus-visible,
.sp-field textarea:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
  border-color: var(--sp-coral);
}
.sp-field__hint {
  font-family: var(--sp-font-ui); font-size: 12.5px; line-height: 1.45;
  color: var(--sp-ink-soft);
}
/* Invalid never signals by colour alone (WCAG 1.4.1): the border also
   thickens, and the message says what to do about it. */
.sp-field.is-invalid .sp-field__control,
.sp-field.is-invalid input,
.sp-field.is-invalid select,
.sp-field.is-invalid textarea {
  border-color: var(--sp-coral);
  border-width: 2px;
}
.sp-field__error {
  display: flex; align-items: flex-start; gap: 6px;
  font-family: var(--sp-font-ui); font-size: 12.5px; font-weight: 500;
  color: var(--sp-coral);
}
.sp-field input[disabled], .sp-field select[disabled], .sp-field textarea[disabled] {
  opacity: .55; cursor: not-allowed; background: var(--sp-paper-2);
}

/* ---------- Row ----------
   A tappable list line: lead slot, body, trailing meta. Nine pages have one. */
.sp-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-space-3);
  padding: var(--sp-space-3) var(--sp-space-4);
  background: var(--sp-paper);
  border: 1px solid var(--sp-rule-soft);
  border-radius: var(--sp-radius-md);
  color: inherit; text-decoration: none;
  transition: transform var(--sp-dur-base) var(--sp-ease-out),
              box-shadow var(--sp-dur-base) var(--sp-ease-out),
              border-color var(--sp-dur-base) var(--sp-ease-out);
}
a.sp-row, button.sp-row { cursor: pointer; width: 100%; text-align: left; }
a.sp-row:hover, button.sp-row:hover {
  transform: translateY(-2px); box-shadow: var(--sp-lift-2); border-color: var(--sp-rule);
}
a.sp-row:focus-visible, button.sp-row:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}
.sp-row__lead {
  width: 40px; height: 40px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--sp-radius-sm);
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  color: var(--sp-ink-2);
}
.sp-row__body { min-width: 0; }
.sp-row__title {
  /* block, not inline: an inline span cannot clip, so a long title ran
     under the trailing control instead of ending in an ellipsis. */
  display: block;
  font-family: var(--sp-font-display); font-weight: 500; font-size: 16px;
  line-height: 1.3; color: var(--sp-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One compact metadata line, middot-separated. Good mobile apps all land on
   this shape, and each page here had invented its own version of it. */
.sp-row__meta {
  display: flex; flex-wrap: wrap; gap: 4px 10px;
  font-family: var(--sp-font-ui); font-size: 12.5px;
  color: var(--sp-ink-soft);
  font-variant-numeric: tabular-nums;
  margin-top: 3px;
}
/* The separator belongs to the item BEFORE it, not the one after.
   `* + *::before` puts it on the following item, so whenever the line wraps —
   which on a 390px phone is most rows with more than three facts — the new line
   opened with a stray "· 140 MCQs". Hung off the preceding item instead, a wrap
   leaves the middot at the end of the line above, where it reads as "continues"
   rather than as a rendering fault. */
.sp-row__meta > *:not(:last-child)::after { content: "\00B7"; margin-left: 10px; opacity: .6; }
.sp-row__trail { flex: 0 0 auto; color: var(--sp-ink-soft); }
.sp-row--dense { padding: var(--sp-space-2) var(--sp-space-3); }
.sp-row--dense .sp-row__lead { width: 32px; height: 32px; }
/* One row carrying more weight than its neighbours — the class's goal beside
   your own, the paper with a deadline beside the ones without. */
.sp-row--accent {
  border-color: color-mix(in oklch, var(--sp-coral) 40%, var(--sp-rule));
  background: color-mix(in oklch, var(--sp-coral) 4%, var(--sp-paper));
}
.sp-row--accent .sp-row__lead {
  background: color-mix(in oklch, var(--sp-coral) 12%, var(--sp-paper));
  border-color: color-mix(in oklch, var(--sp-coral) 30%, var(--sp-rule));
  /* Ink, not coral: coral on its own 12% tint measured 4.21:1 (ruling 362
     asks 4.5). The tint and the edge still say "this one". */
  color: var(--sp-ink);
}
/* A row whose work is finished. Never colour alone (WCAG 1.4.1): the lead slot
   carries a tick, and this only quietens what is around it. */
.sp-row--done .sp-row__title { color: var(--sp-ink-soft); }
.sp-row--done .sp-row__lead { color: var(--sp-pine-text); }
.sp-list { display: flex; flex-direction: column; gap: var(--sp-space-2); }
/* A list of rows is often a real <ul>, and a browser's bullets and indent
   are not part of this component. */
ul.sp-list, ol.sp-list { list-style: none; margin: 0; padding: 0; }

/* ---------- Faces ----------
   Who else is on this course. Owner, 2026-09-06: *"most cards should have a
   bubble of profile picture on it — it could be lecturer or student who has
   access to the course."*

   THERE ARE NO PHOTOGRAPHS. Study Pill holds none; identity, including any
   picture, belongs to the ring, and inventing one would be inventing a person.
   A face is initials on a colour picked from the user's own id, so the same
   person is the same colour on every device, always (classroom.py).

   It lives here because two pages draw it, and it had been written twice —
   once inside packs_list.html's <style> block and once in classroom.css, under
   the same class name, at different sizes. One definition, both callers. */
.sp-faces { display: flex; align-items: center; gap: 0; }
.sp-face {
  width: 28px; height: 28px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--face, var(--sp-ink-soft)); color: var(--sp-on-face);
  font-family: var(--sp-font-ui); font-size: 11px; font-weight: 700;
  border: 2px solid var(--sp-paper);
  /* Overlapped, but only just. At -8px the first face's initials sat
     half under the next one, which is the one thing a face has to show. */
  margin-left: -5px;
}
.sp-faces > .sp-face:first-child { margin-left: 0; }
.sp-face--lg { width: 34px; height: 34px; font-size: 12.5px; margin-left: 0; }
/* Each ring says something true, and each is also in the face's `title`, so
   neither is signalled by colour alone (WCAG 1.4.1). */
.sp-face--rep  { box-shadow: 0 0 0 2px var(--sp-coral); }
.sp-face--live { box-shadow: 0 0 0 2px var(--sp-pine); }
.sp-face--more { background: var(--sp-paper-edge); color: var(--sp-ink-2); }
.sp-faces__live {
  margin-left: 8px;
  font-family: var(--sp-font-ui); font-size: 11.5px; font-weight: 600;
  color: var(--sp-pine-text);
}

/* ---------- Alert ----------
   Eight pages roll their own error box. An alert is two parts: what happened,
   and what to do next — see DESIGN_STANDARD §8. */
.sp-alert {
  display: flex; align-items: flex-start; gap: var(--sp-space-3);
  padding: var(--sp-space-3) var(--sp-space-4);
  border-radius: var(--sp-radius-sm);
  border: 1px solid var(--sp-rule);
  background: var(--sp-paper-2);
  color: var(--sp-ink-2);
  font-family: var(--sp-font-ui); font-size: 13.5px; line-height: 1.5;
}
.sp-alert__icon { flex: 0 0 auto; margin-top: 1px; }
.sp-alert__body { min-width: 0; flex: 1; }
.sp-alert__title { font-weight: 600; color: var(--sp-ink); margin-bottom: 2px; }
.sp-alert--error {
  background: color-mix(in oklch, var(--sp-coral) 8%, var(--sp-paper));
  border-color: color-mix(in oklch, var(--sp-coral) 32%, var(--sp-rule));
}
.sp-alert--error .sp-alert__icon { color: var(--sp-coral); }
.sp-alert--warn {
  background: var(--sp-coral-soft);
  border-color: color-mix(in oklch, var(--sp-coral) 35%, var(--sp-rule));
}
.sp-alert--warn .sp-alert__icon { color: var(--sp-coral); }
.sp-alert--success {
  background: var(--sp-pine-soft);
  border-color: color-mix(in oklch, var(--sp-pine) 35%, var(--sp-rule));
}
.sp-alert--success .sp-alert__icon { color: var(--sp-pine-text); }

/* ---------- Skeleton ----------
   Seven pages fake a loading state. A skeleton says "this is arriving, and
   roughly this shape"; a spinner only says "something is happening". */
.sp-skeleton {
  display: block;
  border-radius: var(--sp-radius-xs);
  background: linear-gradient(
    90deg,
    var(--sp-paper-2) 0%,
    color-mix(in oklch, var(--sp-paper-edge) 60%, var(--sp-paper-2)) 50%,
    var(--sp-paper-2) 100%);
  background-size: 200% 100%;
  animation: spShimmer 1.4s linear infinite;
}
@keyframes spShimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.sp-skeleton--title { height: 1.6em; width: 60%; }
.sp-skeleton--line { height: 1em; width: 100%; margin: .25em 0; }
.sp-skeleton--line-short { height: 1em; width: 40%; margin: .25em 0; }
.sp-skeleton--block { height: 96px; border-radius: var(--sp-radius-md); }
.sp-skeleton--circle { border-radius: 50%; width: 40px; height: 40px; }

/* ---------- Meter ----------
   Label, count, bar. Five pages have one, each with its own thresholds. */
.sp-meter__label {
  display: flex; justify-content: space-between; gap: var(--sp-space-3);
  font-family: var(--sp-font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--sp-ink-soft); margin-bottom: 5px;
}
.sp-meter__count { color: var(--sp-ink); font-variant-numeric: tabular-nums; }
.sp-meter__track {
  height: 7px; border-radius: 999px; overflow: hidden;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
}
.sp-meter__fill {
  height: 100%; border-radius: 999px;
  background: var(--sp-pine);
  transition: width var(--sp-dur-slow) var(--sp-ease-out);
}
.sp-meter__fill--warn { background: var(--sp-coral); }
.sp-meter__fill--danger { background: var(--sp-coral); }
/* At the limit the count also changes weight, so "you are out" does not
   depend on seeing the bar's hue (WCAG 1.4.1). */
.sp-meter.is-danger .sp-meter__count { color: var(--sp-coral); font-weight: 700; }

/* ---------- Tabs / segmented control ---------- */
.sp-tabs {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  border-radius: 999px;
}
.sp-tab {
  appearance: none; border: 0; background: none; cursor: pointer;
  min-height: 44px; padding: 0 var(--sp-space-4);
  border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 13.5px; font-weight: 600;
  color: var(--sp-ink-soft);
  transition: background var(--sp-dur-fast) ease, color var(--sp-dur-fast) ease;
}
.sp-tab:hover { color: var(--sp-ink); }
.sp-tab[aria-selected="true"], .sp-tab.is-active {
  background: var(--sp-coral); color: var(--sp-on-coral);
}
.sp-tab:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}

/* ---------- Sheet ----------
   A bottom sheet on a phone, a centred dialog on a wide screen. Four pages
   have their own modal and none of them had a grab handle, so the affordance
   that says "this can be dismissed" lives here now. */
.sp-sheet-backdrop {
  position: fixed; inset: 0; z-index: 1000;
  background: color-mix(in oklch, var(--sp-ink) 45%, transparent);
  backdrop-filter: blur(2px);
  opacity: 0; pointer-events: none;
  transition: opacity var(--sp-dur-base) ease;
}
.sp-sheet-backdrop.is-open { opacity: 1; pointer-events: auto; }
.sp-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1001;
  max-height: 88dvh; overflow-y: auto;
  background: var(--sp-paper);
  border-top: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius-xl) var(--sp-radius-xl) 0 0;
  padding: var(--sp-space-4) var(--sp-space-5)
           calc(var(--sp-space-6) + env(safe-area-inset-bottom, 0));
  box-shadow: var(--sp-lift-3);
  transform: translateY(100%);
  transition: transform var(--sp-dur-slow) var(--sp-ease-out);
}
.sp-sheet.is-open { transform: translateY(0); }
.sp-sheet__handle {
  width: 40px; height: 4px; border-radius: 999px;
  background: var(--sp-rule);
  margin: 0 auto var(--sp-space-4);
}
.sp-sheet__title {
  font-family: var(--sp-font-display); font-weight: 600; font-size: 20px;
  color: var(--sp-ink); margin-bottom: var(--sp-space-2);
}
@media (min-width: 640px) {
  .sp-sheet {
    left: 50%; bottom: auto; top: 50%;
    max-width: 520px; width: calc(100% - 48px);
    border-radius: var(--sp-radius-lg);
    border: 1px solid var(--sp-rule);
    transform: translate(-50%, -40%);
    opacity: 0;
  }
  .sp-sheet.is-open { transform: translate(-50%, -50%); opacity: 1; }
  .sp-sheet__handle { display: none; }
}

/* ---------- Empty state, with somewhere to go ----------
   `.sp-empty` existed but was type only. Seven pages needed an icon and an
   action, so seven pages wrote their own. An empty state that does not say how
   to put the first thing in is half a screen. */
.sp-empty__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; margin-bottom: var(--sp-space-4);
  border-radius: 50%;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  color: var(--sp-ink-soft);
}
.sp-empty__title {
  font-family: var(--sp-font-display); font-weight: 500;
  font-size: clamp(20px, 3.5vw, 26px); line-height: 1.25;
  color: var(--sp-ink); margin-bottom: var(--sp-space-2);
}
.sp-empty__body {
  font-family: var(--sp-font-ui); font-size: 15px; line-height: 1.55;
  color: var(--sp-ink-2); max-width: 44ch; margin: 0 auto var(--sp-space-5);
}
.sp-empty__actions {
  display: flex; gap: var(--sp-space-3); justify-content: center; flex-wrap: wrap;
}

/* ---------- Reduced motion, for everything added above ---------- */
@media (prefers-reduced-motion: reduce) {
  .sp-row, .sp-sheet, .sp-sheet-backdrop, .sp-meter__fill, .sp-tab,
  .sp-field__control, .sp-field input, .sp-field select, .sp-field textarea {
    transition: opacity .12s ease;
  }
  .sp-skeleton { animation: none; }
  a.sp-row:hover, button.sp-row:hover { transform: none; }
}

/* WHAT IS NEW (studypill/whats_new.py). A dot and words, never colour alone.
   Tokens only, so the phone fallbacks in tokens.css apply. 44px targets. */
.sp-new-dot { display: inline-block; vertical-align: middle; margin-right: 6px; width: 8px; height: 8px; border-radius: 50%; background: var(--sp-coral-deep); flex: none; }
.sp-new-tag { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 1.4; color: var(--sp-coral-deep); background: var(--sp-paper); border: 1px solid var(--sp-coral-deep); vertical-align: middle; }
.sp-new-line { display: block; margin: 0 0 12px; padding: 4px 12px; font-size: 14px; line-height: 1.4; color: var(--sp-ink); background: var(--sp-paper); border: 1px solid var(--sp-coral-deep); border-radius: 12px; }
.sp-new-line[hidden] { display: none; }
/* The daily recheck (ruling 402): a Corrected label on a question, and the
   line saying what the answer was and is. Words and a border, never colour alone. */
.sp-corrected { margin: 8px 0; padding: 8px 12px; border: 1px solid var(--sp-coral-deep); border-radius: 12px; background: var(--sp-paper); color: var(--sp-ink); }
.sp-corrected p { margin: 4px 0 0; font-size: 14px; line-height: 1.4; }
.sp-corrected__why { color: var(--sp-ink); }
.sp-corrected__label { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--sp-coral-deep); color: var(--sp-coral-deep); font-size: 12px; font-weight: 700; line-height: 1.4; }
.sp-corrected-tag { margin: 0 0 6px; }
.sp-new-line a { display: inline-flex; align-items: center; min-height: 44px; margin: 0 4px; color: var(--sp-coral-deep); font-weight: 600; text-decoration: underline; }
.sp-row__new { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--sp-coral-deep); }
.sp-new-home { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.sp-new-home a { display: inline-flex; align-items: center; min-height: 44px; color: var(--sp-coral-deep); font-weight: 600; text-decoration: underline; }
/* In dark, coral-deep on paper measured 4.24:1 (ruling 362); the dark coral
   is the light one and clears 4.5:1. */
html[data-theme="dark"] .sp-new-home a,
html[data-theme="dark"] .sp-new-line a { color: var(--sp-coral); }
