/* Inter ships no Tamil glyphs. Without an explicit Tamil face every Tamil
   line falls back to whatever the OS provides — Nirmala UI on Windows,
   something arbitrary elsewhere, tofu in the worst case. Tamil also needs
   more leading than Latin at the same size. */

body {
  font-family: var(--font-ui);
  background: var(--surface-page);
  color: var(--text-main);
  margin: 0;
}

/* Fraunces is a display face — wide and soft, right at 18-26px and tiring at
   12px across thousands of question stems. Newsreader is the reading face and
   carries the stems. They never appear at the same size, which is what stops
   two serifs from reading as indecision. */
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.015em; }

.q-stem {
  font-family: var(--font-read);
  font-size: 1.05em;
  line-height: 1.45;
  letter-spacing: -0.005em;
  color: var(--text-main);
  max-width: var(--measure);
}

.statement-text-container, .option-text-container {
  max-width: var(--measure);
}

/* Fills the row beside the statement badge; the column keeps a wrapped
   Tamil line under its English line rather than beside it. */
.statement-text-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  font-size: 0.95em;
  line-height: 1.5;
}

.en-text {
  font-family: var(--font-ui);
  line-height: 1.6;
}

/* Tamil keeps 1.85 leading at every width. It is the one value that must not
   be compressed to save vertical space. */
.q-tamil-text {
  font-family: var(--font-tamil);
  line-height: 1.85;
  margin-top: 6px;
  color: var(--text-muted);
}

body.lang-tamil .q-tamil-text { color: var(--text-main); }

/* The 6px above separates a Tamil line from the English line it follows. In
   Tamil-only mode there is no English line, so the Tamil text leads the block
   and the gap is wrong. The old pages suppressed this with an inline
   style="margin-top:0" written into every rendered node; this replaces it. */
.q-tamil-text:first-child { margin-top: 0; }

/* Letterspaced small against serif large is what carries the premium read.
   It is a system rule, not a per-page flourish. */
.micro-label {
  font-family: var(--font-ui);
  font-size: 0.53rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.question-card {
  background: var(--surface-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  padding: 20px;
  width: 100%;
  max-width: 900px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 14px;
}

.q-progress {
  font-weight: 700;
  margin: 0;
}

/* Tabular numerals so counters and stat values do not shift width as they
   change. Fraunces for the display sizes, wherever a number is the subject. */
.stat-value, .q-progress, .option-label, .count-badge {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
}

.tag-row { display: flex; gap: 6px; flex-wrap: wrap; }

.tag-badge {
  font-size: 0.53em;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.tag-badge:empty { display: none; }

/* Rectangular tiles in both study and quiz mode — the decision that makes
   options work on a phone. */
.option-card {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--tap-target);
  padding: 11px 13px;
  margin-bottom: 7px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--text-main);
}

.option-card.quiz-active { cursor: pointer; }
.option-card.quiz-active:hover {
  border-color: var(--border-strong);
  background: var(--surface-sunken);
}

/* Correct is teal. A second green would be the only hue on the page with no
   relationship to the brand — and correct/wrong stays unambiguous because the
   two fill differently as well as differing in hue. */
.option-card.correct, .option-card.user-correct {
  border-color: var(--primary);
  background: var(--primary-glow);
  color: var(--primary-hover);
}
.option-card.correct .option-label, .option-card.user-correct .option-label {
  color: var(--primary-text);
}

.option-card.user-incorrect {
  border-color: var(--accent-red-line);
  background: var(--accent-red-bg);
  color: var(--accent-red);
  cursor: not-allowed;
}
.option-card.user-incorrect .option-label { color: var(--accent-red-line); }
.option-card.user-incorrect.shake { animation: shake 0.3s; }

.options-grid { display: flex; flex-direction: column; gap: 0; margin-top: 14px; }

@keyframes shake {
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

.option-label {
  font-weight: 400;
  font-size: 0.72em;
  color: var(--text-faint);
  min-width: 1.5em;
}

/* Wraps the statements list and the match table. match_table questions
   render two columns that overflow a 360px viewport, so this scrolls inside
   its own box and the page body never scrolls sideways, and stack the lists
   on the narrowest screens. */
.q-content-block {
  overflow-x: auto;
  margin-top: 12px;
  padding: 14px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

.match-table { border-collapse: collapse; width: 100%; min-width: 320px; }
.match-table th, .match-table td {
  border: 1px solid var(--border-color);
  padding: 8px 10px;
  text-align: left;
  vertical-align: top;
}

@media (max-width: 600px) {
  .match-table, .match-table thead, .match-table tbody,
  .match-table tr, .match-table th, .match-table td { display: block; width: 100%; }
  .match-table thead { display: none; }
  .match-table tr { margin-bottom: 10px; border: 1px solid var(--border-color); border-radius: var(--radius); }
  .match-table td { border: none; border-bottom: 1px solid var(--border-color); }
  .match-table td:last-child { border-bottom: none; }

  /* Hiding the header leaves each stacked cell unlabelled — "Chola" with no
     hint that it belongs under "List I". Re-attach the column name from a
     data-label attribute on the cell. render.js must emit it (Task 12); until
     it does, attr() resolves empty and this renders nothing rather than
     breaking the layout. */
  .match-table td[data-label]::before {
    content: attr(data-label) ": ";
    font-weight: 700;
    color: var(--text-muted);
  }
}

/* The remaining selectors below are all emitted by render.js and the quiz
   state machine (Tasks 12-13). The plan's interface list named only
   .statements-list and .ar-container, but the renderer emits seventeen more —
   without these the quiz would show its correct/wrong verdict and its
   remaining-chances counter as unstyled body text. */

.q-source {
  font-size: 0.75em;
  color: var(--text-muted);
}

.q-type-badge {
  font-size: 0.65em;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 2px 8px;
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text-muted);
  white-space: nowrap;
}

/* Tag variants share .tag-badge's shape and only recolour. .tag-theme and
   .tag-topic deliberately drop their own colours and go neutral — three
   coloured tag variants on one card is the "many colours" problem in
   miniature. .tag-unit keeps teal because it is the one that carries
   meaning. */
.tag-unit  { background: var(--primary-glow); color: var(--primary-text); }
.tag-theme { background: var(--surface-sunken); color: var(--text-muted); }
.tag-topic { background: var(--surface-sunken); color: var(--text-muted); }

.statements-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.statement-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

/* The statement marker — "I", "II" — reads as a badge, not as bold text. */
.statement-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: var(--radius);
  background: var(--surface-sunken);
  font-family: var(--font-display);
  font-size: 0.78rem;
  /* Fraunces is loaded at 400 and 600 only, so 600 is the heaviest weight
     available here — a 700 would render as this same 600. */
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

.ar-container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.ar-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 10px 12px;
  border-left: 3px solid var(--primary);
  background: var(--surface-sunken);
  border-radius: 0 var(--radius) var(--radius) 0;
}

/* "ASSERTION" / "REASON". Uppercase and teal so the label reads as a label
   and not as the first words of the sentence. */
.ar-label {
  min-width: 8.5em;
  font-family: var(--font-ui);
  font-size: 0.8em;
  /* Inter tops out at 700 in the loaded set; the 750 an older inline rule
     asked for renders as this same 700. */
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--primary-text);
  white-space: nowrap;
}

.context-stem {
  margin-top: 12px;
  padding-left: 12px;
  border-left: 3px solid var(--border-color);
  color: var(--text-muted);
  max-width: var(--measure);
}

/* The footer sits under the card body with a rule above it, source on the
   left and answer state on the right. */
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 0.8rem;
  color: var(--text-muted);
  border-top: 1px solid var(--border-color);
  padding-top: 14px;
  margin-top: 16px;
}

.correct-ans {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-ui);
  font-size: 0.6em;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--primary-glow);
  color: var(--primary-text);
}

/* Quiz verdict. A wrong answer deliberately does NOT reveal the correct
   option — it reports the chances that remain, so the next guess is still the
   student's to make. */
.quiz-result {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-weight: 600; padding: 10px 13px; border-radius: var(--radius);
}
.quiz-result-correct { background: var(--primary-glow); color: var(--primary-text); }
.quiz-result-wrong {
  background: var(--accent-red-bg);
  color: var(--accent-red);
  border-left: 3px solid var(--accent-red-line);
}

.chances-chip {
  font-size: 0.56em; font-weight: 700; letter-spacing: 0.09em;
  padding: 3px 7px; border-radius: var(--radius);
  background: var(--accent-amber-bg); color: var(--accent-amber);
}

.nav-btn {
  font: inherit; font-family: var(--font-ui);
  /* border-box: index.html sets width: 100% on this at 480px (Task 10, Step
     1) and carries no global box-sizing reset of its own — without this the
     button's own padding would add on top of that 100% and overflow the
     viewport. */
  box-sizing: border-box;
  min-height: var(--tap-target); padding: 10px 18px;
  border: 1px solid var(--border-color); border-radius: var(--radius);
  background: var(--surface-card); color: var(--text-muted); cursor: pointer;
}
.nav-btn:hover { border-color: var(--border-strong); background: var(--surface-sunken); }

/* Access badges on the paper-wise tables. Replaces the old .lock-badge, which
   read "Locked · 200 Q": "Locked" names a wall, "Subscribe" names the way past
   it, and the question count was already its own column beside it.

   Subscribe is a real <button> because it opens the subscribe modal via
   paywall.js's delegated [data-action] handler — it is an action, not a label,
   and gets the 44px floor from the shared tap-target rule below. */
.subscribe-badge {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-ui);
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 7px 13px; border-radius: var(--radius);
  border: 1px solid var(--accent-amber-line);
  background: var(--accent-amber-bg); color: var(--accent-amber);
  cursor: pointer;
  white-space: nowrap;
}
.subscribe-badge:hover { border-color: var(--accent-amber); background: var(--surface-card); }

/* Free is a statement of fact, so it stays quiet — no border, no button
   affordance. It exists so a row is never silent about its access. */
.free-badge {
  display: inline-flex; align-items: center;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--primary-text);
  white-space: nowrap;
}

/* What a paid row says to the student who paid for it. Same quiet treatment as
   .free-badge — it is a fact about the row, not something to act on — and it
   is what replaces every Subscribe button on this page once /api/me confirms
   the entitlement. A paid student being shown "Subscribe" on 70 rows is the
   single loudest way this site can suggest their payment did not land. */
.included-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--primary-text);
  white-space: nowrap;
}

/* ── Multi-select filter ─────────────────────────────────────────────────
   A <details> holding checkboxes, not a <select multiple>: the native control
   needs ctrl-click to pick a second value and collapses to an unusable
   scrolling box on a phone, which is most of this audience. */
.multiselect { position: relative; }

.multiselect-summary {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: var(--tap-target);
  padding: 0 16px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--text-main);
  font-family: var(--font-ui); font-size: 0.9rem;
  cursor: pointer;
  /* The default triangle is replaced by the caret span in the markup, which
     can be positioned with the count beside it. */
  list-style: none;
}
.multiselect-summary::-webkit-details-marker { display: none; }
.multiselect[open] .multiselect-summary { border-color: var(--border-hover); }

.multiselect-count {
  font-size: 0.72rem; font-weight: 700;
  padding: 2px 8px; border-radius: var(--radius);
  background: var(--surface-sunken); color: var(--text-muted);
}
/* Anything other than "All" means the list below is filtered, and that has to
   be visible without opening the menu. */
.multiselect-count.is-active { background: var(--primary-glow); color: var(--primary-text); }

.multiselect-menu {
  position: absolute; z-index: 20; top: calc(100% + 6px); left: 0;
  min-width: max(100%, 240px);
  max-height: 320px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
  box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
}

.multiselect-option {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--tap-target);
  padding: 0 8px;
  border-radius: var(--radius);
  font-size: 0.88rem;
  cursor: pointer;
}
.multiselect-option:hover { background: var(--surface-sunken); }
.multiselect-option input { width: 17px; height: 17px; flex-shrink: 0; accent-color: var(--primary-text); }

.multiselect-clear {
  margin-top: 4px;
  align-self: stretch;
  justify-content: center;
}

/* On a phone the menu is the width of the control's parent rather than a
   floating panel wider than the screen. */
@media (max-width: 520px) {
  .multiselect-menu { left: 0; right: 0; min-width: 0; }
}

.locked-banner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; padding: 13px 15px; margin: 12px 0;
  border: 1px dashed var(--accent-amber-line);
  border-radius: var(--radius);
  background: var(--accent-amber-bg);
  color: var(--text-main);
}

/* Subscribe modal and account menu (Task 14). Without these the checkout
   dialog renders as inline text part-way down the page with no overlay — the
   one screen where a student is being asked to pay. */

.modal-host { position: fixed; inset: 0; z-index: 100; }

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 23, 42, 0.55);
}

/* The one genuinely floating layer — it keeps a slightly larger radius and
   the shadow every other surface on the page gives up for a border. */
.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(420px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 24px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--surface-card);
  color: var(--text-main);
  box-shadow: 0 20px 50px rgba(15, 23, 42, 0.35);
}

.modal h2 { margin: 0; }
.modal p { margin: 0; color: var(--text-muted); }

.price-row { display: flex; align-items: baseline; gap: 8px; }

.price {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 700;
  color: var(--primary-text);
}

.promo-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }

.promo-row { display: flex; gap: 8px; }

.promo-row input {
  flex: 1;
  min-width: 0;
  font: inherit;
  min-height: var(--tap-target);
  padding: 10px 12px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text-main);
}

/* Empty until a code is tried, so it must not reserve a blank line. */
.promo-feedback { font-size: 0.82rem; margin: 0; }
.promo-feedback:empty { display: none; }

/* Qualified with .modal, and that is load-bearing: `.modal p` above sets
   --text-muted at specificity (0,1,1), which outranks a bare `.promo-error` at
   (0,1,0). A rejected promo code was therefore printed in exactly the same
   grey as the rest of the dialog — the one line on the checkout screen that
   has to read as a refusal read as a caption. */
.modal .promo-ok { color: var(--primary-text); font-weight: 600; }
.modal .promo-error { color: var(--accent-red); font-weight: 600; }

.cta-btn {
  font: inherit; font-family: var(--font-ui); font-weight: 600;
  /* border-box: same reasoning as .nav-btn above — index.html sets width:
     100% on this at 480px and has no global box-sizing reset. */
  box-sizing: border-box;
  min-height: var(--tap-target); padding: 12px 20px;
  border: none; border-radius: var(--radius);
  /* The landing page's primary CTA is an <a>, not a <button>, so it arrives
     underlined and baseline-aligned unless this rule says otherwise. */
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
  /* --primary-text, not --primary: white on the logo teal is 3.96:1 and the
     label is 16px, which WCAG does not count as large text. One step deeper
     clears AA at 6.03:1 and still reads as the same colour family. */
  background: var(--primary-text); color: #FFFFFF; cursor: pointer;
}
.cta-btn:hover { background: var(--primary-hover); }

/* --primary lightens in dark mode so white-on-primary would stop being
   readable; the dark surface takes the dark ink instead. */
body.dark-mode .cta-btn { color: #0B1A1C; }

.modal-footnote { font-size: 0.75rem; }

/* The sign-in modal. Shares .modal-host / .modal-backdrop / .modal with the
   subscribe dialog above; everything below is the parts a form needs and a
   checkout dialog did not. */

.auth-modal .auth-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }

.auth-input {
  font: inherit;
  width: 100%;
  box-sizing: border-box;
  min-height: var(--tap-target);
  padding: 10px 12px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text-main);
}

.auth-google { width: 100%; }

/* A rule with the word "or" sitting on it. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--border-color);
}

.auth-hint { font-size: 0.78rem; color: var(--text-muted); }
.auth-footnote { font-size: 0.82rem; }

/* Empty between attempts, so it must not reserve a blank line — same
   reasoning as .promo-feedback above. */
.auth-error { font-size: 0.85rem; color: var(--accent-red); font-weight: 600; }
.auth-error:empty { display: none; }

/* The eye on a password field. Inside the wrapper rather than beside it, so
   the input keeps the full width every other field in the form has and the
   labels above them stay on one grid. */
.auth-password-wrap { position: relative; display: flex; align-items: center; }
.auth-password-wrap .auth-input { padding-right: 68px; }

.auth-eye {
  position: absolute;
  right: 6px;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  /* Full height of the field: the tap target is the strip, not the word. */
  min-height: calc(var(--tap-target) - 8px);
  padding: 0 10px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--text-muted);
  cursor: pointer;
}
.auth-eye:hover { color: var(--primary-text); border-color: var(--border-hover); }
.auth-eye[aria-pressed="true"] { color: var(--primary-text); }

/* ── Waiting ─────────────────────────────────────────────────────────────
   Every wait a student sits through on the checkout path gets one of these.
   currentColor, so the same element reads correctly on a button, in the
   feedback line and on a card. */
.spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* The spinner is followed immediately by its label in both places it appears,
   and neither container supplies the space: .cta-btn is a flex row with no
   gap, and the feedback line is a paragraph where the two are adjacent
   inlines. */
.cta-btn .spinner { margin-right: 8px; }
.promo-feedback .spinner { margin-right: 6px; }

/* A wait is neither a success nor a failure, so it takes neither colour.
   Qualified for the same reason as .promo-error above. */
.modal .promo-pending { color: var(--text-muted); }

/* The list price beside the discounted one. Muted and struck through: the
   number a student is being asked for is the big one next to it. */
.price-was {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: line-through;
}

.modal-loading { align-items: center; text-align: center; padding: 32px 24px; }
.modal-loading .spinner { font-size: 1.8rem; color: var(--primary-text); }
.modal-loading-text { margin: 0; }

/* The sign-out confirmation opens on top of the sign-in modal, which is the
   thing that can have opened it. */
.modal-host-top { z-index: 110; }

/* Sign-out is the one action whose result is invisible — the page reloads and
   nothing on it says what changed. */
.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  z-index: 120;
  transform: translate(-50%, 12px);
  max-width: calc(100vw - 32px);
  padding: 12px 18px;
  border-radius: var(--radius);
  background: var(--text-main);
  color: var(--surface-card);
  font-size: 0.88rem;
  font-weight: 500;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.3);
  opacity: 0;
  transition: opacity 0.25s, transform 0.25s;
}
.toast.is-open { opacity: 1; transform: translate(-50%, 0); }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2s; }
  .toast { transition: none; }
}

/* A button that has to read as a link: "Forgot password?" inside a dialog
   must not compete with the primary action next to it. */
.link-btn {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--primary-text);
  text-decoration: underline;
  cursor: pointer;
}

.account-menu {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Was .account-email, when the bar printed the address. It greets the student
   by first name now, and the address rides along as the title.

   Now also the account page's one nav entry point (Task 10): an <a>, not a
   <button>, because it only ever navigates. color: inherit + no underline
   keeps it reading as the same bold greeting it always was rather than a
   default blue link; the hover rule is .topbar-link's own convention, so it
   still signals "clickable" without adding a visible affordance that costs
   width in the ≤520px budget below. */
.account-name { font-weight: 600; white-space: nowrap; color: inherit; text-decoration: none; }
.account-name:hover { color: var(--primary-text); }

.account-status {
  font-size: 0.72rem;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: var(--radius);
  background: var(--primary-glow);
  color: var(--primary-text);
}

.empty-state {
  padding: 32px 16px;
  text-align: center;
  color: var(--text-muted);
}

.skeleton {
  height: 14px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border-color) 50%, var(--surface-sunken) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.2s infinite;
}

@keyframes shimmer {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

.error-card {
  border: 1px solid var(--accent-red);
  background: var(--accent-red-bg);
  border-radius: var(--radius);
  padding: 16px;
  max-width: 520px;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton, .option-card.user-incorrect.shake { animation: none; }
}

/* Defensive floor: every interactive control stays at least 44px tall at
   phone widths, whatever page-specific sizing tries to shrink it to. */
@media (max-width: 480px) {
  .nav-btn, .cta-btn, .lang-btn, .unit-btn, .option-card, .theme-toggle-btn {
    min-height: var(--tap-target);
  }
}

/* ── Lead gate ─────────────────────────────────────────────────────────────
   Reuses .modal / .auth-input / .auth-label wholesale; only the consent row
   and the two buttons need anything of their own. */
.lead-consent {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  font-size: 0.88rem;
  line-height: 1.45;
  color: var(--text-main);
  cursor: pointer;
}

/* Large enough to hit on a phone, and never shrunk by the flex row. */
.lead-consent input[type="checkbox"] {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.1rem;
  accent-color: var(--primary);
}

.lead-modal .cta-btn,
.lead-modal .nav-btn {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* "Not now" must read as a real way out, not a greyed-off afterthought — the
   gate is a prompt and declining it is a supported answer, not a failure. */
.lead-modal .nav-btn { color: var(--text-main); }

/* ── Share ─────────────────────────────────────────────────────────────── */

/* Shared by every surface that offers "share this with a friend": the landing
   page band, the analysis hub card, the site footer, the reader's Display
   popover. It lives in app.css rather than in site.css because it is the one
   stylesheet all four of those pages already load — reader.css and hub.css
   would each need their own copy otherwise, and three copies of a button is
   three buttons that drift.

   Every colour comes from tokens.css, including the brand destinations. A
   WhatsApp green and a Telegram blue here would be the only two colours on the
   site chosen by somebody else's marketing department, and they read as ads.
   The marks are drawn in currentColor and identified by their silhouette. */
.share-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* Square, sized to the tap target, holding a glyph rather than a name. The
   labels are gone from the face of the button but not from the button: title
   and aria-label carry them, so hover, screen readers and search-in-page all
   still find "WhatsApp". */
.share-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  /* The anchors and the button must measure the same. Only site.css sets a
     global box-sizing, and these render on pages that do not load it. */
  box-sizing: border-box;
  width: var(--tap-target);
  height: var(--tap-target);
  padding: 0;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--text-main);
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.share-link svg {
  width: 20px;
  height: 20px;
  display: block;
}

.share-link:hover {
  border-color: var(--primary);
  color: var(--primary-text);
  background: var(--primary-glow);
}

/* The confirmation lives on the button that was pressed rather than in a toast:
   it is the only place the eye is already looking. The glyph itself flips to a
   tick in share.js; this is the fill behind it. */
.share-copy.is-done {
  border-color: var(--primary);
  color: var(--primary-text);
  background: var(--primary-glow);
}

/* The words behind the tick. Empty for all but a couple of seconds, and it
   must take no space while it is — an empty flex item still eats the row's
   gap. */
.share-status {
  /* Its own line under the marks, in both hosts: flex-basis 100% forces the
     wrap. Sitting inline it would shove the row sideways for two seconds every
     time somebody copies, and on a failure it prints a whole URL. */
  flex: 1 0 100%;
  margin-top: 2px;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--primary-text);
  overflow-wrap: anywhere;
}

.share-status:empty {
  display: none;
}

/* The fallback popover, for the desktop browsers with no navigator.share.
   position: fixed and pinned by script — several hosts (the footer, the
   reader toolbar) clip or scroll their children. */
.share-pop {
  position: fixed;
  z-index: 1200;
  width: max-content;
  max-width: min(320px, calc(100vw - 16px));
  padding: 14px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  background: var(--surface-card);
  box-shadow: var(--shadow-lg);
}

.share-pop[hidden] {
  display: none;
}

.share-pop-title {
  margin: 0 0 10px;
  font-family: var(--font-ui);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Four squares fit one row inside the popover, so the stacked column the text
   buttons needed is gone with them and the base .share-links rule is enough. */

/* The device manager. Rendered in two places from one markup builder — the
   modal a blocked reader opens, and the block on the account page — so the
   styles are written against the list rather than against either container. */

.device-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.device-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface-card);
}

.device-name {
  font-weight: 600;
  color: var(--text-main);
}

/* The one label that has to be findable at a glance: without it a student
   cannot tell which row is the browser they are sitting in, and the first
   thing they do is sign themselves out of it. */
.device-current {
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--primary-glow);
  color: var(--primary-text);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.device-meta {
  margin-top: 2px;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.device-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.device-rename {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  width: 100%;
}

.device-rename-input {
  flex: 1 1 140px;
  min-width: 0;
  padding: 7px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--text-main);
  font: inherit;
}

.device-note,
.device-busy {
  margin: 10px 0 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.device-empty {
  margin: 0;
  color: var(--text-muted);
}

.device-error {
  margin: 0 0 10px;
  padding: 10px 12px;
  border: 1px solid var(--accent-amber-line);
  border-radius: var(--radius);
  background: var(--accent-amber-bg);
  color: var(--text-main);
}

/* On a phone the row stacks, and the buttons go full width rather than
   crushing the name they belong to. */
@media (max-width: 480px) {
  .device-row { flex-direction: column; align-items: stretch; }
  .device-actions { justify-content: flex-end; }
}

/* The loss warning on the hand-off screen. Amber rather than red: signing a
   device out remotely is a legitimate escape hatch for a lost phone, not an
   error — but it is the one path that can drop work, so it must not read like
   ordinary body copy either. */
.device-warn {
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--accent-amber-line);
  border-radius: var(--radius);
  background: var(--accent-amber-bg);
  color: var(--text-main);
  font-size: 0.9375rem;
}

/* The armed state of a Remove button, once it is asking rather than telling.
   Red only at this point: the first press is a normal control, and the second
   is the one that ends another device's unsaved work. */
.nav-btn.device-danger {
  border-color: var(--accent-red-line);
  color: var(--accent-red);
  font-weight: 600;
}


/* ── The hand-off screen ───────────────────────────────────────────────────
   Shown when a subscription is already open somewhere else. Its job is to make
   one trade-off legible: the calm route saves everything and happens on the
   other device, the immediate route costs whatever that device has not saved.
   The layout carries that — the recommended path reads as prose because it has
   no button to offer, and the button that does exist sits below a labelled
   divider so it is plainly the second choice rather than the obvious one. */

.modal.handoff { gap: 0; }

/* Three sizes across the whole dialog — title, body, footnote — and every one
   of them in rem, so a phone set to large text scales all three together. It
   had six, which is what made a short screen read as a wall. */
.handoff-title {
  margin: 0 0 4px;
  font-size: 1.125rem;
  line-height: 1.3;
}

.handoff-lead {
  margin: 0 0 16px;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* The other device, set as a physical object rather than a list row: recessed,
   so it reads as the thing being described instead of a control. */
.handoff-device {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  margin-bottom: 20px;
  border: 1px solid var(--border-color);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

.handoff-device-name {
  font-weight: 600;
  color: var(--text-main);
}

.handoff-device-meta {
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.handoff-best h3 {
  margin: 0 0 6px;
  font-size: 0.9375rem;
  color: var(--text-main);
}

.handoff-best p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* The live line. It is a promise that the instruction above will be noticed, so
   it gets the accent colour and the only movement on the screen. */
.handoff-watching {
  display: flex;
  /* flex-start, not center: the line wraps on a phone, and centring pushed the
     dot to the middle of two lines where it read as a stray bullet. */
  align-items: flex-start;
  gap: 8px;
  margin-top: 12px !important;
  color: var(--primary-text) !important;
  font-size: 0.9375rem !important;
  font-weight: 500;
}

.handoff-pulse {
  flex-shrink: 0;
  /* Nudged down to sit on the first line's optical centre. */
  margin-top: 6px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--primary);
  animation: handoff-pulse 1.8s ease-in-out infinite;
}

.handoff-pulse-done { animation: none; }

@keyframes handoff-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.35; transform: scale(0.8); }
}

/* Somebody who has asked for less movement gets a steady dot, not a still one
   that looks broken. */
@media (prefers-reduced-motion: reduce) {
  .handoff-pulse { animation: none; }
}

/* A labelled rule, because the label is the branch: everything below it applies
   only to the student who cannot reach their other device. */
.handoff-or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 24px 0 16px;
  color: var(--text-faint);
  font-size: 0.8125rem;
}

.handoff-or::before,
.handoff-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border-color);
}

.handoff-take { display: flex; flex-direction: column; gap: 8px; }

.handoff-take .cta-btn { width: 100%; }

.handoff-cost {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.8125rem;
}

/* Once the button is armed the cost stops being a footnote. */
.handoff-cost-armed {
  color: var(--accent-red);
  font-weight: 500;
}

/* .nav-btn.handoff-close, not .handoff-close: the button carries both classes
   and .nav-btn is declared later in this file, so a single-class rule lost the
   border and background it was meant to strip. Dismissing is not an action with
   a consequence and should not look like one. */
.nav-btn.handoff-close {
  margin-top: 18px;
  align-self: center;
  padding: 6px 10px;
  background: none;
  border: none;
  color: var(--text-muted);
  font-weight: 400;
}

.nav-btn.handoff-close:hover {
  background: none;
  color: var(--text-main);
  text-decoration: underline;
}
