﻿/* ============================================================ tokens ===== */

:root {
  color-scheme: light;

  --plane: #f9f9f7;
  --surface-1: #fcfcfb;
  --surface-2: #f2f1ed;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  /*
   * Darkened from #898781 on 19 August 2026. That value is 3.41:1 on --plane,
   * 3.50:1 on --surface-1 and 3.18:1 on --surface-2, against a 4.5:1
   * requirement, and it is the colour of 57 declarations - every timestamp,
   * hint, placeholder, prompt label and "no translation yet" in the product.
   * The whole secondary layer of the light theme was below the line.
   *
   * It reads as a deliberate grey rather than a faded one, which is what it
   * was always meant to be. The dark theme keeps #898781: on a dark plane the
   * same value is 5.41:1 and already passes, which is why this went unseen by
   * anyone working in dark mode.
   */
  --muted: #6c6a65;
  --grid: #e1e0d9;
  --axis: #c3c2b7;
  --border: rgba(11, 11, 11, 0.1);
  --ring: rgba(42, 120, 214, 0.35);

  /* categorical slots 1-2 (validated all-pairs, light) */
  --series-1: #2a78d6;
  --series-2: #eb6834;
  /* meter track: a lighter step of the fill's own blue ramp (blue-100) */
  --meter-track: #cde2fb;

  /* the wall: the one panel that asks for something, tinted off the accent so
   * it does not read as one more card to scroll past. Defined in all three
   * token blocks, never only in a dark one. */
  --wall-bg: #eef4fd;
  --wall-border: rgba(42, 120, 214, 0.28);

  /* status palette - reserved, always shipped with an icon + label */
  --good: #0ca30c;
  --warning: #fab219;
  /*
   * The same amber, dark enough to read as text.
   *
   * #fab219 is 1.74:1 on the page. It is fine as a fill - a swatch, a bar, a
   * dot - and it was also being used for the "Moderate" figure on the session
   * summary and for the number that flies up when a card is rated, both of
   * which are text and both of which were illegible in the light theme.
   *
   * --success-text already existed for exactly this reason. This is its pair,
   * and the rule is the same: --warning paints, --warning-text is read.
   */
  --warning-text: #8a5a00;
  --critical: #d03b3b;
  --success-text: #006300;

  --shadow: 0 1px 2px rgba(11, 11, 11, 0.05), 0 8px 24px rgba(11, 11, 11, 0.04);
  --radius: 14px;
  --radius-sm: 9px;

  --font: system-ui, -apple-system, 'Segoe UI', sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme='light'])) {
    color-scheme: dark;
    --plane: #0d0d0d;
    --surface-1: #1a1a19;
    --surface-2: #232322;
    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --muted: #898781;
    --grid: #2c2c2a;
    --axis: #383835;
    --border: rgba(255, 255, 255, 0.1);
    --ring: rgba(57, 135, 229, 0.45);
    --series-1: #3987e5;
    --series-2: #d95926;
    --meter-track: #0d366b; /* blue-700: recedes against the dark surface */
    --wall-bg: #142335;
    --wall-border: rgba(57, 135, 229, 0.32);
    --success-text: #0ca30c;
    --warning-text: #fab219;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3);
  }
}

:root[data-theme='dark'] {
  color-scheme: dark;
  --plane: #0d0d0d;
  --surface-1: #1a1a19;
  --surface-2: #232322;
  --text-primary: #ffffff;
  --text-secondary: #c3c2b7;
  --muted: #898781;
  --grid: #2c2c2a;
  --axis: #383835;
  --border: rgba(255, 255, 255, 0.1);
  --ring: rgba(57, 135, 229, 0.45);
  --series-1: #3987e5;
  --series-2: #d95926;
  --meter-track: #0d366b; /* blue-700: recedes against the dark surface */
  --wall-bg: #142335;
  --wall-border: rgba(57, 135, 229, 0.32);
  --success-text: #0ca30c;
  --warning-text: #fab219;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3);
}

/* ============================================================== base ===== */

* {
  box-sizing: border-box;
}

/* component classes below set display, which would otherwise beat the UA's
 * [hidden] rule and leave "hidden" elements on screen */
[hidden] {
  display: none !important;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--plane);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.01em;
}

h1 {
  font-size: 1.4rem;
}
h2 {
  font-size: 1.05rem;
}
h3 {
  font-size: 0.9rem;
}

a {
  color: inherit;
  text-decoration: none;
}

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

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/*
 * Off screen until it is tabbed to, then a real button in the top left.
 *
 * Not .sr-only, which cannot be revealed: clip and a 1px box mean that even
 * when focused there is nothing to show, so a sighted keyboard user gets a
 * focus ring around nothing and no idea what is selected. This moves instead
 * of hiding, which is the difference between a skip link that works for
 * everybody and one that only works for screen readers.
 */
.skip-link {
  position: absolute;
  left: 8px;
  top: -60px;
  z-index: 100;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-primary);
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}

.skip-link:focus {
  top: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .skip-link {
    transition: none;
  }
}

/* ============================================================ topbar ===== */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--plane) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.topbar__inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  gap: 18px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 600;
  white-space: nowrap;
}

/* The name and its qualifier, as one lockup.

   The qualifier is the half that separates this from every other product that
   could reasonably be called Caminantes, so it is shown wherever there is room
   and dropped where there is not, rather than being shortened to something
   that says less on every screen. */
.brand__name {
  font-weight: 600;
}

/* The colon lives in the markup, inside the qualifier, so it leaves with it
   rather than sitting at the end of the name pointing at nothing - and so the
   accessible name and anything copied off the page read as the real name
   rather than as two words run together. A CSS ::before looked identical and
   was neither of those things.

   HIDDEN BY DEFAULT AND SHOWN WHEN THERE IS ROOM, not the other way round, and
   that direction is the whole safety of it. The topbar is a flex row with the
   tab strip beside it, so a qualifier that has to be squeezed out under
   pressure takes the tabs with it on the way. Written this way the failure
   mode is that the page shows less of the name, never that the bar breaks.

   1180px is a judgement, not a measurement: the brand is 276px with the
   qualifier and the tab strip needs about 440, which leaves room at 1120 (the
   content cap) with the actions beside them. It was NOT checked in a narrow
   window - the automated browser here renders at a fixed 1280 and ignores a
   resize - so if the bar ever looks tight between roughly 1100 and 1200, this
   number is the thing to move. */
.brand__tag {
  display: none;
  font-weight: 400;
  color: var(--text-secondary);
}

@media (min-width: 1180px) {
  .brand__tag { display: inline; }
}

.brand__mark {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
}

.brand__mark svg {
  display: block;
  border-radius: 6px;
}

.tabs {
  display: flex;
  gap: 2px;
  margin-left: auto;
  background: var(--surface-2);
  padding: 3px;
  border-radius: 11px;
}

.tab {
  padding: 6px 14px;
  border-radius: 8px;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-secondary);
  transition: background 0.12s, color 0.12s;
}

.tab:hover {
  color: var(--text-primary);
}

.tab[aria-current='page'] {
  background: var(--surface-1);
  color: var(--text-primary);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.topbar__actions {
  display: flex;
  gap: 6px;
  align-items: center;
}

/* ------ who is looking ------
 *
 * index.html is one document served to everyone, because the server does not
 * know who is asking until app.js has been and asked. So both navs are in the
 * markup and one of them is switched off: is-anon lands on <body> during boot,
 * before the first render, so nothing flickers between the two states.
 *
 * Both rules are written as a descendant of body, and that is not decoration.
 * A bare `.anon-only { display: none }` lost to `.btn`, which also sets
 * display and is defined later in this file, so the Sign in button stayed on
 * screen for signed-in users. Two class selectors beat one whatever the order.
 *
 * Hiding is the polite half. The router refuses the same views independently
 * in currentView(), because a hash is a URL anyone can type, and every write
 * refuses again in refuseAnonymous(). */
body:not(.is-anon) .anon-only { display: none; }
body.is-anon .account-only { display: none; }

/* ------ level badge (always visible in the top bar) ------ */

.levelbadge {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 4px 11px 4px 9px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-1);
  white-space: nowrap;
  transition: border-color 0.12s, background 0.12s;
}

.levelbadge:hover {
  border-color: var(--series-1);
  background: var(--surface-2);
}

.levelbadge__code {
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--series-1);
}

.levelbadge__meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.levelbadge__count {
  font-size: 0.66rem;
  color: var(--muted);
  line-height: 1;
}

.levelbadge__track {
  display: block;
  width: 74px;
  height: 4px;
  border-radius: 999px;
  background: var(--grid);
  overflow: hidden;
}

.levelbadge__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--series-1);
  transition: width 0.35s ease;
}

@media (max-width: 860px) {
  .levelbadge__meta {
    display: none;
  }
}

/*
 * The tab row does not fit on a phone and never did; adding Map made seven of
 * them and pushed it over on any handset. A flex row that cannot wrap forces
 * the whole document wider than the viewport, so the browser zooms the page
 * out and every other element shrinks with it.
 *
 * Below 760px the tabs drop to their own full-width row and scroll sideways
 * inside it, so the strip absorbs the overflow instead of the page.
 * navigate() scrolls the current tab into view, because a tab strip whose
 * selected tab is off-screen is worse than one that does not fit.
 */
@media (max-width: 760px) {
  .topbar__inner {
    flex-wrap: wrap;
    gap: 10px;
    padding: 8px 12px;
  }

  .tabs {
    order: 3;
    width: 100%;
    margin-left: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .tabs::-webkit-scrollbar {
    display: none;
  }

  .tab {
    flex: none;
    white-space: nowrap;
  }

  /* Was pushed right by the tabs' margin-left; now it has to hold its own. */
  .topbar__actions {
    margin-left: auto;
  }
}

/* ------ level card on the stats page ------ */

.levelcard__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.levelcard__title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 2px;
}

.levelcard__code {
  font-size: 2.1rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.levelcard__name {
  color: var(--text-secondary);
  font-size: 0.95rem;
}

.levelcard__blurb {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-size: 0.88rem;
  max-width: 46ch;
  line-height: 1.6;
}

.levelcard__count {
  text-align: right;
  flex: none;
}

/* Its own size rather than an inline style, and tabular figures so the number
   does not shift the caption under it as it grows. */
.levelcard__figure {
  font-size: 2.3rem;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.levelcard__note {
  margin: 14px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
  max-width: 62ch;
}

/* If it does wrap on a narrow screen, it lines up with everything else rather
   than sitting right-aligned over nothing. */
@media (max-width: 640px) {
  .levelcard__count {
    text-align: left;
  }
}

.levelcard__progress {
  margin-top: 16px;
}

.levelcard__progress-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: 0.83rem;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

/* the whole ladder, so the next rung is always visible */
.levelscale {
  display: flex;
  gap: 4px;
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  overflow-x: auto;
}

.levelscale__step {
  flex: 1;
  min-width: 58px;
  text-align: center;
  padding: 7px 4px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  opacity: 0.55;
}

.levelscale__step--done {
  opacity: 1;
  border-color: transparent;
  background: color-mix(in srgb, var(--series-1) 16%, transparent);
}

.levelscale__step--now {
  opacity: 1;
  border-color: var(--series-1);
  background: color-mix(in srgb, var(--series-1) 12%, transparent);
}

.levelscale__code {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
}

.levelscale__at {
  display: block;
  font-size: 0.66rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.icon-btn {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-secondary);
  border-radius: 9px;
  cursor: pointer;
  font-size: 15px;
  font-family: inherit;
}

.icon-btn:hover {
  color: var(--text-primary);
}

/* =============================================================== app ===== */

.app {
  max-width: 1120px;
  margin: 0 auto;
  padding: 26px 20px 90px;
}

.loading {
  padding: 60px 0;
  text-align: center;
  color: var(--muted);
}

/*
 * The server-rendered page, which is what stands here until app.js boots and
 * what a crawler keeps. See seo.js.
 *
 * Styled at all because it is now the first paint - it replaced a centred
 * "Loading your words" - and unstyled markup flashing before the app is worse
 * than the spinner it replaced. Kept plain on purpose though: it is a
 * stand-in, and making it look like a designed screen would only make the
 * swap to the real one more jarring.
 */
.seo-fallback {
  padding: 24px 0 40px;
  max-width: 70ch;
}

.seo-fallback h1 {
  margin: 0 0 10px;
  font-size: 1.6rem;
}

.seo-fallback h2 {
  margin: 28px 0 8px;
  font-size: 1.1rem;
}

.seo-fallback p {
  margin: 0 0 12px;
  color: var(--muted);
  line-height: 1.6;
}

.seo-fallback ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 6px;
}

.seo-fallback__words {
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.seo-fallback__shelf li span {
  display: block;
  color: var(--muted);
  font-size: 0.85rem;
}

.view-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.view-head p {
  margin: 4px 0 0;
  color: var(--text-secondary);
  font-size: 0.875rem;
}

.card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 18px 20px;
}

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.card__sub {
  color: var(--muted);
  font-size: 0.8rem;
  margin: 2px 0 0;
}

/* ---------------------------------------------------------------- account ---
   The one screen where the destructive action lives. The card carries a red
   left edge rather than a red fill: a whole panel in the critical colour reads
   as an error that has already happened, when what it is is a door. */
.card--danger {
  border-left: 3px solid var(--critical);
}

.card--danger h2 {
  color: var(--critical);
}

.account__email {
  font-weight: 600;
  margin: 0 0 4px;
  word-break: break-all;
}

.account__actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
}

.account__delete {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.account__label {
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--text-secondary);
  margin-top: 8px;
}

.account__input {
  font: inherit;
  color: var(--text-primary);
  background: var(--plane);
  border: 1px solid var(--axis);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  /* Wide enough for an email address, not so wide it invites a paragraph. */
  max-width: 22rem;
}

.account__input:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}

.account__delete .btn {
  align-self: flex-start;
  margin-top: 14px;
}

.account__error {
  color: var(--critical);
  font-size: 0.85rem;
  margin: 12px 0 0;
}

/* Deliberately not the critical colour. This appears when a Google
   confirmation SUCCEEDED and the deletion has not happened yet, so red would
   say the opposite of what is true. */
.account__notice {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-left: 3px solid var(--accent, #aa151b);
  background: rgba(255, 255, 255, 0.04);
  font-size: 0.85rem;
  line-height: 1.45;
}

/* The second route to the same proof, kept visually subordinate to the red
   button so it does not read as an easier way to delete something. */
.account__delete-alt {
  margin-top: 14px;
}

.btn--quiet {
  margin-left: 6px;
  padding: 2px 10px;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.18));
  background: none;
  font-size: 0.8125rem;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px;
}

/* =========================================================== controls ==== */

button,
input,
select,
textarea {
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--text-primary);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 36px;
  padding: 0 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-primary);
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s, opacity 0.12s;
  white-space: nowrap;
}

.btn:hover:not(:disabled) {
  background: var(--surface-2);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--series-1);
  border-color: transparent;
  color: #fff;
}

.btn--primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--series-1) 88%, #000);
}

.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-secondary);
}

.btn--ghost:hover:not(:disabled) {
  background: var(--surface-2);
  color: var(--text-primary);
}

.btn--sm {
  min-height: 30px;
  padding: 0 11px;
  font-size: 0.82rem;
}

.btn--danger {
  color: var(--critical);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.field > label {
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--text-secondary);
}

.input,
select.input,
textarea.input {
  width: 100%;
  min-height: 38px;
  padding: 8px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  transition: border-color 0.12s, box-shadow 0.12s;
}

.input:focus {
  outline: none;
  border-color: var(--series-1);
  box-shadow: 0 0 0 3px var(--ring);
}

.input::placeholder {
  color: var(--muted);
}

textarea.input {
  min-height: 88px;
  resize: vertical;
  line-height: 1.55;
}

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

.seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--surface-2);
  border-radius: 10px;
}

.seg button {
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  padding: 5px 12px;
  border-radius: 7px;
  cursor: pointer;
  font-size: 0.82rem;
  font-weight: 500;
}

.seg button[aria-pressed='true'] {
  background: var(--surface-1);
  color: var(--text-primary);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

/* ==================================================== confidence chips === */
/*
 * Confidence is a status scale (needs work / getting there / solid), so it uses
 * the reserved status palette. Colour never carries it alone - every chip and
 * every chart segment ships an icon and a text label.
 */

.conf-set {
  display: inline-flex;
  gap: 6px;
  flex-wrap: wrap;
}

.conf-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 11px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-secondary);
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s, color 0.12s;
}

.conf-btn .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}

.conf-btn .glyph {
  font-size: 0.95em;
  line-height: 1;
}

.conf-btn:hover {
  color: var(--text-primary);
  background: var(--surface-2);
}

.conf-btn[aria-pressed='true'] {
  color: var(--text-primary);
  background: var(--surface-2);
  border-color: currentColor;
}

.conf-0 .dot,
.dot--conf-0 {
  background: var(--critical);
}
.conf-1 .dot,
.dot--conf-1 {
  background: var(--warning);
}
.conf-2 .dot,
.dot--conf-2 {
  background: var(--good);
}

.conf-btn.conf-0[aria-pressed='true'] {
  border-color: var(--critical);
  box-shadow: inset 0 0 0 1px var(--critical);
}
.conf-btn.conf-1[aria-pressed='true'] {
  border-color: var(--warning);
  box-shadow: inset 0 0 0 1px var(--warning);
}
.conf-btn.conf-2[aria-pressed='true'] {
  border-color: var(--good);
  box-shadow: inset 0 0 0 1px var(--good);
}

.conf-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: var(--text-secondary);
}

.conf-badge .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

/* ========================================================== add form ===== */

.addbar {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 12px;
  align-items: end;
}

@media (max-width: 720px) {
  .addbar {
    grid-template-columns: 1fr;
  }
}

.addbar__actions {
  display: flex;
  gap: 8px;
}

/* accent bar - inserts Spanish characters a US keyboard can't type directly */

.accentbar {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
  margin-top: 10px;
}

.accentbar__label {
  font-size: 0.75rem;
  color: var(--muted);
  margin-right: 2px;
}

.accent-key {
  min-width: 32px;
  height: 30px;
  padding: 0 7px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface-1);
  color: var(--text-primary);
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.1s, border-color 0.1s, transform 0.06s;
}

.accent-key:hover {
  background: var(--surface-2);
  border-color: var(--axis);
}

.accent-key:active {
  transform: translateY(1px);
}

.accent-key--target {
  border-style: dashed;
  color: var(--muted);
  cursor: default;
  font-size: 0.75rem;
  padding: 0 9px;
}

.suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: 10px;
  font-size: 0.8rem;
  color: var(--muted);
}

.chip {
  border: 1px dashed var(--border);
  background: transparent;
  color: var(--text-secondary);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}

.chip:hover {
  border-style: solid;
  color: var(--text-primary);
  background: var(--surface-2);
}

/* ========================================================== word list ==== */

.toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.toolbar .input {
  max-width: 260px;
}

.toolbar__spacer {
  margin-left: auto;
}

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

.word {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid transparent;
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px 16px;
  align-items: center;
}

/* Never met. Deliberately not --critical: a word the user has not been shown
   is not a word they are failing, and an edge of warning red on every
   untouched row is the list calling 108 curated words a problem. */
.word--new {
  border-left-color: var(--axis);
}

.word--conf-0 {
  border-left-color: var(--critical);
}
.word--conf-1 {
  border-left-color: var(--warning);
}
.word--conf-2 {
  border-left-color: var(--good);
}

.word__text {
  min-width: 0;
}

.word__es {
  font-size: 1rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.word__en {
  color: var(--text-secondary);
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

.word__meta {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 4px;
  font-size: 0.73rem;
  color: var(--muted);
}

.word__side {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.word__tools {
  display: flex;
  gap: 2px;
}

@media (max-width: 640px) {
  .word {
    grid-template-columns: 1fr;
  }
  .word__side {
    justify-content: flex-start;
  }
}

.empty {
  text-align: center;
  padding: 46px 20px;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}

.empty strong {
  display: block;
  color: var(--text-primary);
  font-size: 1rem;
  margin-bottom: 6px;
}

/* ====================================================== pronunciation ==== */

.phonetic {
  font-size: 0.78rem;
  color: var(--muted);
  letter-spacing: 0.02em;
  margin-top: 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.phonetic--card {
  justify-content: center;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  margin: -4px 0 0;
}

.speak-btn {
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 0.85em;
  line-height: 1;
  padding: 4px;
  border-radius: 6px;
  opacity: 0.65;
  vertical-align: middle;
  transition: opacity 0.12s, background 0.12s;
}

.speak-btn:hover {
  opacity: 1;
  background: var(--surface-2);
}

.speak-btn--lg {
  font-size: 1.05rem;
  padding: 6px;
}

/* ============================================================== read ===== */

.reader-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 330px;
  gap: 18px;
  align-items: start;
}

@media (max-width: 940px) {
  .reader-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

.reader-side {
  display: flex;
  flex-direction: column;
  gap: 14px;
  position: sticky;
  top: 74px;
}

@media (max-width: 940px) {
  .reader-side {
    position: static;
  }
}

.reader-text {
  font-size: 1.08rem;
  line-height: 2.05;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/*
 * Tokens keep text-coloured ink and carry state in a tinted wash plus a solid
 * underline, so the passage stays readable and the colour is never the only cue
 * (legend above, title on hover, full state named in the side panel on click).
 */
.tok {
  font: inherit;
  color: inherit;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 4px 4px 0 0;
  padding: 1px 3px;
  margin: 0 -1px;
  cursor: pointer;
  background: transparent;
  transition: background 0.1s;
}

.tok:hover {
  filter: brightness(1.08);
}

.tok--conf-0 {
  border-bottom-color: var(--critical);
  background: color-mix(in srgb, var(--critical) 15%, transparent);
}

.tok--conf-1 {
  border-bottom-color: var(--warning);
  background: color-mix(in srgb, var(--warning) 22%, transparent);
}

.tok--conf-2 {
  border-bottom-color: var(--good);
  background: color-mix(in srgb, var(--good) 15%, transparent);
}

.tok--new {
  border-bottom-color: var(--series-1);
  background: color-mix(in srgb, var(--series-1) 15%, transparent);
}

.tok--selected {
  outline: 2px solid var(--text-primary);
  outline-offset: 1px;
}

/* a proven word-to-word link between the two panes */
.tok--linked,
.entok--linked {
  outline: 2px solid var(--series-2);
  outline-offset: 1px;
  border-radius: 4px;
}

/* the weaker claim: we only know which sentence it came from */
.tok--sentence,
.en-sentence--active {
  background: color-mix(in srgb, var(--series-2) 14%, transparent);
  border-radius: 4px;
}

/* ---- the English pane ---- */

.en-pane {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--grid);
}

.en-pane__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}

.en-pane__title {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
}

.reader-text--en {
  font-size: 0.98rem;
  line-height: 1.9;
  color: var(--text-secondary);
}

.entok {
  font: inherit;
  color: inherit;
  border: 0;
  background: transparent;
  padding: 1px 2px;
  margin: 0 -1px;
  border-radius: 4px;
  cursor: pointer;
}

.entok:hover {
  background: var(--surface-2);
  color: var(--text-primary);
}

.en-missing {
  color: var(--muted);
  font-style: italic;
}

#reader-input {
  min-height: 130px;
}

.genbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--grid);
}

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

.mix-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
}

.mix-counts b {
  color: var(--text-primary);
}

.newword-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 230px;
  overflow-y: auto;
}

.newword {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  border-bottom: 1px solid var(--grid);
}

.newword:last-child {
  border-bottom: 0;
}

.newword__text {
  flex: 1;
  min-width: 0;
  font-size: 0.88rem;
}

.newword__text .phonetic {
  margin-top: 0;
}

.newword__gloss {
  color: var(--text-secondary);
  font-size: 0.82rem;
}

/* ============================================================ grammar ==== */

.gchips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin: 6px 0 2px;
  justify-content: inherit;
}

.gchip {
  font-size: 0.72rem;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  background: var(--surface-2);
  white-space: nowrap;
}

/* an inferred fact is drawn as a hedge, not as a statement */
.gchip--guess {
  border-style: dashed;
  color: var(--muted);
  background: transparent;
}

.gchip--guess span {
  opacity: 0.7;
  margin-left: 1px;
}

.gchip--soft {
  background: transparent;
  color: var(--muted);
  border-color: transparent;
  padding-left: 2px;
}

.flashcard .gchips {
  justify-content: center;
}

.activity {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-top: 10px;
  font-size: 0.73rem;
  color: var(--muted);
}

.flashcard .activity {
  justify-content: center;
  margin-top: 6px;
}

/* ============================================================ numbers ==== */

.numbar {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  align-items: end;
}

@media (max-width: 560px) {
  .numbar {
    grid-template-columns: 1fr;
  }
}

.num-preview {
  margin-top: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.num-preview__row {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.num-preview__value {
  font-size: 2rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}

.num-preview__text {
  min-width: 0;
}

.num-preview__es {
  font-size: 1.05rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 4px;
}

.numgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}

.numcard {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-top: 3px solid transparent;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.numcard--conf-0 {
  border-top-color: var(--critical);
}
.numcard--conf-1 {
  border-top-color: var(--warning);
}
.numcard--conf-2 {
  border-top-color: var(--good);
}

.numcard__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.numcard__value {
  font-size: 1.9rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.numcard__es {
  font-size: 1rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.numcard__en {
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 8px;
}

.numcard .conf-set {
  gap: 4px;
}

.numcard .conf-btn {
  min-height: 28px;
  padding: 0 8px;
  font-size: 0.74rem;
}

.numcard__del {
  align-self: flex-start;
  margin-top: 6px;
  padding-left: 0;
}

/* =========================================================== practice ==== */

/*
 * Back to 660. The card was widened to buy vertical space, but moving the set
 * and Options out into the gutters bought that space a better way, and a
 * narrower card leaves those two room to sit comfortably either side of it.
 */
.practice {
  max-width: 660px;
  margin: 0 auto;
}

.flashcard {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 20px;
  box-shadow: var(--shadow);
  padding: 24px 32px;
  text-align: center;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
}

/*
 * A new card arriving.
 *
 * Added only when the WORD changed, never on a reveal and never when a late
 * example lands - see refreshCard. Short and small on purpose: this fires
 * every few seconds for an hour, and anything longer or larger stops reading
 * as a card being dealt and starts reading as the app being slow.
 *
 * Possible at all only since the card region began updating on its own. While
 * the whole view was rebuilt for every keystroke there was no way to tell a
 * new card from the same card redrawn, so any animation ran on both.
 */
@keyframes card-enter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.flashcard.card-enter {
  animation: card-enter 0.16s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  .flashcard.card-enter {
    animation: none;
  }
}

.flashcard__prompt-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
}

.flashcard__prompt {
  font-size: 2.1rem;
  font-weight: 600;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.flashcard__answer {
  font-size: 1.35rem;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.flashcard__notes {
  font-size: 0.85rem;
  color: var(--muted);
  font-style: italic;
}

.flashcard__hidden {
  color: var(--muted);
  font-size: 0.9rem;
}

.practice__rate {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-top: 16px;
}

.rate-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 13px 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  cursor: pointer;
  font-weight: 500;
  transition: background 0.12s, border-color 0.12s;
}

.rate-btn:hover {
  background: var(--surface-2);
}

.rate-btn__glyph {
  font-size: 1.15rem;
  line-height: 1;
}

.rate-btn__key {
  font-size: 0.7rem;
  color: var(--muted);
}

.rate-btn--0:hover {
  border-color: var(--critical);
}
.rate-btn--1:hover {
  border-color: var(--warning);
}
.rate-btn--2:hover {
  border-color: var(--good);
}

/*
 * The moment between answering and the next card.
 *
 * On a fast connection this is two frames and nobody sees it. On mobile data
 * it is long enough that a card which looks unchanged reads as a card that did
 * not take the answer, and the honest response to that is to press again -
 * which used to count the word twice. So the row states what is happening
 * instead of freezing.
 *
 * Deliberately understated: no spinner, no layout shift. Something that
 * announces itself every single card becomes the thing you look at instead of
 * the Spanish. It should only be noticeable when it is slow, which is exactly
 * when it is worth noticing.
 */
.practice__rate.is-saving .rate-btn {
  cursor: default;
  opacity: 0.55;
  transition: opacity 0.18s ease;
}

.practice__rate.is-saving .rate-btn:hover {
  background: var(--surface-1);
  border-color: var(--border);
}

/* The one that was chosen stays lit, so the row reads as this answer being
 * sent rather than as the whole row going dead. Marked with a class rather
 * than :focus, because the buttons are disabled by then and a disabled button
 * cannot hold focus - and because the keyboard path never focused one at all. */
.practice__rate.is-saving .rate-btn.is-chosen {
  opacity: 1;
}

/* example sentence on the flashcard */

.example {
  margin: 14px auto 0;
  max-width: 460px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  text-align: left;
}

.example--loading,
.example--none {
  text-align: center;
  color: var(--muted);
  font-size: 0.82rem;
  background: transparent;
  border-style: dashed;
}

.example__es {
  font-size: 0.98rem;
  line-height: 1.5;
}

.example__es b {
  font-weight: 700;
  border-bottom: 2px solid var(--series-1);
}

.example__en {
  font-size: 0.87rem;
  color: var(--text-secondary);
  margin-top: 4px;
}

.example__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 8px;
  font-size: 0.7rem;
  color: var(--muted);
}

.fixit {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  justify-content: center;
  margin-top: 12px;
}

.practice__bar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.practice__stats {
  display: flex;
  gap: 16px;
  justify-content: center;
  margin-top: 18px;
  font-size: 0.8rem;
  color: var(--muted);
}

.practice__stats b {
  color: var(--text-primary);
  font-weight: 600;
}

/* ============================================================== stats ==== */

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 14px;
}

.tile {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 15px 17px;
  box-shadow: var(--shadow);
}

.tile__label {
  font-size: 0.78rem;
  color: var(--text-secondary);
}

.tile__value {
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 1.15;
  margin-top: 3px;
}

.tile__delta {
  font-size: 0.76rem;
  color: var(--muted);
  margin-top: 2px;
}

.tile__delta--up {
  color: var(--success-text);
}

.hero {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 22px 24px;
  display: flex;
  align-items: center;
  gap: 26px;
  flex-wrap: wrap;
}

.hero__figure {
  font-size: 3.4rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}

.hero__label {
  font-size: 0.85rem;
  color: var(--text-secondary);
}

.hero__spark {
  margin-left: auto;
}

/* charts */

.chart-wrap {
  width: 100%;
  overflow-x: auto;
}

.chart {
  display: block;
  width: 100%;
  min-width: 320px;
}

.chart text {
  font-family: var(--font);
  fill: var(--muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.chart .axis-line {
  stroke: var(--axis);
  stroke-width: 1;
}

.chart .grid-line {
  stroke: var(--grid);
  stroke-width: 1;
}

.chart .bar-hit {
  fill: transparent;
  cursor: pointer;
}

.chart .bar-hit:hover + .bar-group path,
.chart .bar-hit:focus-visible + .bar-group path {
  opacity: 0.78;
}

.legend {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 0.78rem;
  color: var(--text-secondary);
  margin-bottom: 12px;
}

.legend__item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.legend__swatch {
  width: 11px;
  height: 11px;
  border-radius: 3px;
  flex: none;
}

.tooltip {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 9px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
  padding: 8px 11px;
  font-size: 0.79rem;
  color: var(--text-primary);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.1s;
}

.tooltip[data-show='true'] {
  opacity: 1;
}

.tooltip__row {
  display: flex;
  align-items: center;
  gap: 7px;
}

.tooltip__title {
  color: var(--text-secondary);
  margin-bottom: 3px;
}

/* mix bar (part-to-whole, ordered status scale) */

.mixbar {
  display: flex;
  width: 100%;
  height: 34px;
  gap: 2px; /* 2px surface gap between fills - never a stroke */
  margin: 4px 0 14px;
}

.mixbar__seg {
  height: 100%;
  display: grid;
  place-items: center;
  font-size: 0.76rem;
  font-weight: 600;
  color: #fff;
  min-width: 2px;
}

.mixbar__seg:first-child {
  border-radius: 6px 0 0 6px;
}

.mixbar__seg:last-child {
  border-radius: 0 6px 6px 0;
}

/* data table (the WCAG-clean twin of every chart) */

.datatable {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.83rem;
}

.datatable th,
.datatable td {
  text-align: left;
  padding: 7px 10px;
  border-bottom: 1px solid var(--grid);
}

.datatable th {
  color: var(--text-secondary);
  font-weight: 500;
  font-size: 0.76rem;
}

.datatable td.num,
.datatable th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.datatable tr:last-child td {
  border-bottom: 0;
}

details.table-view {
  margin-top: 12px;
}

details.table-view > summary {
  cursor: pointer;
  font-size: 0.78rem;
  color: var(--text-secondary);
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
}

details.table-view > summary::-webkit-details-marker {
  display: none;
}

details.table-view > summary::before {
  content: 'â–¸';
  font-size: 0.7em;
}

details.table-view[open] > summary::before {
  content: 'â–¾';
}

.table-scroll {
  overflow-x: auto;
  margin-top: 8px;
}

/* ============================================================== goals ==== */

.goal {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  box-shadow: var(--shadow);
}

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

.goal__title {
  font-weight: 600;
}

.goal__sub {
  font-size: 0.78rem;
  color: var(--muted);
}

.goal__numbers {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 10px 0 8px;
}

.goal__count {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1;
}

.goal__target {
  color: var(--text-secondary);
  font-size: 0.85rem;
}

/* meter: fill and track are steps of one ramp, so state reads across the bar */
.meter {
  height: 9px;
  border-radius: 999px;
  background: var(--grid);
  overflow: hidden;
}

.meter__fill {
  height: 100%;
  border-radius: 999px;
  background: var(--series-1);
  transition: width 0.3s ease;
}

.meter--done .meter__fill {
  background: var(--good);
}

.goal__foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  margin-top: 9px;
  font-size: 0.78rem;
  color: var(--muted);
}

.goal-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  align-items: end;
}

/* ============================================================= toasts ==== */

.toast-stack {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
}

.toast {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--series-1);
  border-radius: 10px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.14);
  padding: 10px 14px;
  font-size: 0.85rem;
  max-width: 340px;
  animation: toast-in 0.18s ease;
}

.toast--error {
  border-left-color: var(--critical);
}

.toast--good {
  border-left-color: var(--good);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==================================================== translation check == */

.recheck-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 10px;
}

.recheck {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  background: var(--surface-2);
}

.recheck--done {
  opacity: 0.55;
}

.recheck__word {
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.recheck__flag {
  font-size: 0.68rem;
  font-weight: 500;
  padding: 1px 7px;
  border-radius: 999px;
  color: #fff;
  background: var(--critical);
}

/* a missing accent is a question, not a verdict - warning, not critical */
.recheck__flag--accent {
  background: var(--warning);
  color: #0b0b0b;
}

.accent-hint {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 0.8rem;
  color: var(--text-secondary);
}

.accent-hint .chip {
  border-style: solid;
  border-color: var(--warning);
}

.recheck__row {
  display: flex;
  gap: 8px;
  font-size: 0.85rem;
  line-height: 1.6;
}

.recheck__label {
  flex: none;
  width: 72px;
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding-top: 3px;
}

.recheck__cur {
  color: var(--text-secondary);
  text-decoration: line-through;
  text-decoration-color: var(--muted);
}

.recheck__new {
  color: var(--text-primary);
  font-weight: 500;
}

.recheck__actions {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}

/* ============================================================= dialog ==== */

dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  color: var(--text-primary);
  padding: 0;
  max-width: 440px;
  width: calc(100% - 32px);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
}

dialog::backdrop {
  background: rgba(0, 0, 0, 0.42);
}

.dialog__body {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.dialog__foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 20px;
  border-top: 1px solid var(--border);
}

/* ---------------------------------------------------------------- footer ---
   Licence attribution for Wiktionary (CC BY-SA) and Tatoeba (CC BY). This is a
   legal obligation of using those sources, so it is deliberately always
   rendered and never collapsed behind a toggle. */
.sitefoot {
  margin-top: 3rem;
  padding: 20px 20px 32px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}

.sitefoot__inner {
  /*
   * 1120px, not 60rem, so the footer's left edge lands on the same line as
   * every heading, card and paragraph above it. At 60rem (960px) against
   * .app's 1120px the footer copy sat 76px to the right of the content on a
   * 1440px screen - the kind of misalignment that reads as "assembled"
   * without a reader being able to say why.
   */
  max-width: 1120px;
  margin: 0 auto;
}

.sitefoot__credits,
.sitefoot__note {
  margin: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.sitefoot__note {
  margin-top: 0.35rem;
}

/* Privacy and terms. Set apart from the licence credits above by a rule
   rather than by more space, because they are a different kind of obligation
   and running them together reads as one long disclaimer nobody finishes. */
.sitefoot__legal {
  margin: 0.9rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 0.5rem;
  color: var(--muted);
  font-size: 0.8125rem;
}

.sitefoot a {
  color: var(--text-secondary);
  text-decoration: underline;
}

.sitefoot a:hover,
.sitefoot a:focus-visible {
  color: var(--text-primary);
}

/* ----------------------------------------------------------------- login ---
   Standalone page shown before a session exists. Uses only the shared tokens
   so it stays in step with the app's light and dark themes. */
.loginpage {
  display: grid;
  place-items: center;
  min-height: 100vh;
  background: var(--surface-2);
}

.loginbox {
  width: min(22rem, calc(100vw - 2rem));
  padding: 2rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-1);
  text-align: center;
}

.loginbox__brand {
  display: flex;
  justify-content: center;
  margin-bottom: 0.75rem;
}

.loginbox__title {
  margin: 0 0 0.35rem;
  font-size: 1.125rem;
  color: var(--text-primary);
}

/* Carries the bottom margin the title used to, so the spacing below the brand
   is unchanged whether or not the tagline is present. */
.loginbox__tagline {
  margin: 0 0 1.5rem;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.45;
}

.loginbox__label {
  display: block;
  margin-bottom: 0.35rem;
  text-align: left;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

.loginbox__input {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text-primary);
  font: inherit;
}

.loginbox__input:focus-visible {
  outline: 2px solid var(--text-secondary);
  outline-offset: 1px;
}

/*
 * Used by checkout.html and defined nowhere, so the one button on the payment
 * page sat in an unstyled div, flush against the paragraph above it.
 */
.loginbox__actions {
  display: flex;
  justify-content: center;
  margin-top: 18px;
}

.loginbox__button {
  width: 100%;
  margin-top: 0.9rem;
  padding: 0.6rem 0.7rem;
  border: 0;
  border-radius: 8px;
  background: #aa151b;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.loginbox__button:disabled {
  opacity: 0.6;
  cursor: default;
}

.loginbox__error {
  margin: 0.9rem 0 0;
  color: #c0392b;
  font-size: 0.8125rem;
}

/* Login: account tabs, fallback and messages. */
.loginbox__tabs {
  display: flex;
  gap: 0.25rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}

.loginbox__tab {
  flex: 1;
  padding: 0.5rem 0.25rem;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}

.loginbox__tab.is-active {
  color: var(--text-primary);
  border-bottom-color: #aa151b;
  font-weight: 600;
}

.loginbox__link {
  display: block;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.8125rem;
  text-decoration: underline;
  cursor: pointer;
}

.loginbox__link:hover { color: var(--text-primary); }
.loginbox__link:disabled { opacity: 0.6; cursor: default; }

/* .loginbox__fallback styled the shared-password disclosure, removed with it
   on 13 August 2026. */

/* Google sign-in. Deliberately quieter than the primary button: the email
   form is the way in that works without a third party, and the alternative
   should read as an alternative rather than as the recommended path. */
.loginbox__alt {
  margin-top: 1.1rem;
}

.loginbox__or {
  display: block;
  margin-bottom: 0.6rem;
  color: var(--text-secondary);
  font-size: 0.75rem;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.loginbox__google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.18));
  border-radius: 8px;
  background: none;
  color: var(--text-primary);
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

.loginbox__google:hover {
  border-color: var(--text-secondary);
}

/* Fixed size in both directions. An SVG with no height in a flex row
   collapses to nothing in some browsers, which is a logo that is present in
   the markup and invisible on the screen. */
.loginbox__glogo {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
}

/* The consent tick beside the Google button. Not styled as an error, because
   it appears when the server asked a question rather than when something
   went wrong. */
.loginbox__gate {
  margin-bottom: 0.75rem;
  padding: 0.7rem 0.75rem;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.18));
  border-radius: 8px;
}

.loginbox__gate-text {
  margin: 0 0 0.55rem;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.45;
}

/* ---------------------------------------------- account created ---------- */

.signupdone {
  text-align: center;
}

.signupdone__tick {
  width: 64px;
  height: 64px;
  margin: 0.5rem auto 1rem;
  display: block;
}

.signupdone__circle,
.signupdone__check {
  stroke: #2e9e5b;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Drawn rather than faded in. The circle sweeps, then the tick is struck
   through it, which reads as something completing instead of something
   appearing. */
.signupdone__circle {
  stroke-dasharray: 151;
  stroke-dashoffset: 151;
  animation: signupdone-draw 0.45s ease-out forwards;
}

.signupdone__check {
  stroke-dasharray: 40;
  stroke-dashoffset: 40;
  animation: signupdone-draw 0.3s 0.4s ease-out forwards;
}

@keyframes signupdone-draw {
  to { stroke-dashoffset: 0; }
}

/* Movement is decoration here; the mark itself is the message. Anybody who
   has asked for less motion gets it drawn already complete. */
@media (prefers-reduced-motion: reduce) {
  .signupdone__circle,
  .signupdone__check {
    animation: none;
    stroke-dashoffset: 0;
  }
}

.signupdone__title {
  margin: 0 0 0.6rem;
  font-size: 1.15rem;
}

.signupdone__text {
  margin: 0 0 0.75rem;
  font-size: 0.9rem;
  line-height: 1.5;
}

.signupdone__note {
  margin: 0 0 1.1rem;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.45;
}

/* Keyboard users must be able to see where they are. The link is the only
   control on this page that is not a native button, so it does not inherit
   whatever focus treatment those carry. */
.loginbox__google:focus-visible {
  outline: 2px solid #aa151b;
  outline-offset: 2px;
}

.loginbox__notice {
  margin: 0.9rem 0 0;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.45;
}

/* The consent row. Laid out so the box sits beside the first line of text
   rather than centred against a wrapped paragraph, which is what makes a
   tick box look like an afterthought. */
.loginbox__consent {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  margin-top: 0.9rem;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-secondary);
  text-align: left;
  cursor: pointer;
}

.loginbox__consent input {
  /* Nudged down to sit on the text baseline rather than above it. */
  margin: 0.18rem 0 0;
  flex: none;
  accent-color: var(--series-1);
  width: 1rem;
  height: 1rem;
  cursor: pointer;
}

.loginbox__consent a {
  color: var(--series-1);
}

.loginbox__legal {
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  color: var(--muted);
  font-size: 0.8125rem;
}

.loginbox__legal a {
  color: var(--text-secondary);
}

/* ================================================================== map === */

/*
 * The canvas cannot read CSS, so the palette is handed to it through these
 * custom properties and re-read whenever the theme changes. Red, amber and
 * green are the same values as the confidence dots on purpose: the map has to
 * agree with the rest of the app about what "getting there" looks like.
 */
:root {
  --graph-new: #9a9892;
  --graph-low: #d03b3b;
  --graph-mid: #fab219;
  --graph-high: #0ca30c;
  --graph-locked: #cfcdc4;
  --graph-topic: #2a78d6;
  --graph-topic-locked: #b6b4ab;
  --graph-edge: rgba(11, 11, 11, 0.07);
  --graph-edge-link: rgba(11, 11, 11, 0.22);
  --graph-edge-prereq: rgba(42, 120, 214, 0.4);
  --graph-label: #0b0b0b;
  --graph-label-dim: rgba(11, 11, 11, 0.4);
  --graph-halo: rgba(249, 249, 247, 0.85);
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme='light'])) {
    --graph-new: #7e7c76;
    --graph-locked: #3a3a37;
    --graph-topic: #3987e5;
    --graph-topic-locked: #4a4a45;
    --graph-edge: rgba(255, 255, 255, 0.07);
    --graph-edge-link: rgba(255, 255, 255, 0.2);
    --graph-edge-prereq: rgba(57, 135, 229, 0.45);
    --graph-label: #ffffff;
    --graph-label-dim: rgba(255, 255, 255, 0.4);
    --graph-halo: rgba(13, 13, 13, 0.85);
  }
}

:root[data-theme='dark'] {
  --graph-new: #7e7c76;
  --graph-locked: #3a3a37;
  --graph-topic: #3987e5;
  --graph-topic-locked: #4a4a45;
  --graph-edge: rgba(255, 255, 255, 0.07);
  --graph-edge-link: rgba(255, 255, 255, 0.2);
  --graph-edge-prereq: rgba(57, 135, 229, 0.45);
  --graph-label: #ffffff;
  --graph-label-dim: rgba(255, 255, 255, 0.4);
  --graph-halo: rgba(13, 13, 13, 0.85);
}

.graph {
  display: grid;
  gap: 16px;
}

/*
 * On a wide screen the topic list sits beside the map rather than under it.
 * The graph settles into a roughly square shape, so in a full-width stage it
 * letterboxes badly and leaves a third of the page empty either side.
 */
@media (min-width: 1000px) {
  .graph {
    grid-template-columns: minmax(0, 1fr) 340px;
    align-items: start;
  }

  /* Embedded in the Camino home there is no topic list beside it, so the
     second column would just reserve 340px of nothing. */
  .graph--embedded {
    grid-template-columns: minmax(0, 1fr);
  }

  .graph__stage {
    height: min(72vh, 680px);
  }

  /* In a column this narrow the name and the bar fight for the same space and
     every topic wraps to two lines. Same stacking as the phone layout. */
  .graph__topics .topiclist__row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 6px;
  }

  .graph__topics .topiclist__row .graph__bar {
    grid-column: 1 / -1;
    order: 3;
  }
}

.graph__stage {
  position: relative;
  height: min(68vh, 620px);
  min-height: 340px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  overflow: hidden;
}

.graph__canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* The canvas owns pan and pinch. Without this the browser scrolls the page
     out from under a drag, which makes the map unusable on a phone. */
  touch-action: none;
}

.graph__legend {
  position: absolute;
  left: 12px;
  bottom: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  pointer-events: none;
}

.key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 0.72rem;
  white-space: nowrap;
}

.key::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.key--new::before { background: var(--graph-new); }
.key--low::before { background: var(--graph-low); }
.key--mid::before { background: var(--graph-mid); }
.key--high::before { background: var(--graph-high); }
.key--locked::before { background: var(--graph-locked); }

.graph__tools {
  position: absolute;
  right: 12px;
  top: 12px;
  display: flex;
  gap: 6px;
}

.graph__panel {
  position: absolute;
  left: 12px;
  top: 12px;
  width: min(280px, calc(100% - 24px));
  max-height: calc(100% - 24px);
  overflow-y: auto;
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--shadow);
}

.graph__panel h3 {
  margin: 0 0 2px;
  font-size: 1.05rem;
}

.graph__close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.graph__close:hover {
  background: var(--surface-2);
  color: var(--text-primary);
}

.graph__en {
  margin: 0 0 8px;
  color: var(--text-secondary);
}

.graph__notes {
  margin: 0 0 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-secondary);
  font-size: 0.83rem;
}

.graph__meta {
  margin: 10px 0 4px;
  color: var(--muted);
  font-size: 0.78rem;
}

.graph__locked {
  margin: 8px 0 0;
  color: var(--text-secondary);
  font-size: 0.83rem;
}

.graph__links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
}

.graph__links li {
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-size: 0.83rem;
}

.graph__links span {
  color: var(--muted);
}

.linkbtn {
  border: 0;
  background: none;
  padding: 0;
  color: var(--series-1);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  text-align: left;
}

.linkbtn:hover {
  text-decoration: underline;
}

/*
 * A neutral track, not --meter-track. The shared meter track is a step of the
 * fill's own blue ramp, which is fine where bars are usually part full. Here
 * seven of eight topics sit at zero, and a row of solid blue tracks reads as
 * seven completed topics at a glance.
 */
.graph__bar {
  height: 6px;
  border-radius: 999px;
  background: var(--grid);
  overflow: hidden;
}

.graph__bar > span {
  display: block;
  height: 100%;
  background: var(--series-1);
}

/* The topic list is the non-canvas version of the same information, and the
   only part of this view a screen reader can use. It is not decoration. */
.topiclist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.topiclist__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 120px auto;
  gap: 12px;
  align-items: center;
}

.topiclist__row.is-locked {
  opacity: 0.55;
}

.topiclist__name {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 500;
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
}

.topiclist__name:hover {
  color: var(--series-1);
}

.lockmark {
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--muted);
}

.topiclist__count {
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  font-size: 0.83rem;
}

.empty__actions {
  margin-top: 12px;
}

@media (max-width: 640px) {
  .graph__stage {
    height: min(60vh, 460px);
  }

  /* On a phone the panel would cover the graph it describes, so it docks to
     the bottom instead of floating over the middle of it. */
  .graph__panel {
    left: 8px;
    right: 8px;
    top: auto;
    bottom: 8px;
    width: auto;
    max-height: 52%;
  }

  .graph__legend {
    bottom: auto;
    top: 12px;
    left: 12px;
    max-width: 60%;
  }

  .topiclist__row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .topiclist__row .graph__bar {
    grid-column: 1 / -1;
    order: 3;
  }
}

/* ============================================================== course === */

.course {
  padding: 20px 22px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.course__stats {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin-bottom: 16px;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.stat b {
  font-size: 1.9rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat span {
  font-size: 0.82rem;
  color: var(--text-secondary);
}

/* Neutral track for the same reason the map's bars use one: most of these sit
   at zero, and a coloured track reads as a finished bar. */
.course__bar {
  height: 8px;
  border-radius: 999px;
  background: var(--grid);
  overflow: hidden;
}

.course__bar > span {
  display: block;
  height: 100%;
  background: var(--series-1);
}

.course__bar--sm {
  height: 5px;
  width: 110px;
  flex: none;
}

.course__pct {
  margin: 8px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.course__cta {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.course__cta .hint {
  margin: 0;
}

.btn--lg {
  min-height: 48px;
  padding: 0 26px;
  font-size: 1rem;
}

.tlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 12px;
}

/*
 * A grid, not a flex row.
 *
 * The last cell holds either a Practise button or the word locked, and those
 * are different widths, so with flex every locked row pushed its bar 36px
 * right of the unlocked ones and the column zigzagged down the card. Fixed
 * tracks mean the bars line up whatever ends up in the last cell.
 */
.tlist__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 110px 76px;
  align-items: center;
  gap: 14px;
  min-height: 40px;
}

.tlist__row > .course__bar--sm {
  width: 100%;
}

.tlist__row .btn,
.tlist__row .lockmark {
  justify-self: end;
}

.tlist__row.is-locked {
  opacity: 0.6;
}

.tlist__main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.tlist__name {
  font-weight: 500;
}

.tlist__note {
  font-size: 0.78rem;
  color: var(--muted);
}

@media (max-width: 560px) {
  .course__stats {
    gap: 18px;
  }

  .stat b {
    font-size: 1.5rem;
  }

  /* The bar would be squeezed to nothing beside the name and the button. */
  .tlist__row .course__bar--sm {
    grid-column: 1 / -1;
  }
}

/* ============================================================= summary === */

.summary {
  padding: 26px 24px;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-align: center;
}

.summary h2 {
  margin: 0 0 4px;
  font-size: 1.4rem;
}

.summary__sub {
  margin: 0 0 20px;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.summary__counts {
  display: flex;
  justify-content: center;
  gap: 34px;
  margin-bottom: 20px;
}

.summary__count {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.summary__count b {
  font-size: 1.8rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.summary__count b.conf-0 { color: var(--critical); }
.summary__count b.conf-1 { color: var(--warning-text); }
.summary__count b.conf-2 { color: var(--good); }

.summary__count span {
  font-size: 0.76rem;
  color: var(--text-secondary);
}

/* The moment a topic opens is the most motivating thing the course can show,
   so it gets its own block rather than a line of body text. */
.summary__unlocked {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 16px;
  margin-bottom: 20px;
  border: 1px solid var(--good);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--good) 10%, transparent);
}

.summary__unlocked strong {
  color: var(--success-text);
}

.summary__unlocked span {
  font-size: 0.88rem;
}

.summary__actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* ====================================================== practice chrome === */

/*
 * The three rating buttons must be on screen without scrolling, always.
 *
 * Two earlier attempts failed in opposite directions. Letting the page grow
 * pushed the buttons below the fold on a short screen. Making the row sticky
 * kept it visible but floated it across the bottom of the card, hiding the
 * content behind it.
 *
 * So the screen is claimed as a column: the bar and the buttons take what
 * they need, and the card takes what is left. When a card has more in it than
 * fits, the card itself scrolls and the word being tested stays pinned to the
 * top of it, which is the one thing that must never scroll away.
 */
.practice {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* Deliberately no height on the column. Fixing it made the card share the
     viewport with the disclosure, the skip row and the tally underneath, and
     the card was squeezed to a sliver. Only the card is bounded, below. */
}

.practice__rate,
.pbar {
  flex: 0 0 auto;
}

.pbar {
  display: flex;
  align-items: center;
  gap: 12px;
}

.pbar__back {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  flex: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface-1);
  color: var(--text-secondary);
  font-size: 1.05rem;
  text-decoration: none;
}

.pbar__back:hover {
  color: var(--text-primary);
  background: var(--surface-2);
}

.pbar__set {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.pbar__name {
  /* An h1 as of the accessibility pass. The class already set font-size and
   * weight, but not margin, and the browser's own h1 margin would have pushed
   * the bar apart. Stated here so the element can change without the layout
   * moving. */
  margin: 0;
  font-weight: 600;
  font-size: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pbar__count {
  font-size: 0.76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.pbar__opts {
  position: relative;
  flex: none;
}

.pbar__opts > summary {
  list-style: none;
  cursor: pointer;
  padding: 8px 14px;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-1);
  color: var(--text-secondary);
  font-size: 0.83rem;
}

.pbar__opts > summary::-webkit-details-marker { display: none; }
.pbar__opts[open] > summary { color: var(--text-primary); background: var(--surface-2); }

.pbar__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 20;
  width: min(300px, calc(100vw - 40px));
  display: grid;
  gap: 12px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  box-shadow: var(--shadow);
}

/*
 * The rating buttons are the only thing on this screen the user must be able
 * to reach. A revealed card carries grammar, an example, notes and history,
 * which pushed them off the bottom of a phone, so the card scrolls inside
 * itself and the buttons stay put.
 */
/*
 * The card takes the space left over. It only scrolls when its contents
 * genuinely do not fit, the scrollbar is hidden because a bar down the side
 * of a flashcard looks broken, and the prompt is pinned so the word under
 * test cannot scroll out of view the way it used to.
 */
.flashcard {
  /* Bounded so that the card plus the three rating buttons always fit one
     screen: roughly the header, the set bar, the buttons and the gaps between
     them. Everything below the buttons is free to fall past the fold. */
  /* Loose enough that a full card -- word, pronunciation, answer, grammar and
     the sentence with its footer -- is never clipped, and tight enough that
     the three buttons stay on screen on a phone. Clipping the sentence to
     guarantee the buttons was the wrong trade: the sentence is the reason the
     card is worth reading. */
  /* The card must end above the locked bar, or the bar covers it. This is the
     space above the card plus the bar's own height, with a little slack. */
  max-height: calc(100vh - 260px);
  max-height: calc(100svh - 260px);
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: none;
  /* The base rule centres the card's contents. Centred flex content that
     overflows gets clipped at the TOP and cannot be scrolled back to, which
     would hide the very word this is all about. 'safe' drops to top-aligned
     exactly when that would happen; the first line is the fallback for
     engines that do not know the keyword. */
  justify-content: flex-start;
  justify-content: safe center;
}

.flashcard::-webkit-scrollbar {
  display: none;
}

/*
 * THE RATING BAR IS PINNED ON A PHONE AND IN ORDINARY FLOW ON A DESKTOP.
 *
 * Pinning it exists for one reason: on a phone the card, the skip row and the
 * tally are taller than the screen, so without it you would scroll down to
 * rate every single card. Under your thumb, always, is worth a floating bar.
 *
 * ON A LARGE SCREEN NONE OF THAT APPLIES and it made the page worse. The whole
 * practice column is about 360px tall, so there is nothing to scroll to and
 * nothing to keep in reach - the bar was pinned to the bottom of a window it
 * had no relationship with, and the site footer, which lives after #app and
 * knows nothing about any of this, ran underneath it. What a reader saw was
 * the footer sitting on top of the ratings, which is exactly as odd as it
 * sounds and got worse the bigger the window, because the emptier the page the
 * further the bar was from the thing it belonged to.
 *
 * So above 900px it is what it always should have been on a desktop: the next
 * thing after the card. Card, buttons, tally, footer, in that order, with the
 * footer directly under the tally.
 *
 * 900px rather than a pointer query. `pointer: coarse` is the more precise
 * question - the bar is for thumbs - but a touchscreen laptop answers coarse
 * at 1600px wide and would keep a floating bar it has no use for, and a phone
 * with a mouse attached is not a case worth designing around.
 */
@media (max-width: 900px) {
  .practice__rate {
    /* fixed, not sticky. Sticky only holds inside its own container, and this
       one ends at the tally, so the bar would leave the screen the moment you
       scrolled past it - which is not what "locked to the screen" means. The
       horizontal padding centres the three buttons on the same column the
       rest of the app uses. */
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    padding: 10px max(16px, calc((100vw - 660px) / 2)) 12px;
    background: var(--plane);
    border-top: 1px solid var(--border);
  }

  /*
   * Room at the end of the column for the bar to cover.
   *
   * The comment on the old rule claimed this padding existed. It did not, and
   * that was the whole defect: with nothing reserved, the last 80-odd pixels
   * of the document sat permanently under an opaque bar, and on the practice
   * screen the document ends with the site footer.
   *
   * It goes on .practice rather than on the footer or the body because
   * .practice is in the DOM only on this screen. Making that column taller
   * pushes everything after #app - the footer included - down by the same
   * amount, so what ends up under the bar at full scroll is this padding and
   * nothing else.
   *
   * Deliberately more than the bar needs. It is about 85px tall on a phone
   * once the three labels sit on one line each, and the two failure modes are
   * not symmetric: reserving too much costs a little dead scroll below the
   * tally that nobody will notice, and reserving too little hides the end of
   * the document behind an opaque bar, which is the defect this whole block
   * exists to fix. When in doubt, reserve more.
   */
  .practice {
    padding-bottom: 120px;
  }
}

@media (max-width: 560px) {
  .pbar__name {
    font-size: 0.95rem;
  }
}

/* The text generator, folded away. It is a different intent from reading
   something in front of you, and having both open at once is most of why this
   tab was hard to name. */
.gen {
  margin-top: 14px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

.gen > summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--text-secondary);
  list-style: none;
}

.gen > summary::-webkit-details-marker { display: none; }
.gen > summary::before { content: '+ '; color: var(--muted); }
.gen[open] > summary::before { content: '\2212 '; }
.gen > summary:hover { color: var(--text-primary); }
.gen .genbar { margin-top: 12px; }

/*
 * The disclosure under a revealed answer.
 *
 * Centred, because it sits under a card whose every other element is centred
 * and a lone left-aligned control reads as a mistake. The contents get their
 * own recessed panel and go back to left alignment, since reference text
 * centred is reference text nobody reads.
 */
.cardmore {
  margin-top: 4px;
  text-align: center;
}

.cardmore > summary {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 15px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-1);
  color: var(--text-secondary);
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
  list-style: none;
  transition: color 0.12s, background 0.12s, border-color 0.12s;
}

.cardmore > summary::-webkit-details-marker {
  display: none;
}

/* A chevron drawn from borders, so it can rotate rather than swap glyph and
   jump the summary's width around as it opens. */
.cardmore > summary::after {
  content: '';
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.18s ease;
}

.cardmore[open] > summary::after {
  transform: translateY(1px) rotate(-135deg);
}

.cardmore > summary:hover {
  color: var(--text-primary);
  background: var(--surface-2);
}

.cardmore > summary:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

.cardmore[open] > summary {
  color: var(--text-primary);
  background: var(--surface-2);
}

.cardmore__body {
  display: grid;
  gap: 12px;
  margin-top: 14px;
  padding: 16px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  text-align: left;
}

/* Inside the panel these are content, not card furniture, so they lose the
   centring the flashcard gives them. */
.cardmore__body .flashcard__prompt-label,
.cardmore__body .flashcard__notes,
.cardmore__body .fixit {
  text-align: left;
  margin: 0;
}

@media (prefers-reduced-motion: reduce) {
  .cardmore > summary::after {
    transition: none;
  }
}

/* Compact enough that the answer, the sentence and the three buttons are on
   screen together. Reading a sentence you have to scroll to reach is not
   reading it. */
.flashcard .example {
  margin-top: 2px;
  padding: 10px 12px;
}

.flashcard .example__es {
  font-size: 0.98rem;
}

.flashcard .example__en {
  font-size: 0.86rem;
}

.flashcard .example__foot {
  margin-top: 6px;
  font-size: 0.72rem;
}

/*
 * On a wide screen, move the set and the Options button off the card.
 *
 * They were a row above the card, so they cost the card vertical space on
 * every screen, while the desktop layout had two empty gutters either side
 * doing nothing. Out there they cost nothing: the bar is taken out of flow
 * and hung in the margins, level with the top of the card, and the card
 * reclaims the height the row used to take.
 *
 * Only above 1280px, where the gutters are genuinely 200px wide. Narrower
 * than that the row stays where it is, above the card.
 */
@media (min-width: 1280px) {
  .practice {
    position: relative;
  }

  .pbar {
    position: absolute;
    top: 2px;
    /* Asymmetric on purpose. The left holds a back button and a two line
       label; the right holds one small button, so pulling that side out as
       far would leave it stranded 140px from the card while the left sat
       against it. These two values put both about 20px off the card. */
    left: -220px;
    right: -96px;
    align-items: flex-start;
    gap: 10px;
  }

  /* Held to the gutter so a long course name can never run over the card.
     36 for the back button, 10 for the gap and 165 here comes to 211, inside
     the 220 the row is pulled out by. */
  .pbar__set {
    flex: 0 0 154px;
  }

  .pbar__opts {
    margin-left: auto;
  }

  /* The row is out of flow, so the card gets that height back. */
  .flashcard {
    max-height: calc(100svh - 210px);
  }
}

/* ========================================================== feedback ====== */

/*
 * The rating flash. A wash of the rating's own colour up from the bottom of
 * the screen, gone in under half a second: long enough to register as an
 * answer landing, short enough never to delay the next card.
 */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 60;
  opacity: 0;
  background: radial-gradient(120% 60% at 50% 100%, var(--flash, transparent), transparent 70%);
}

body.rated--0 { --flash: color-mix(in srgb, var(--critical) 55%, transparent); }
body.rated--1 { --flash: color-mix(in srgb, var(--warning) 55%, transparent); }
body.rated--2 { --flash: color-mix(in srgb, var(--good) 55%, transparent); }

body.rated--0::after,
body.rated--1::after,
body.rated--2::after {
  animation: rating-flash 320ms ease-out;
}

@keyframes rating-flash {
  0% { opacity: 0; }
  22% { opacity: 0.5; }
  100% { opacity: 0; }
}

/* The button itself gives way under the press. */
.rate-btn {
  transition: transform 90ms ease, border-color 120ms ease, background 120ms ease;
}

.rate-btn:active {
  transform: scale(0.955);
}

/* Each new card arrives rather than blinking into place. */
.flashcard__answer {
  animation: answer-in 190ms ease-out;
}

@keyframes answer-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/*
 * Finishing a set. The panel lifts in, the three counts land one after the
 * other, and an unlocked topic gets its own arrival after them, because that
 * is the news worth waiting a beat for.
 */
.summary {
  animation: summary-in 320ms cubic-bezier(0.2, 0.9, 0.3, 1.2);
}

@keyframes summary-in {
  from { opacity: 0; transform: scale(0.94) translateY(10px); }
  to { opacity: 1; transform: none; }
}

.summary__count {
  animation: count-in 380ms cubic-bezier(0.2, 0.9, 0.3, 1.4) backwards;
}

.summary__count:nth-child(1) { animation-delay: 160ms; }
.summary__count:nth-child(2) { animation-delay: 260ms; }
.summary__count:nth-child(3) { animation-delay: 360ms; }

@keyframes count-in {
  from { opacity: 0; transform: scale(0.4); }
  60% { transform: scale(1.15); }
  to { opacity: 1; transform: none; }
}

.summary__unlocked {
  animation: unlocked-in 520ms cubic-bezier(0.2, 0.9, 0.3, 1.3) 520ms backwards;
}

@keyframes unlocked-in {
  from { opacity: 0; transform: scale(0.8); }
  55% { transform: scale(1.06); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  body::after,
  .flashcard__answer,
  .summary,
  .summary__count,
  .summary__unlocked {
    animation: none;
  }

  .rate-btn {
    transition: none;
  }
}

/* =========================================================== bundles ====== */

.bgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: 14px;
}

.bgrid__head {
  margin-top: 34px;
}

.bcard {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  color: inherit;
  text-decoration: none;
}

.bcard__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.bcard h2 {
  margin: 0;
  font-size: 1.05rem;
}

.bcard__blurb {
  margin: 0;
  flex: 1;
  font-size: 0.86rem;
  color: var(--text-secondary);
}

.bcard__meta {
  margin: 0;
  font-size: 0.76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.bcard__badge {
  flex: none;
  padding: 2px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--good) 16%, transparent);
  color: var(--success-text);
  font-size: 0.7rem;
  font-weight: 600;
}

.bcard__lock {
  flex: none;
  font-size: 0.9rem;
  opacity: 0.5;
}

.bcard--owned {
  border-color: color-mix(in srgb, var(--series-1) 45%, var(--border));
  transition: transform 120ms ease, border-color 120ms ease;
}

.bcard--owned:hover {
  transform: translateY(-2px);
  border-color: var(--series-1);
}

/* Locked cards are sketches. They must read as unavailable at a glance and
   never look like something a click has failed to open. */
.bcard--locked {
  opacity: 0.55;
  cursor: default;
}

.bcard--locked h2 {
  font-weight: 500;
}

/* ------------------------------------------------------------- tools ----- */

.tools {
  position: relative;
}

.tools > summary {
  list-style: none;
  cursor: pointer;
}

.tools > summary::-webkit-details-marker {
  display: none;
}

.tools__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 50;
  min-width: 180px;
  display: grid;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  box-shadow: var(--shadow);
}

.tools__panel a {
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 0.88rem;
}

.tools__panel a:hover {
  background: var(--surface-2);
  color: var(--text-primary);
}

/* A quieter variant, for a control that sits inside a card header rather than
   at the top of a page. */
.seg--sm button {
  padding: 5px 11px;
  font-size: 0.78rem;
}

.card__head .gran-seg {
  flex: none;
}

/* The goal the form is about to create, said in one sentence. Four controls
   whose meanings depend on each other are easier to confirm than to explain. */
.goal-preview {
  margin: 14px 0 0;
  padding: 10px 14px;
  border-left: 3px solid var(--series-1);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 0.92rem;
}

/* The map's header carries the Start button, so it needs to align to the top
   rather than the baseline: the heading has a two-line hint under it and the
   button should sit level with the heading, not with the end of the hint. */
.card__head--map {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.card__head--map .btn {
  flex: none;
}

/* ------------------------------------------------- the rating mark ------- */

/*
 * The glyph that lands when you rate a card.
 *
 * Quick and quiet: it fades in already moving, drifts a little to one side
 * rather than straight up, and is gone in just over half a second. The drift
 * and a slight tilt come from two custom properties set per mark, because
 * identical motion every time stops reading as a reaction to what you did.
 *
 * Soft rather than loud: no heavy weight, a wide low glow in its own colour
 * instead of a hard edge, and it never reaches full opacity.
 */
.ratepop {
  position: fixed;
  left: 50%;
  top: 46%;
  z-index: 70;
  display: grid;
  place-items: center;
  width: 104px;
  height: 104px;
  margin: -52px 0 0 -52px;
  font-size: 3rem;
  font-weight: 500;
  line-height: 1;
  pointer-events: none;
  will-change: transform, opacity;
  text-shadow: 0 2px 22px color-mix(in srgb, currentColor 40%, transparent);
  animation: ratepop-rise 700ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

.ratepop--0 { color: var(--critical); }
.ratepop--1 { color: var(--warning-text); }
.ratepop--2 { color: var(--good); }

@keyframes ratepop-rise {
  0% {
    opacity: 0;
    transform: translate(0, 4px) scale(0.86);
  }
  22% {
    opacity: 0.9;
    transform: translate(calc(var(--dx, 0px) * 0.35), -6px) scale(1);
  }
  55% {
    opacity: 0.75;
    transform: translate(calc(var(--dx, 0px) * 0.7), -13px) scale(1) rotate(calc(var(--rot, 0deg) * 0.6));
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx, 0px), -20px) scale(0.99) rotate(var(--rot, 0deg));
  }
}

/* A single soft ring for Confident, so the best answer is the one that feels
   biggest without anything getting louder. */
.ratepop::before {
  content: '';
  position: absolute;
  inset: 12px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  opacity: 0;
}

.ratepop--2::before {
  animation: ratepop-ring 620ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

@keyframes ratepop-ring {
  0% { opacity: 0.4; transform: scale(0.6); }
  100% { opacity: 0; transform: scale(1.35); }
}

@media (prefers-reduced-motion: reduce) {
  .ratepop { display: none; }
}

/* ------ the wall ------
 *
 * The one screen that asks a visitor for something, so it is allowed to look
 * different from the cards around it: a tinted panel rather than another
 * surface-1 box that reads as one more section to scroll past.
 *
 * Everything is a token, both themes included, because this is the block a
 * stranger is most likely to be looking at and a wall that renders as dark
 * text on a dark ground is a wall that sells nothing. */
.wall {
  margin-top: 18px;
  padding: 26px 24px;
  border: 1px solid var(--wall-border);
  border-radius: var(--radius);
  background: var(--wall-bg);
  text-align: center;
}

.wall h2 {
  margin: 0 0 6px;
  font-size: 1.35rem;
  text-wrap: balance;
}

.wall__lead {
  margin: 0 auto;
  max-width: 56ch;
  color: var(--text-secondary);
  line-height: 1.55;
}

.wall__done {
  margin: 14px 0 0;
  color: var(--text-primary);
}

.wall__done b {
  font-variant-numeric: tabular-nums;
}

.wall__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-top: 18px;
}

.wall__note {
  margin: 14px auto 0;
  max-width: 60ch;
  font-size: 0.875rem;
  color: var(--muted);
  line-height: 1.5;
}

/* ------ carrying try-it progress into an account ------
 *
 * A strip above whatever view is on screen, shown once and only to somebody
 * who practised before signing up. Deliberately quieter than .wall: this one
 * is not selling anything, it is asking a yes or no question about data that
 * is already theirs. */
/* The terms strip. Same shape as the adoption strip below it, and a quieter
   surface on purpose: this is a notice, not an offer, and it should not
   compete with the thing that asks a visitor to keep their progress. */
.terms-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.terms-strip__text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.terms-strip__text span {
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.terms-strip__links {
  display: flex;
  gap: 14px;
  margin-top: 2px;
}

.terms-strip__actions {
  flex: none;
}

.adopt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
  padding: 14px 16px;
  border: 1px solid var(--wall-border);
  border-radius: var(--radius);
  background: var(--wall-bg);
}

.adopt__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.adopt__text span {
  color: var(--text-secondary);
  font-size: 0.9rem;
}

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

/* ------------------------------------------------- the shelf accordion --- */

/* A grid of cards, as the shelf has always been, except that the open one
   spans the full width so its map has room to be worth looking at. */
.brows {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: 14px;
  align-items: start;
}

.brow {
  /* Each card measures itself, because the answer depends on how wide THIS
     card is and not on the window. A closed card is one 270px column of the
     shelf grid; the open one spans the whole row. A viewport breakpoint cannot
     tell those apart and would wrap the open card at the same moment. */
  container-type: inline-size;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.16));
  border-radius: 12px;
  overflow: hidden;
  transition: border-color 0.18s ease, background-color 0.18s ease;
}

/* Yours are brighter than the ones you cannot open yet. The shelf already
   sorts them to the top; this is what makes that visible at a glance rather
   than only on reading the names. */
.brow--locked {
  border-color: rgba(255, 255, 255, 0.07);
}

.brow--locked .brow__name { color: var(--text-secondary); }
.brow--locked .brow__blurb { opacity: 0.72; }

/* The open one takes the whole row. Anything narrower turns the map into a
   thumbnail, and the map is the reason to open a bundle at all. */
.brow.is-open {
  grid-column: 1 / -1;
  background: rgba(255, 255, 255, 0.03);
  border-color: rgba(255, 255, 255, 0.22);
}

/* ---------------------------------------------- the switcher rail --------

   Two courses or more and the shelf stops being one column of rows: the open
   course keeps the left, and the others stand beside it. The map is still the
   widest thing on the screen and the next course no longer lives below the
   fold.

   Only the group that holds the open card splits, and only when it has
   something to switch to. Everything else uses .brows unchanged. */
.brows--split {
  display: grid;
  /* minmax(0, 1fr) not 1fr: a grid item floors at its content's min size, and
     the map's canvas would push the left track wider than the row and shove
     the rail off the edge. */
  grid-template-columns: minmax(0, 1fr) minmax(250px, 300px);
  gap: 14px;
  align-items: start;
}

/* The open card spans the whole row in .brows, where the row is the shelf.
   Here the row is two named places and it belongs in the first, so the span
   has to be given back or it would sit under its own rail. */
.brows--split .brow.is-open {
  grid-column: auto;
}

.brows__rail {
  display: grid;
  gap: 14px;
  align-content: start;
}

/* The way out of the rail: same box, same size, plainly not a course.

   THE SAME GREY AS .shelf-offer, deliberately. That panel at the foot of the
   shelf is the other signpost on this screen, and two signposts that look like
   each other teach the reader one thing instead of two. Tokens rather than
   literals, so the pair stay matched in both themes.

   Not a filled button: a control at the foot of a column of courses reads as
   an eleventh course you have to buy. A signpost should look like the thing it
   stands next to without pretending to be one. */
.brow--more {
  border-color: var(--border);
  background: var(--surface-2);
}

.brow--more .brow__name {
  color: var(--text-secondary);
  font-weight: 600;
}

.brow--more:hover {
  border-color: var(--series-1);
}

.brow--more:hover .brow__name,
.brow--more:hover .brow__more-arrow {
  color: var(--series-1);
}

/* Pushed to the far edge of the name row, where it reads as the direction the
   box goes in rather than as punctuation after the word "courses". */
.brow__more-arrow {
  margin-left: auto;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text-secondary);
  transition: transform 0.15s ease;
}

.brow--more:hover .brow__more-arrow {
  transform: translateY(2px);
}

@media (prefers-reduced-motion: reduce) {
  .brow--more:hover .brow__more-arrow { transform: none; }
}

/* The rail's second line: progress instead of the description. Rendered, not
   hidden - see the comment on it in app.js. */
.brow__mini {
  display: block;
  font-size: 0.82rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* A hairline of the same progress, under the words rather than beside them.
   The full .course__bar is 6px and belongs to the open card, where it is one
   of several figures; at this size it is a texture that tells you which of
   your courses is nearly done without your having to read any of them. */
.brow__rail-bar {
  display: block;
  /* No margin-top beyond this: .brow__head is a flex column with a 4px gap,
     and a margin here would be added to it rather than replace it. */
  margin-top: 2px;
  height: 3px;
  border-radius: 999px;
  background: var(--surface-2, rgba(255, 255, 255, 0.08));
  overflow: hidden;
}

.brow__rail-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--series-1);
}

/* Narrow: the rail goes back under the map rather than squeezing both. The
   measure is the shelf's own width, not the window's, because the app's
   content column is not the viewport and a breakpoint on the window would
   collapse this at the wrong moment. */
@container shelf (max-width: 820px) {
  .brows--split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The header row: the expand button takes the width, the course link sits
   at its top right. The link is a SIBLING of the button, never inside it - a
   link nested in a button is invalid and the click target becomes a coin
   toss between expanding and navigating. */
.brow__bar {
  display: flex;
  /* Centred on the row, so the button sits against the whole two-line block
     rather than against the title alone. This is also what removed the hand
     tuned top margin: the head defines the row height and the button now
     finds its own middle, whatever either of them grows to. */
  align-items: center;
  gap: 10px;
}

/* min-width 0 so the head may actually shrink: a flex item defaults to
   min-width auto, which floors it at its longest word. */
.brow__bar .brow__head { flex: 1 1 auto; min-width: 0; }

/* Narrow card: the button drops under the title rather than squeezing it. At
   270px a button beside the heading put "Verbs that carry you" on four lines.
   Wide card: it stays on the row, where there is room for both. */
@container (max-width: 460px) {
  .brow__bar { flex-wrap: wrap; }
  /* Room under it as well as beside it. Wrapped onto its own line the button
     sat flush against the card's bottom border, which read as a rendering
     fault rather than as a control. */
  .brow__bar .brow__cta { margin-left: 18px; margin-bottom: 16px; }
}

/* Bigger than the btn--sm it inherits from. The name and the blurb make a
   two-line block about 47px tall, and a 30px button beside that read as an
   afterthought rather than as the thing you are meant to press. */
/* ONE HEIGHT FOR EVERY BUTTON ON THE SHELF. Start and Get this course sit in
   the same place on cards that are one click apart, and a control that changes
   size as you move between them reads as the page shifting under you rather
   than as two of the same thing. */
.brow__cta {
  flex: 0 0 auto;
  min-height: 54px;
  padding: 0 22px;
  margin-right: 18px;
  font-size: 0.95rem;
  font-weight: 600;
  white-space: nowrap;
}

/* The start button: a quiet label over the chapter name, not one line of text
   with a dot in the middle of it. Set at the same size and weight, "Start",
   the separator and the chapter title read as a string that got printed
   rather than as the primary action on the card.

   BOTH CLASSES IN THE SELECTOR. .brow__cta sets min-height and padding a few
   rules up, and a single-class selector here would win or lose on where it
   sits in the file. This wins on specificity, which does not move when
   somebody reorders the sheet. */
/* A FIXED WIDTH, not a width that follows the chapter name. The name under the
   label changes every time you switch course - 174px on the shortest, 267px on
   the longest - and a button that resized itself under the cursor was the one
   thing about switching that did not feel solid. It is set wide enough for the
   longest chapter in the product, so nothing truncates and nothing moves.

   max-width keeps it inside a card narrower than itself, which is the phone
   layout, where the bar has already wrapped the button onto its own line. */
.brow__cta.brow__start {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 290px;
  max-width: 100%;
  padding: 8px 22px;
  line-height: 1.25;
  text-align: center;
}

.brow__start-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  opacity: 0.72;
}

/* There is nothing to label in the moment before the structure lands, and
   nothing to label on a course with every chapter done. An empty span that
   still held a line would push the one line the button does have off centre. */
.brow__start-label:empty {
  display: none;
}

/* The ellipsis is a backstop, not a plan: the longest chapter name in the
   product sits comfortably inside the max-width. It is here so that a longer
   one added later shortens itself instead of stretching the button across the
   card and pushing the course name into four lines. */
.brow__start-topic {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* Outlined, not filled and not bare.
   A filled primary button on all ten locked cards turned the shelf into ten
   asks. A fully transparent one was the opposite mistake: it read as a label
   and stopped looking like something you could press. An outline is quiet and
   still unmistakably a control. */
.brow__get {
  border: 1px solid var(--border);
  background: var(--surface-1);
  color: var(--text-primary);
  text-decoration: none;
  font-weight: 600;
}

.brow__get:hover {
  border-color: var(--series-1);
  color: var(--series-1);
  background: var(--surface-2);
}

/* Two lists rather than one wall. The heading only appears when both groups
   have something in them, so a new account sees a plain shelf instead of a
   "Your courses" heading over an empty space. */
/* A named container so the split rail can measure the shelf rather than the
   window. Named rather than anonymous because the cards inside are containers
   too: an unnamed query would resolve to the nearest one, which is the card,
   and the rail would be asking the wrong element how wide it is. */
.shelf-group {
  display: block;
  container-type: inline-size;
  container-name: shelf;
}
.shelf-group + .shelf-group { margin-top: 30px; }

/* A heading, not a label. At 0.82rem uppercase these read as eyebrows over
   the cards; at this size they read as the two halves of the shelf, which is
   what they are. */
.shelf-group__title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 10px;
  font-size: 1.25rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

.shelf-group__count {
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--muted);
}

.shelf-group__note {
  margin: 0 0 12px;
  font-size: 0.9rem;
  color: var(--muted);
  max-width: 60ch;
}

.shelf-group__title + .brows { margin-top: 10px; }

/* The one quiet ask on the shelf, after the courses rather than before them.
   Somebody who reaches the end of the list has browsed; that is when an offer
   helps instead of interrupting. */
.shelf-offer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  margin-top: 18px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.shelf-offer__text {
  margin: 0;
  color: var(--text-secondary);
  font-size: 0.95rem;
  max-width: 52ch;
}

/* ------------------------------------------------------------- plans ----- */

/* The till. Three offers side by side, each carrying its own choice, so that
   deciding never takes over a screen somebody was using for something else.
   Deliberately plain: no badge naming a favourite, no struck-through price,
   no urgency. The comparison the cards make is arithmetic that is true. */
.plans {
  display: flex;
  flex-direction: column;
  gap: 28px;
  max-width: 1100px;
  margin: 0 auto;
}

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

.plans__title { margin: 0; font-size: 1.7rem; letter-spacing: -0.01em; }

.plans__sub {
  margin: 0;
  color: var(--text-secondary);
  max-width: 60ch;
  line-height: 1.55;
}

/* What they already have, said first. Leading with what is free is the
   difference between an invitation and a demand. */
.plans__free {
  margin: 0;
  color: var(--muted);
  font-size: 0.92rem;
}

.plans__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  align-items: start;
}

/* The four bands of every card line up across all three, so the comparison can
   be read across as well as down. Without this each card sets its own rhythm
   from the length of its own sentence, and the tick lists start at three
   different heights, which is the one thing a comparison must not do.

   Behind @supports because the fallback is what the cards already do: flex,
   stacked, correct, merely not aligned to the pixel. */
@supports (grid-template-rows: subgrid) {
  .plans__grid { grid-template-rows: repeat(4, auto); }
  .plans__grid .plan {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 4;
    align-content: start;
  }
}

.plan {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 22px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  box-shadow: var(--shadow);
  height: 100%;
}

/* The subscription is the only recurring offer, so it is the only one tinted.
   That is a distinction of KIND, not a recommendation. */
.plan--wall {
  background: var(--wall-bg);
  border-color: var(--wall-border);
}

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

.plan__name { margin: 0; font-size: 1.12rem; font-weight: 650; }

.plan__price {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-variant-numeric: tabular-nums;
}
.plan__price b { font-size: 1.85rem; font-weight: 640; letter-spacing: -0.02em; }
.plan__price span { color: var(--muted); font-size: 0.9rem; }

.plan__line {
  margin: 0;
  color: var(--text-secondary);
  font-size: 0.93rem;
  line-height: 1.5;
}

/* The value ladder, and the thing that closes the gap in the shortest card.
   The first version pushed a single caveat line to the bottom with margin-top
   auto, which aligned the buttons and left a hole the height of the difference
   between the cards. Same rows on every card fills that space with the
   comparison itself, which is what somebody is there to make. */
.plan__feats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.plan__feat {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 9px;
  align-items: start;
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--text-primary);
}

.plan__tick {
  font-size: 0.95rem;
  line-height: 1.35;
  /* Bold 15.2px is not "large text", so this needs 4.5:1. --good is 3.27:1. */
  color: var(--success-text);
  font-weight: 700;
  text-align: center;
}

/* Not shouted, not hidden. A line the tier does not include is part of the
   comparison and has to stay readable; it is dimmed, never struck through. */
.plan__feat.is-off { color: var(--muted); }
.plan__feat.is-off .plan__tick { color: var(--axis); font-weight: 400; }

/* Name on the left, marker on the right. They were stacked, which pushed the
   price down and left the top of the widest card looking emptier than the two
   beside it. */
.plan__namerow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

/* One quiet marker on the richest tier. It states a fact about what is in the
   box, not a claim about what other people bought. */
/* The descriptor beside a plan's name: what the tier IS, in plain words, next
   to what it is called. Quiet by default; the subscription's is tinted because
   that card is tinted, not because it is being recommended. */
.plan__flag {
  margin: 0;
  align-self: center;
  white-space: nowrap;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 100px;
  background: var(--surface-2);
}

.plan--wall .plan__flag {
  color: var(--series-1);
  border-color: var(--wall-border);
  background: var(--surface-1);
}

/* The chooser and its button sit at the bottom of every card, so the three
   Continue buttons line up however tall the cards are. margin-top is declared
   HERE rather than in a rule of its own: a separate .plan__choose block was
   overridden by this one's margin shorthand, which is the sort of collision
   that looks fine in the source and does nothing in the browser. */
/* Top of its band, not bottom.
   The three choosers are very different heights - a select, a scrolling list
   of eleven, and two segments - and subgrid sizes the band to the tallest. Sat
   at the bottom of that band, the short ones floated with dead space above
   them and read as misplaced. At the top they read as following the list they
   belong to, and the slack falls between the chooser and the button where it
   is quiet. The buttons still line up, because they have a band of their own. */
.plan__choose {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding: 0;
  border: 0;
}

/* The radio group inside .plan__choose, on the subscription card only, and
   only when there is more than one interval to choose between. Same reset as
   .plan__choose itself, kept as a rule of its own because it nests inside
   that div rather than replacing it - see planEverythingCard() in app.js. */
.plan__interval {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding: 0;
  border: 0;
}

.plan__label {
  padding: 0;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.plan__select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.94rem;
}

.plan__scroll {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 158px;
  overflow-y: auto;
  padding-right: 2px;
}

.plan__opt {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.93rem;
  line-height: 1.35;
}
.plan__opt:hover { background: var(--surface-2); }
.plan__opt input { width: 17px; height: 17px; flex: 0 0 auto; cursor: pointer; }

/* Full rather than forbidden: the remaining options grey out once the count is
   reached, so the limit is visible instead of being explained by an error. */
.plan__opt.is-full { color: var(--muted); cursor: default; }
.plan__opt.is-full:hover { background: none; }
.plan__opt.is-full input { cursor: default; }

.plan__count {
  margin: 0;
  font-size: 0.86rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The sentence that answers the one dash on this card. It sits with the
   chooser rather than in the shared fine print, because it is the answer to a
   worry somebody only has while looking at THIS option. */
.plan__reassure {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.45;
  color: var(--text-secondary);
}

.plan__segs { display: flex; gap: 8px; }

.plan__seg {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  cursor: pointer;
  line-height: 1.3;
}
.plan__seg.is-on { border-color: var(--series-1); box-shadow: inset 0 0 0 1px var(--series-1); }
.plan__seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.plan__seg-when { font-size: 0.8rem; color: var(--muted); }
.plan__seg-price { font-weight: 640; font-variant-numeric: tabular-nums; }
.plan__seg:has(input:focus-visible) { outline: 2px solid var(--series-1); outline-offset: 2px; }

.plan__go { width: 100%; justify-content: center; }

.plans__fine {
  margin: 0;
  color: var(--muted);
  font-size: 0.86rem;
  max-width: 62ch;
  line-height: 1.5;
}

.plans__back { margin: 0; font-size: 0.92rem; }

@media (max-width: 560px) {
  .plans__grid { grid-template-columns: 1fr; }
  .plan { padding: 18px; }
}

.brow__head {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  padding: 16px 18px;
  border: 0;
  background: none;
  color: var(--text-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* The hover belongs to the whole row, not to the button inside it.

   It used to sit on .brow__head, which is only as wide as the text column -
   it stopped 207px short of the card edge, so you could see precisely where
   the course button had been slotted in. */
.brow__bar:hover { background: rgba(255, 255, 255, 0.05); }

.brow__head:focus-visible {
  outline: 2px solid var(--text-secondary);
  outline-offset: -3px;
}

.brow__name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 1rem;
  font-weight: 600;
}

.brow__blurb,
.brow__meta {
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.4;
}

.brow__meta { margin: 0 0 10px; }
.brow__lock { font-size: 0.8rem; opacity: 0.6; }

.brow__body { padding: 0 18px 18px; }

/* Switching bundles rebuilds the DOM, so this is an animation on mount rather
   than a transition between states: the body arrives rising and fading in,
   which reads as the card opening instead of the page flickering. */
.brow.is-open .brow__body {
  animation: brow-open 0.26s cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

@keyframes brow-open {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}

/* The map is the slowest part to appear, so it comes in just behind the
   numbers rather than with them. */
.brow.is-open .brow__map {
  animation: brow-open 0.3s 0.06s cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .brow,
  .brow.is-open .brow__body,
  .brow.is-open .brow__map {
    transition: none;
    animation: none;
  }
}

/* The three ratings the user actually gives, in the same order and the same
   colours the map uses, so a dot on the graph and a number here mean the
   same thing without anybody explaining it. */
.brow__stats {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-bottom: 8px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

.brow__stat { display: inline-flex; align-items: center; gap: 6px; }

.brow__stat i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}

.brow__stat--high i { background: #30a46c; }
.brow__stat--mid i { background: #e8a317; }
.brow__stat--low i { background: #e5484d; }

.brow__map { margin-top: 12px; }

/* The stop control sits in the practice bar, between the set name and the
   options. Quiet: it is the way out, not the thing you came to do. */
.pbar__stop {
  flex: none;
  margin-left: auto;
}

/* The two numbers that only exist at the end of a sitting. */
.summary__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 14px 0 0;
  color: var(--text-secondary);
  font-size: 0.85rem;
}

.summary__meta b { color: var(--text-primary); }

/* ============================================== the map of all your words = */

/*
 * A canvas that fills the space it is given, with the counts above and a key
 * below. The stage has an explicit height because a canvas in a flex column
 * with no height collapses to zero and draws nothing - silently, which is the
 * worst way for a picture to fail.
 */
.wordmap {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.wordmap__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.wordmap__counts {
  color: var(--muted);
  font-size: 0.9rem;
}

.wordmap__stage {
  position: relative;
  height: min(68vh, 720px);
  min-height: 320px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  overflow: hidden;
}

.wordmap__stage canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  /* The wheel is zoom here, so the page must not scroll under it, and a drag
   * on a phone is a pan rather than a scroll. */
  touch-action: none;
}

.wordmap__key {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  color: var(--muted);
  font-size: 0.82rem;
}

.wordmap__key span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.wordmap__key .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  display: inline-block;
}

/* The same five colours the canvas uses. Kept beside it so a change to one is
 * an obvious prompt to change the other. */
.dot--confident { background: #3ddc84; }
.dot--moderate  { background: #f1bf00; }
.dot--shaky     { background: #ff7a7a; }
.dot--unmet     { background: #3c3833; }
.dot--own       { background: #7ac8ff; }

.wordmap__actions {
  display: flex;
  gap: 8px;
}

/* The interaction is not discoverable from a field of dots, so it is said
 * once, quietly, beside the key rather than as a tooltip nobody opens. */
.wordmap__hint {
  margin-left: auto;
  opacity: 0.75;
}

/* ------------------------------------------- the subscriber tool gate ---- */

/*
 * What somebody sees where a tool they have not got would be.
 *
 * A panel, not a modal and not a banner. A modal has to be dismissed before
 * the screen underneath can be read, which is a toll on somebody who arrived
 * by pressing a button they thought was theirs; a banner is ignorable and
 * would leave the screen looking broken instead of closed.
 *
 * The surface and border are the shelf's, so this reads as part of the app
 * rather than as an advert that has landed on it.
 */
.gate {
  margin-bottom: 18px;
  padding: 26px 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

/* Above the heading, small, and the only place the product is named. It
   answers "why am I seeing this" before the heading answers "what is it". */
.gate__tag {
  margin: 0 0 8px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--series-1);
}

.gate__title {
  margin: 0 0 8px;
  font-size: 1.3rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--text-primary);
}

/* What the tool does, before what it costs. Held to a readable measure: this
   is the one paragraph on the screen that has to actually be read. */
.gate__what {
  margin: 0;
  max-width: 58ch;
  color: var(--text-secondary);
  line-height: 1.55;
}

.gate__actions {
  margin-top: 18px;
}

/*
 * THE CONTROLS THAT WRITE TO A GATED ROUTE, hidden by one rule rather than by
 * a condition at each place they are drawn.
 *
 * Edit and delete appear on the list rows, in the practice screen, in the
 * translation panel and in the word detail, and that list is the kind that
 * grows one site at a time. A selector keyed off the body catches the sites
 * written next year as well as the four written so far.
 *
 * This hides; it does not protect. Every one of these routes is gated on the
 * server, which is what makes it safe for this to be a stylesheet.
 */
body.no-word-list #add-card,
body.no-word-list #bulk-open,
body.no-word-list #import-btn,
/* Re-check rewrites the glosses on words already saved, which is a PATCH per
   word. Reading and exporting stay: those take nothing that was not given. */
body.no-word-list #recheck-open,
body.no-word-list [data-edit],
body.no-word-list [data-delete] {
  display: none;
}

/* ------------------------------------------------- the email list form --- */
/*
 * Built entirely out of the existing custom properties, deliberately. The
 * design system audit counted 46 hex colours and 53 font sizes on this site
 * already; a signup form is not the place to add the 47th. Every value below
 * is a var() that something else on the page is using too, so this block
 * follows the theme in both directions without defining a palette of its own.
 */
.listsign {
  border-top: 1px solid var(--border);
  padding-top: 1.5rem;
  margin-bottom: 1.5rem;
  max-width: 34rem;
}

.listsign__title {
  font-size: 1.05rem;
  margin: 0 0 0.35rem;
  color: var(--text-primary);
}

.listsign__pitch {
  margin: 0 0 0.9rem;
  color: var(--text-secondary);
}

/*
 * The label is for a screen reader, not for the eye: the placeholder and the
 * heading above already say what the field is, and a visible label here made
 * the footer look like a form to be filled in rather than an offer. Hidden
 * with the clip pattern rather than display:none, because display:none takes
 * it out of the accessibility tree as well and leaves the input unnamed.
 */
.listsign__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.listsign__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.listsign__input {
  flex: 1 1 14rem;
  min-width: 0;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--plane);
  color: var(--text-primary);
  font: inherit;
}

.listsign__input:focus-visible,
.listsign__send:focus-visible,
.listsign__consent input:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

.listsign__send {
  padding: 0.6rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--series-1);
  color: #ffffff;
  font: inherit;
  cursor: pointer;
}

.listsign__send[disabled] {
  opacity: 0.6;
  cursor: default;
}

.listsign__consent {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  margin-top: 0.75rem;
  color: var(--text-secondary);
}

.listsign__consent input {
  margin-top: 0.2rem;
  flex: none;
}

/*
 * Empty until something happens, and it must not reserve space while empty:
 * a permanent gap under the form reads as a rendering fault. aria-live on the
 * element in the markup is what makes the message reach a screen reader when
 * it does appear.
 */
.listsign__status:empty {
  display: none;
}

.listsign__status {
  margin: 0.75rem 0 0;
}

.listsign__status[data-kind="ok"] { color: var(--success-text); }
.listsign__status[data-kind="error"] { color: var(--critical); }
.listsign__status[data-kind="working"] { color: var(--text-secondary); }

.listsign__small {
  margin: 0.6rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}
