/* Page chrome for repertoire.html — the database and the learning screen.

   Every color is a variable from theme.css, including the seven level colors
   and the three side badges. Nothing here is a literal, so restyling the whole
   tab means editing theme.css and pressing F5. */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ui-bg);
  color: var(--ui-text);
  font: 0.875rem/1.5 var(--ui-font); /* 14px */
}

.bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.625rem 1.125rem;
  background: var(--ui-panel);
  border-bottom: 1px solid var(--ui-line);
  position: sticky;
  top: 0;
  z-index: 10;
}

.brand { font-weight: 700; letter-spacing: 0.14em; font-size: 0.8125rem; /* 13px */ }
.grow { flex: 1; }
.row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.625rem; }

.inline {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--ui-dim);
  cursor: pointer;
  user-select: none;
}

.hint { color: var(--ui-dim); font-size: 0.75rem; margin: 0.25rem 0 2px; }
.msg { font-size: 0.75rem; color: var(--ui-dim); }
.msg.bad { color: var(--learn-wrong); }
.msg.good { color: var(--learn-right); }

.btn {
  font: inherit;
  color: var(--ui-text);
  /* --ui-btn, not --ui-panel: a button needs to be visibly lighter than the
     panel it sits on, or only its 1px border is visible and it reads as an
     outline rather than a pressable control. See theme.css, item 11. */
  background: var(--ui-btn);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 0.375rem 0.75rem;
  cursor: pointer;
}
@media (hover: hover) { .btn:hover { background: var(--ui-btn-hover); border-color: var(--ui-accent); } }
.btn:disabled { opacity: 0.45; cursor: default; border-color: var(--ui-line); }
.btn.primary { background: var(--ui-accent); color: var(--ui-accent-text); border-color: var(--ui-accent); font-weight: 600; }
@media (hover: hover) { .btn.primary:hover { filter: brightness(1.08); } }
.btn.small { padding: 0.1875rem 0.5625rem; font-size: 0.75rem; }
.btn.ghost { background: transparent; border-color: transparent; color: var(--ui-dim); }
@media (hover: hover) { .btn.ghost:hover { color: var(--ui-text); border-color: var(--ui-line); background: transparent; } }

input[type="text"], input[type="search"], input[type="number"], textarea, select {
  font: inherit;
  width: 100%;
  color: var(--ui-text);
  background: var(--ui-bg);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 0.375rem 0.5rem;
}
input:focus, textarea:focus, select:focus { outline: 1px solid var(--ui-accent); }
textarea { resize: vertical; font-family: var(--ui-mono); }

.field-label { display: block; font-size: 0.75rem; color: var(--ui-dim); margin: 0.625rem 0 0.25rem; }

.panel {
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 0.75rem 0.875rem;
}

/* ── layout ─────────────────────────────────────────────────────────────── */

.layout {
  display: flex;
  flex-wrap: wrap;
  gap: 1.375rem;
  padding: 1.25rem;
  margin: 0 auto;
  max-width: 87.5rem;
  align-items: flex-start;
}

.side { flex: 0 0 16.25rem; min-width: min(15rem, 100%); display: grid; gap: 1rem; }
.main { flex: 1 1 32.5rem; min-width: min(20rem, 100%); }

/* Where the sidebar and the main column stop fitting side by side (16.25 +
   32.5 + the gap and padding is about 842px), the sidebar used to stack ABOVE
   everything — so on a phone the weekly log started below the fold, under
   "+ Variation" and the Today panel. The log is the home screen's centrepiece,
   so there it is lifted out and put first: `display: contents` lets the main
   column's children take part in this flex row, and `order` puts the log and
   Start (and the page message) before the sidebar, and the rest after it. */
@media (max-width: 842px) {
  .layout { gap: 1rem; }
  .layout > .side { flex: 1 1 100%; }
  .layout > .main { display: contents; }
  .layout > .main > * { flex: 1 1 100%; min-width: 0; order: 1; }
  .layout > .main > .home { order: -2; }
  .layout > .main > .pagemsg { order: -1; }
  .layout > .main > [hidden] { display: none; }
}

/* ── the folder tree ────────────────────────────────────────────────────── */

/* `minmax(0, 1fr)`, for exactly the reason `.list` below needs it: a grid track
   sizes to its widest item by default and does NOT shrink to its container. One
   folder called "D15 Slav Defense: Chebanenko Variation, Advance System" made
   this 406px wide inside a 260px sidebar, and the overflow lay straight on top
   of the variation cards. The ellipsis on .name cannot help while the track is
   free to grow to fit it. */

/* ── the course grid ────────────────────────────────────────────────────────
   The main screen. A repertoire is a set of courses; this is that set, at a
   size you can take in at a glance, instead of the first hundred of a
   thousand PGN cards. */

/* ── the overview ──────────────────────────────────────────────
   What the tab opens on: two rows of numbers above the course cards. Sized so
   the whole thing plus the first row of courses is on screen at 900px tall — a
   summary you have to scroll to is not a summary. */

.overview { margin-bottom: 1.25rem; }
.overview[hidden] { display: none; }

.ovhead { display: flex; align-items: baseline; gap: 0.625rem; margin-bottom: 0.75rem; }
.ovhead h1 { font: 700 1.25rem/1.2 var(--ui-font); margin: 0; }

.ovtotals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.375rem, 1fr));
  gap: 0.75rem;
}

.ovbig {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  text-align: left;
  padding: 0.875rem 1rem;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-panel);
  color: var(--ui-text);
  font: inherit;
  cursor: pointer;
}
@media (hover: hover) { .ovbig:hover { border-color: var(--ui-accent); } }
.ovnum { font: 700 1.875rem/1 var(--ui-font); /* 30px */ font-variant-numeric: tabular-nums; }
.ovlabel { font-size: 0.75rem; color: var(--ui-dim); }

/* "Never studied" is the one number here you can do something about tonight,
   so it is the one that is coloured. */
#ovNew .ovnum { color: var(--ui-accent); }

.ovsub {
  font: 600 max(0.6875rem, var(--text-min))/1 var(--ui-font); /* 11px */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-dim);
  margin: 1.125rem 0 0.5rem;
}

.ovlevels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: 0.5rem;
}

/* Centred and bigger since 2026-09-14, when the share-of-repertoire bar under
   each count was removed: the number IS the tile now. */
.ovlevel {
  display: flex;
  flex-direction: column;
  gap: 0.3125rem;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0.875rem 0.5rem 0.75rem;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-panel);
  color: var(--ui-text);
  font: inherit;
  cursor: pointer;
}
@media (hover: hover) { .ovlevel:hover { border-color: var(--lvl); } }
/* A level with nothing at it is still worth showing — the ladder has seven
   rungs whether or not you are standing on all of them — but it must not
   compete with the ones that have something on them. */
.ovlevel.empty { opacity: 0.45; }

.ovlnum {
  font: 800 1.875rem/1 var(--ui-font); /* 30px */
  font-variant-numeric: tabular-nums;
  color: var(--lvl);
}
.ovlname { font-size: 0.8125rem; /* 13px */ font-weight: 600; color: var(--ui-dim); }

/* "By level" and its "?". The heading keeps its small-caps look; the button is
   a round thing the size of a letter, so it reads as help and not as a tool. */
.ovsubrow { display: flex; align-items: center; gap: 0.5rem; margin: 1.125rem 0 0.5rem; }
.ovsubrow .ovsub { margin: 0; }
.helpbtn {
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  border: 1px solid var(--ui-line);
  background: var(--ui-btn);
  color: var(--ui-dim);
  font: 700 0.75rem/1 var(--ui-font);
  cursor: pointer;
  padding: 0;
}
@media (hover: hover) { .helpbtn:hover { color: var(--ui-accent); border-color: var(--ui-accent); } }
.helpbtn.is-on { background: var(--ui-accent); border-color: var(--ui-accent); color: var(--ui-accent-text); }
@media (pointer: coarse) { .helpbtn { width: var(--touch-target); height: var(--touch-target); } }
.levelshelp {
  margin: 0 0 0.625rem;
  padding: 0.5rem 0.75rem;
  border-left: 3px solid var(--ui-accent);
  background: var(--ui-panel);
  border-radius: 0 var(--ui-radius) var(--ui-radius) 0;
  font-size: 0.8125rem;
  color: var(--ui-text);
}

/* ── the weekly log ────────────────────────────────────────────────────────
   The home screen's centrepiece since 2026-09-14. A coloured card, because it
   is the one thing on this screen that is about YOU rather than about the
   repertoire, and it should be the first thing the eye lands on.

   Every colour is a theme token. Until 2026-09-15 the card was a violet
   gradient with white text; José kept its shape and borders and asked for the
   colours of the light and dark themes instead. So it is a panel now — the
   theme's panel ground, its text, its line — and the colour lives only where
   it carries meaning: the top edge of each stat, in that stat's level colour,
   and a thin accent edge across the top of the card. */

.home { margin-bottom: 0.25rem; }
.home[hidden] { display: none; }

.weeklog {
  --wl-ink: var(--ui-text);
  position: relative;
  overflow: hidden;
  padding: 1rem 1.125rem 1.125rem;
  border-radius: calc(var(--ui-radius) * 2);
  color: var(--wl-ink);
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-top: 3px solid var(--ui-accent);
}

.wlhead { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.wltitle { display: flex; flex-direction: column; min-width: 7rem; text-align: center; }
.wlkicker {
  font: 700 max(0.6875rem, var(--text-min))/1.2 var(--ui-font); /* 11px */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ui-dim);
}
.wllabel { margin: 0; font: 800 1.375rem/1.2 var(--ui-font); /* 22px */ white-space: nowrap; }

.wlarrow {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1px solid var(--ui-line);
  background: var(--ui-btn);
  color: var(--wl-ink);
  font: 700 1.375rem/1 var(--ui-font);
  cursor: pointer;
  padding: 0 0 0.1875rem;
}
@media (hover: hover) { .wlarrow:hover:not(:disabled) { background: var(--ui-btn-hover); border-color: var(--ui-accent); } }
.wlarrow:disabled { opacity: 0.35; cursor: default; }
@media (pointer: coarse) { .wlarrow { width: var(--touch-target); height: var(--touch-target); } }

.wlstart {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.75rem;
  color: var(--ui-dim);
}
.wlstart select {
  width: auto;
  color: var(--ui-text);
  background: var(--ui-input-bg);
  border-color: var(--ui-line);
  padding: 0.1875rem 0.375rem;
}

.wlstats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.625rem;
  margin-top: 0.875rem;
}
@media (max-width: 700px) { .wlstats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.wlstat {
  --mark: var(--lvl-2);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.75rem 0.875rem;
  border-radius: var(--ui-radius);
  background: var(--ui-bg);
  border: 1px solid var(--ui-line);
  border-top: 3px solid var(--mark);
  min-width: 0;
}
.wlstat[data-stat="accuracy"] { --mark: var(--learn-right); }
.wlstat[data-stat="moves"] { --mark: var(--lvl-4); }
.wlstat[data-stat="time"] { --mark: var(--lvl-1); }

.wlnum {
  font: 800 2.125rem/1 var(--ui-font); /* 34px */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wlname { font-size: 0.8125rem; font-weight: 600; color: var(--ui-dim); }

/* A week with nothing in it is not an error — the numbers just step back. */
.weeklog.is-empty .wlnum { opacity: 0.7; }

.wlnote { margin: 0.625rem 0 0; font-size: 0.75rem; color: var(--ui-dim); }

/* ── Start ─────────────────────────────────────────────────────────────────
   One big button under the log. The page's own text colour on its own
   background colour, swapped — near-white on the dark theme, near-black on the
   light one — so it is the highest-contrast thing on the screen in both, and
   does not melt into the card above it. */
.startbtn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.625rem;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0.9375rem 1rem;
  border: 0;
  border-radius: calc(var(--ui-radius) * 2);
  background: var(--ui-text);
  color: var(--ui-bg);
  font: 800 1.25rem/1 var(--ui-font); /* 20px */
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: 0 0.25rem 0.875rem color-mix(in srgb, var(--ui-text) 22%, transparent);
  transition: transform var(--anim-fade) ease;
}
@media (hover: hover) { .startbtn:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--ui-text) 88%, var(--ui-accent)); } }
.startbtn:focus-visible { outline: 3px solid var(--ui-accent); outline-offset: 3px; }
.starticon { font-size: 0.9375rem; color: var(--ui-accent); }

.pagemsg { display: block; min-height: 1.125rem; margin: 0.5rem 0 0.25rem; text-align: center; }
.pagemsg:empty { min-height: 0; margin: 0; }
/* Air between Start and the repertoire's own numbers, which are a different
   subject from the week above them. */
.home:not([hidden]) ~ .overview { margin-top: 1.375rem; }

/* The choice behind Start. Two large options, stacked on a phone. */
dialog.start-dialog { width: min(36rem, 94vw); }
.startoptions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem;
  margin: 0.875rem 0 0.25rem;
}
.startopt {
  --opt: var(--ui-accent);
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  align-items: flex-start;
  text-align: left;
  min-height: 7rem;
  padding: 1rem 1.125rem;
  border: 2px solid var(--opt);
  border-radius: calc(var(--ui-radius) * 1.5);
  background: color-mix(in srgb, var(--opt) 12%, var(--ui-panel));
  color: var(--ui-text);
  font: inherit;
  cursor: pointer;
}
.startopt.intense { --opt: var(--lvl-0); }
@media (hover: hover) { .startopt:hover:not(:disabled) { background: color-mix(in srgb, var(--opt) 22%, var(--ui-panel)); } }
.startopt:disabled { opacity: 0.45; cursor: default; border-color: var(--ui-line); background: var(--ui-panel); }
.startopt .sotitle { font: 800 1.125rem/1.2 var(--ui-font); color: var(--opt); }
.startopt:disabled .sotitle { color: var(--ui-dim); }
.startopt .sohint { font-size: 0.8125rem; color: var(--ui-dim); line-height: 1.4; }

/* ── Goal and Schedule, as buttons ─────────────────────────────────────────
   They were thin fold-out rows that nobody noticed. Same <details>, same
   contents; the summary is drawn as a big, bordered button with its own
   chevron, which turns when it is open. */
.bigtoggle > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.625rem;
  padding: 0.6875rem 0.875rem;
  border: 2px solid var(--ui-accent);
  border-radius: var(--ui-radius);
  background: color-mix(in srgb, var(--ui-accent) 12%, var(--ui-panel));
  color: var(--ui-text);
  font: 700 1rem/1.2 var(--ui-font); /* 16px */
  cursor: pointer;
}
.bigtoggle > summary::-webkit-details-marker { display: none; }
.bigtoggle > summary::after {
  content: "\25B8";            /* ▸ */
  margin-left: auto;
  color: var(--ui-accent);
  transition: transform var(--anim-fade) ease;
}
.bigtoggle[open] > summary { margin-bottom: 0.375rem; }
.bigtoggle[open] > summary::after { transform: rotate(90deg); }
@media (hover: hover) { .bigtoggle > summary:hover { background: color-mix(in srgb, var(--ui-accent) 22%, var(--ui-panel)); } }
.bigtoggle .bticon { font-size: 1.0625rem; line-height: 1; }
.scheduler .bigtoggle > summary { margin-top: 0; }

/* Three across, and square. Four narrow letterboxes was what `auto-fill` at
   210px gave on a normal window: a lot of cards, none of them worth looking
   at. Three squares in the same width are half again as large each, which is
   room for a course name to be READ rather than clamped at three lines.

   `minmax(0, 1fr)` and not `1fr`, for the reason written on `.list` below: a
   grid track sizes to its widest item and does not shrink back, and a course
   called "D15 Slav Defense: Chebanenko Variation, Advance System" is a very
   wide item.

   It steps down to two and then one, because a square at one third of a 400px
   phone is a 120px card with a novel in it. */
.courses {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin-bottom: 1.125rem;
  /* Three across, but not three across a 1600px monitor: the cards are square,
     so letting them fill that width makes each one 500px tall and puts a
     repertoire of nine courses three full screens deep. This caps a card at
     roughly 300px and lets the column keep the rest. */
  max-width: 60rem;

  /* The card's three lines, loudest first: the course name, how many
     variations it holds, and what is waiting for you in it. The grid went from
     four narrow letterboxes to three squares and the type never followed, so a
     300px card was carrying 11px numbers with a lot of empty around them.
     These are the whole type scale for a card — change them here and the
     hierarchy moves together. */
  --course-name-size: 1.1875rem;
  --course-total-size: 0.875rem;
  --course-badge-size: 0.75rem;
  --course-line-gap: 0.375rem;
}
@media (max-width: 900px) { .courses { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .courses { grid-template-columns: minmax(0, 1fr); } }

/* ── BIG SCREENS — 2026-09-14 ─────────────────────────────────────────────────
   The same reasoning as the 60rem cap above, turned around. On a laptop three
   squares is right. On a monitor the page is drawn larger (theme.css, SCREEN
   SIZE), so three squares were already big, and the fourth and fifth courses
   sat two screens down beside a band of empty page. Past 1800px the page may
   be wider and the cards stop being "three across": each stays between
   --course-card-min and --course-card-max, and as many fit as the row holds.
   Cards keep the size they were designed at; there are just more in view. */
@media (min-width: 1800px) {
  .layout { max-width: var(--repertoire-wide, 150rem); }
  /* The learning screen too, and this one is not optional. With the board
     allowed to fill the height of a 4K screen (theme.css --board-max), a 2040px
     board and the 495px column beside it no longer fit inside .learnwrap's
     87.5rem, so the column WRAPPED under the board, off the bottom of the
     screen — the buttons below the fold, the exact bug this layout was built to
     prevent. Measured, not guessed: side column bottom at 2503 of 2160. The
     board's own width bound (repertoire.js, LEARN_SIDE_MIN) keeps the pair
     inside the window, so the wrapper needs no cap of its own up here.
     `.learn .learnwrap`, not `.learnwrap`: the base rule is further down this
     file, and at equal specificity the later rule wins — the first version of
     this line lost to it and changed nothing, which a measurement caught. */
  .learn .learnwrap { max-width: none; }
  .courses {
    --course-card-min: 17rem;
    --course-card-max: 19rem;
    max-width: none;
    grid-template-columns: repeat(auto-fill, minmax(var(--course-card-min), var(--course-card-max)));
  }
}

/* The pinch. Between 900px and about 1150px it is still three across, but the
   260px sidebar has taken enough of the row that a card is only 185–250px —
   smaller than it is at either of the breakpoints on both sides of this band.
   The type steps back to the size it had before, because "Against the Sicilian
   Defense" has to fit inside the square there and it is the square that is
   short, not the name that is long. */
@media (min-width: 901px) and (max-width: 1150px) {
  .courses {
    --course-name-size: 1rem;
    --course-total-size: 0.75rem;
    --course-badge-size: 0.6875rem;
  }
}

.course {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: var(--course-line-gap);
  text-align: center;
  /* Square. `min-height` is the floor for a narrow column, where the aspect
     ratio would otherwise make a card too short to hold its own name. */
  aspect-ratio: 1 / 1;
  min-height: 9.375rem;
  padding: 1.125rem 1rem;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  /* A wash of the accent rather than the flat panel grey. It is the same
     colour the rest of the app is built on, at a tenth strength — enough that
     a wall of cards reads as a wall of courses and not as empty boxes. */
  background:
    linear-gradient(160deg,
      color-mix(in srgb, var(--ui-accent) 13%, var(--ui-panel)),
      var(--ui-panel) 72%);
  color: var(--ui-text);
  cursor: pointer;
  font: inherit;
  transition: border-color 0.12s ease, background 0.12s ease;
}
@media (hover: hover) {
  .course:hover {
    border-color: var(--ui-accent);
    background:
      linear-gradient(160deg,
        color-mix(in srgb, var(--ui-accent) 24%, var(--ui-panel)),
        var(--ui-panel) 78%);
  }
}

/* "All variations" is the whole repertoire and is not one course among the
   others, so it carries the accent outright instead of a wash of it. */
.course.all {
  border-color: var(--ui-accent);
  background:
    linear-gradient(160deg,
      color-mix(in srgb, var(--ui-accent) 30%, var(--ui-panel)),
      color-mix(in srgb, var(--ui-accent) 8%, var(--ui-panel)) 80%);
}

.course .cname {
  font: 600 var(--course-name-size)/1.3 var(--ui-font);
  /* The name is the card's heading and the two lines of numbers belong
     together underneath it, so it takes a little more room below itself than
     those two take between them. */
  margin-bottom: 0.25rem;
  /* Course names arrive from Chessable at full length — "A43 Benoni Defense:
     French Benoni…" — so the card stops at four lines rather than growing to
     fit the longest one in the grid. Four, not three: the card is square now
     and has the room. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* How big the course is — its own line, because it is the one number here that
   describes the whole thing rather than the part of it asking for your evening.
   Mono, so a column of cards lines its digits up. */
.course .cn {
  color: var(--ui-dim);
  font: var(--course-total-size)/1.2 var(--ui-mono);
}

.course .cnums {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.375rem;
  align-items: center;
  font: var(--course-badge-size)/1 var(--ui-mono);
}
/* A course with nothing due and nothing new has no third line to show, and an
   empty row would still take the gap above it and push the card off centre. */
.course .cnums:empty { display: none; }
.course .cdue {
  color: var(--ui-accent-text);
  background: var(--due-now);
  border-radius: 0.5625rem;
  padding: 0.1875rem 0.4375rem;
  font-weight: 700;
}

/* Same rule as the folder tree had: the tools belong to the card they act on,
   and only appear on the one being pointed at. */
/* 2026-09-13. This one needed more than a guard.
   `display: none` until hover means that on a phone — where nothing ever
   hovers — the rename and delete buttons for a course did not exist at all.
   Not "hard to reach": unreachable, with no other route to them anywhere on
   the screen. Wrapping the hover rule in @media (hover: hover) on its own
   would have documented that rather than fixed it.

   So there are two rules now. Where a pointer exists, nothing changes: the
   tools appear on the card under the cursor, as before. Where the primary
   input is coarse, they are simply always on, which is what a phone UI does
   with a row action anyway. The cost is a slightly busier card on touch; the
   alternative was a feature that is not there. */
.course .foldertools { position: absolute; top: 0.5rem; right: 0.5rem; display: none; gap: 0.25rem; }
@media (hover: hover) { .course:hover .foldertools { display: flex; } }
@media (hover: none)  { .course .foldertools { display: flex; } }

/* ── the side panel ─────────────────────────────────────────────────────── */

.actions { display: grid; gap: 0.375rem; }
.btn.wide { width: 100%; justify-content: center; }
.leaderboard h2 { margin-top: 0; }

.listhead { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.listhead h1 { font-size: 1.125rem; /* 18px */ margin: 0 0 0.5rem; }
.counts { color: var(--ui-dim); font-size: 0.75rem; }

.filters {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.5rem 0 0.875rem;
  border-bottom: 1px solid var(--ui-line);
  margin-bottom: 0.75rem;
}
.filters input[type="search"] { width: 12.5rem; }

/* `minmax(0, 1fr)`, not `1fr`. A grid track sizes to its content by default, so
   one long movetext stretched the track, which stretched .main, which pushed the
   whole page into a horizontal scroll — found at 1,462 imported variations, and
   invisible with three. The ellipsis on .moves cannot help while its track is
   free to grow to fit it. */
.list { display: grid; gap: 0.5rem; grid-template-columns: minmax(0, 1fr); }

.card {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-left: 3px solid var(--lvl-1);
  border-radius: var(--ui-radius);
  padding: 0.625rem 0.75rem;
}
.card.due { border-left-color: var(--due-now); }
.card .body { flex: 1; min-width: 0; }
.card .title { font-weight: 600; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
/* Opening names run long — "Caro-Kann Defense: Advance Variation, Botvinnik-Carls
   Defense — 1.e4 c6…" is a real one. It gets the room the chips do not need. */
/* A button since 2026-09-01 — it opens the variation's window, the same one the
   review screen's title opens. Reset to look exactly like the text it replaced:
   a title that suddenly rendered as a grey pill would read as a control that
   had appeared, not as the name it has always been. The underline on hover is
   the whole affordance, and it matches .learnlink for the same reason. */
.card .title .nm {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
@media (hover: hover) { .card .title .nm:hover { text-decoration: underline; } }
.card .moves {
  font: 0.75rem/1.4 var(--ui-mono);
  color: var(--ui-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 2px;
}
.card .meta { display: flex; gap: 0.625rem; flex-wrap: wrap; font-size: 0.75rem; color: var(--ui-dim); margin-top: 0.25rem; }
.card .actions { display: flex; gap: 0.375rem; flex-wrap: wrap; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  font: 600 max(0.6875rem, var(--text-min))/1 var(--ui-font); /* 11px */
  padding: 0.25rem 0.5rem;
  border-radius: 0.625rem;
  white-space: nowrap;
}
/* ── a variation reviewed from part-way in ─────────────────────────────────
   The card says it twice. The chip names the move review starts at and is the
   signal that survives a narrow card; the greyed-italic run in `.moves` shows
   how much of the line is only there for reference. Colour is never the only
   carrier: the chip has a ▶ and words in it. */
.chip.startchip {
  background: transparent;
  color: var(--ui-accent);
  border: 1px solid var(--ui-accent);
}
.card .moves .ref { font-style: italic; opacity: 0.65; }

.chip.side-white { background: var(--side-white); color: var(--side-white-text); }
.chip.side-black { background: var(--side-black); color: var(--side-black-text); border: 1px solid var(--ui-line); }
.chip.side-both { background: var(--side-both); color: var(--side-both-text); }

/* Set from JS: style="--lvl: var(--lvl-3)". One rule, seven colors. */
.chip.level { background: transparent; color: var(--lvl); border: 1px solid var(--lvl); }
.chip.tokens { background: var(--lvl-0); color: var(--ui-accent-text); }

/* The fail-time countdown, on the prompt row beside "Your move". Quiet by
   design — an outline, not a fill — until the last few seconds, where it
   borrows --due-now (the same color a due variation's badge already uses)
   rather than introducing a fourth "urgent" color to the palette. */
.chip.timer {
  background: transparent;
  border: 1px solid var(--ui-line);
  color: var(--ui-dim);
  font-family: var(--ui-mono);
  font-variant-numeric: tabular-nums;   /* "9s" to "10s" should not shift width */
}
.chip.timer.low { color: var(--due-now); border-color: var(--due-now); }

/* Which Intense Mode round this is. It borrows --lvl-0, the Difficult color,
   because a Difficult variation is the only thing an Intense sitting contains
   — the chip and the level badges on the cards are talking about one set of
   variations, and should look like it. */
.chip.intense {
  background: transparent;
  border: 1px solid var(--lvl-0);
  color: var(--lvl-0);
  font-variant-numeric: tabular-nums;
}

.due-now { color: var(--due-now); font-weight: 600; }
.due-later { color: var(--due-later); }

.empty { color: var(--ui-dim); padding: 1.875rem 0; text-align: center; }
.empty.small { padding: 0.875rem 0; font-size: 0.75rem; }

/* ── the database tree (item 13) ──────────────────────────────────────────
   Cards vs Tree is a toggle, not two separate pages — same filters, same
   data, different shape. `.on` mirrors the folder tree's `.node.on`: a
   filled background plus an accent border rather than a whole second color,
   so it reads as "the pressed one" without competing with due-badges. */

.viewmode { display: flex; gap: 0.25rem; margin: -0.25rem 0 0.625rem; }
.viewmode .btn.on { background: var(--ui-accent); color: var(--ui-accent-text); border-color: var(--ui-accent); }

.dbtree { margin-top: 0.25rem; }
.dbcols {
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
  align-items: flex-start;
}
.dbcol { flex: 1 1 20rem; min-width: min(17.5rem, 100%); }
.dbcol h2 {
  font-size: 0.8125rem; /* 13px */
  margin: 0 0 0.5rem;
  padding-bottom: 0.375rem;
  border-bottom: 1px solid var(--ui-line);
  color: var(--ui-dim);
}
.dbroot { display: grid; gap: 2px; }

/* One <details> per move. `list-style: none` + the custom marker below
   replace the browser's default triangle so it can be recolored and sized
   like everything else here instead of sitting outside the theme. */
.dbnode { font-size: 0.8125rem; /* 13px */ }
.dbnode > .dbchildren { margin-left: 1rem; padding-left: 0.625rem; border-left: 1px solid var(--ui-line); margin-top: 2px; }

.dbhead {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.1875rem 0.25rem;
  border-radius: var(--ui-radius);
  cursor: pointer;
  list-style: none;
}
.dbhead::-webkit-details-marker { display: none; }
@media (hover: hover) { .dbhead:hover { background: var(--ui-panel); } }
/* A leaf node (no children) is rendered as a plain <div>, not <details> — it
   has no marker and nothing to expand, and forcing a triangle onto it would
   promise a twist that does nothing. */
div.dbhead { cursor: default; }
@media (hover: hover) { div.dbhead:hover { background: transparent; } }

.dbnode > summary.dbhead::before {
  content: '▸';
  display: inline-block;
  width: 0.625rem;
  color: var(--ui-dim);
  transition: transform var(--anim-fade) linear;
}
.dbnode[open] > summary.dbhead::before { transform: rotate(90deg); }

.dbmove { font: 600 0.8125rem/1 var(--ui-mono); /* 13px */ color: var(--ui-text); }
.dbcount { font-size: max(0.6875rem, var(--text-min)); /* 11px */ color: var(--ui-dim); }

/* The variation(s) that end exactly at this position — usually one, but two
   when different move orders transpose into the same line. */
.dbends { display: flex; flex-wrap: wrap; gap: 0.375rem; margin: 2px 0 2px 1.125rem; }
.dbend {
  font: inherit;
  font-size: 0.75rem;
  color: var(--ui-accent);
  background: transparent;
  border: 1px dashed var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 2px 0.5rem;
  cursor: pointer;
}
@media (hover: hover) { .dbend:hover { border-color: var(--ui-accent); background: var(--ui-panel); } }
.dbend.due { color: var(--due-now); border-color: var(--due-now); }

/* ── backups panel ──────────────────────────────────────────────────────── */

.backuplist { display: grid; gap: 0.25rem; margin: 0.625rem 0; max-height: 13.75rem; overflow-y: auto; }
.backuprow {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.75rem;
  padding: 0.25rem 2px;
}
.backuprow.rejected { color: var(--status-down); }
.backuprow .when { color: var(--ui-dim); }
.empty.small { font-size: 0.75rem; padding: 0.625rem 0; color: var(--ui-dim); }

/* ── the schedule ─────────────────────────────────────────────────────────
   One row per level: what it is called, and how many hours until it comes
   back. (A third column showed XP until 2026-09-14.) */

.levels { display: grid; gap: 0.25rem; margin: 0.625rem 0; }
.levels .lvlrow {
  display: grid;
  grid-template-columns: 1fr 3.875rem;
  gap: 0.375rem;
  align-items: center;
  font-size: 0.75rem;
}
.levels .lvlrow .nm { color: var(--lvl); font-weight: 600; }
.levels .head { color: var(--ui-dim); }
.levels input { padding: 0.1875rem 0.3125rem; font-size: 0.75rem; }

/* ── the leaderboard ──────────────────────────────────────────────────────
   Rank, name, and moves played. Your own row is marked, because a leaderboard
   you cannot find yourself on is a list of strangers. */

.rangetabs { gap: 0.25rem; margin: 0.5rem 0; }
.rangetabs .btn.is-on {
  border-color: var(--ui-accent);
  color: var(--ui-accent);
}

.boardlist { display: grid; gap: 0.25rem; margin: 0.5rem 0; }
/* One line: place, name, moves. There was a second row underneath for country,
   rating and days studied; it and its `.bmeta` span went on 2026-09-04 — see
   paintLeaderboard. */
.boardrow {
  display: grid;
  grid-template-columns: 1.125rem 1fr auto;
  grid-template-areas: "rank who moves";
  gap: 0 0.5rem;
  align-items: baseline;
  padding: 0.3125rem 0.375rem;
  border-radius: var(--ui-radius);
  font-size: 0.75rem;
}
.boardrow.you { background: var(--ui-btn); }
.boardrow .rank { grid-area: rank; color: var(--ui-dim); font-family: var(--ui-mono); }
.boardrow .who { grid-area: who; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boardrow .bmoves {
  grid-area: moves;
  font-family: var(--ui-mono);
  font-weight: 700;
  color: var(--ui-accent);
}

/* ── importing a repertoire ───────────────────────────────────────────────
   A dialog that is mostly a textarea and a set of instructions, because for
   Chessable — where most of these lines live — getting the PGN out is the
   hard part, not pasting it in. */

.import-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
.import-dialog {
  width: min(42.5rem, 94vw);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-panel);
  color: var(--ui-text);
  padding: 0;
}
.import-dialog .dialogform { padding: 1rem 1.125rem; }
.import-dialog textarea {
  width: 100%;
  font: 0.75rem/1.5 var(--ui-mono);
  resize: vertical;
}
.import-dialog .howto { margin: 0.75rem 0; }
.import-dialog .howto summary { cursor: pointer; font-size: 0.75rem; color: var(--ui-accent); }
.import-dialog .howto ol { margin: 0.375rem 0 0.375rem 1.125rem; padding: 0; }
.import-dialog .howto li { margin: 0.1875rem 0; }
.import-dialog .howto p { margin: 0.5rem 0; }

.importpreview { display: grid; gap: 0.25rem; max-height: 13.75rem; overflow-y: auto; margin: 0.5rem 0; }
.importbad {
  display: grid;
  gap: 2px;
  padding: 0.375rem 0.5rem;
  border-left: 3px solid var(--learn-wrong);
  background: var(--ui-btn);
  border-radius: var(--ui-radius);
  font-size: max(0.6875rem, var(--text-min)); /* 11px */
}
.importbad .ib-line { font-family: var(--ui-mono); color: var(--ui-text); word-break: break-all; }
.importbad .ib-why { color: var(--learn-wrong); }

/* ── the learning screen ────────────────────────────────────────────────── */

/* 2026-09-13: this `vh` is deliberately NOT `dvh`, unlike the two caps further
   down. It is a FLOOR, not a ceiling — nothing is hidden by it being generous,
   and a min-height that shrinks as the URL bar slides changes the page's total
   height mid-scroll, which is how you get a scroll position that jumps under
   your thumb. The worst `vh` costs here is a little dead space below the fold
   on a phone; `dvh` would cost jitter on every scroll. */
.learn { display: flex; flex-direction: column; min-height: 100vh; }
.learnwrap {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  padding: 0.875rem 1.25rem;
  /* `width: 100%` is load-bearing. .learn is a COLUMN flex container, and a
     column flex item with `margin: 0 auto` shrinks to fit its content instead
     of filling the row — which left this 841px wide inside a 2732px window and
     squeezed the board down to 797px for no visible reason. The margin still
     centres it; the width is what makes there be something to centre. */
  width: 100%;
  margin: 0 auto;
  max-width: 87.5rem;
  align-items: flex-start;
  justify-content: center;
}
/*
   The learning board is bounded by HEIGHT as well as width.

   Everything under the board — the prompt, the verdict line, the buttons and
   the progress bar — plus the header above it comes to roughly 240px. A board
   sized only by width therefore pushed the buttons off the bottom of the
   window, which is exactly the "too big, doesn't fit" complaint: you cannot
   press Continue if Continue is below the fold.

   `min()` of three things, so whichever runs out first wins:
     --board-size   what you dragged the grip to, or the theme default
     100%           never wider than the column
     100vh - 240px  never taller than what is left of the window
*/
.learnwrap .boardcol {
  flex: 0 0 auto;
  /* No height term here any more. The resizer applies it (reserveHeight:
     --learn-chrome) so --board-size is already the size the board will be —
     capping it a second time is what gave the grip a dead zone. */
  width: min(var(--board-size), 100%);
}

/* ── the column beside the board ──────────────────────────────────
   Everything that is not the board. It is a column of small blocks in the
   order you need them: what this is, what to do, how the sitting is going.

   It must never be the thing that decides the page's height — the board is —
   so it is allowed to scroll inside itself on a short window rather than
   pushing the board down. */

.learnhead { display: flex; flex-direction: column; gap: 0.375rem; }
.learnhead .row { display: flex; align-items: center; gap: 0.5rem; }

.learnname {
  font: 700 1.0625rem/1.25 var(--ui-font); /* 17px */
  margin: 0;
  /* A variation name arrives from Chessable at full length. Two lines, then
     stop — a title is not the thing being studied. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.learntools { display: flex; gap: 0.375rem; flex-wrap: wrap; }

/* The settings that only exist while studying. It replaces the move list
   rather than sitting above it, so opening it cannot push the board's
   neighbours around. */
.learnsettings {
  background: var(--ui-panel);
  border: 1px solid var(--ui-accent);
  border-radius: var(--ui-radius);
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.learnsettings[hidden] { display: none; }
.learnsettings h2 {
  font: 600 max(0.6875rem, var(--text-min))/1 var(--ui-font); /* 11px */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-dim);
  margin: 0 0 0.375rem;
}
.learnsettings input[type="number"] { width: 5.625rem; }
.learnsettings .row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.375rem; }
/*
   The side column NEVER grows.

   `flex: 1 1 300px` let it eat every pixel the board's height cap left over —
   on a short, wide window the board sat at 320px while the summary sprawled
   across a thousand. The board is the thing being looked at; everything beside
   it is a caption.
*/
.learnside {
  flex: 0 1 20.625rem;
  min-width: 0;
  font-size: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  /* Bounded by the window, like the board, and scrolls inside itself. Without
     this a long move list on a short window makes the PAGE scroll, and a board
     you have to scroll to see all of is the bug this screen keeps fighting.

     2026-09-13: `dvh` as well as `vh`, written twice so a browser older than
     Safari 15.4 / Chrome 108 drops the second line and keeps today's
     behaviour. On a phone `vh` is the viewport with the URL bar HIDDEN, so
     this cap was measured against a window taller than the real one and the
     bottom of this column ended up under the browser chrome. `dvh` is also
     the better match for the resizer, which computes the board's ceiling in
     script from `window.innerHeight` — that is the dynamic height, not the
     large one, so the two halves of this screen were measuring against
     different windows.

     Only the UNIT changed. `--learn-chrome` and the board's own `min()` are
     untouched; see the block above and theme.css. */
  max-height: calc(100vh - var(--learn-chrome));
  max-height: calc(100dvh - var(--learn-chrome));
  overflow-y: auto;
}

/* ── stored calculations ────────────────────────────────────────────────────
   Deliberately plainer than a variation card: these are not variations yet, and
   should not look like they are. */

.stagedbar { align-items: center; gap: 0.5rem; margin: 0.625rem 0; }
.stagedbar .minilabel { margin: 0; }

.stagedlist { display: grid; gap: 0.375rem; }

.stagedrow {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.625rem;
  align-items: baseline;
  padding: 0.5rem 0.625rem;
  border: 1px dashed var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-panel);
}
.stagedrow .sline { font: max(0.6875rem, var(--text-min))/1.5 var(--ui-mono); /* 11px */ word-break: break-word; }
.stagedrow .swhy { color: var(--ui-dim); font-size: max(0.6875rem, var(--text-min)); /* 11px */ }
.stagedrow.partial { border-color: var(--lvl-0); }

/* The one cue the course cards were missing: a course you have lines in that
   you have never studied looks exactly like one you know and are up to date on,
   and those are opposite situations. */
.course .cnew {
  color: var(--ui-accent-text);
  background: var(--ui-accent);
  border-radius: 0.5625rem;
  padding: 0.1875rem 0.4375rem;
  font-weight: 700;
}
.course.staged { border-style: dashed; }

/* ── paused ─────────────────────────────────────────────────────────────────
   The point of pausing mid-review is to stop looking at the position, so the
   pieces go — and the last-move and selection highlights with them, or the
   move you are being asked for is still on the board in outline. The squares
   stay, so the board does not collapse and the layout does not jump. */

#learnBoard.paused .cb-piece,
#learnBoard.paused .cb-overlay { visibility: hidden; }
#learnBoard.paused .cb-square::before { background: transparent !important; }

.boardwrap { position: relative; }
.pausedveil {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  font: 600 0.8125rem/1 var(--ui-font); /* 13px */
  color: var(--ui-dim);
  pointer-events: none;
}
#learnBoard.paused ~ .pausedveil { display: grid; }

/* ── the promotion picker ──────────────────────────────────────────────────
   Over the board, because that is where the pawn is and where you are looking.
   Only ever shown when the answer is an underpromotion — see the Promotion
   section in repertoire.js.

   `[hidden]` needs the `!important`: `display: grid` below would otherwise win
   on specificity and the picker would sit over every board permanently. Same
   trap as .tabpanel[hidden] on the Build screen. */
.promopick {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* Not opaque. The position is the thing being asked about, and a picker that
     hides the board hides the pawn it is about. */
  background: color-mix(in srgb, var(--ui-bg) 72%, transparent);
  z-index: 3;
}
.promopick[hidden] { display: none !important; }

.promobox {
  display: grid;
  gap: 0.625rem;
  justify-items: center;
  padding: 0.875rem 1.125rem;
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
}
.promotitle {
  font: 600 0.75rem/1 var(--ui-font);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ui-dim);
}
.promorow { display: flex; gap: 0.5rem; }

.promobtn {
  width: 3.875rem;
  height: 3.875rem;
  display: grid;
  place-items: center;
  padding: 0.25rem;
  background: var(--ui-btn);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  cursor: pointer;
}
/* Split on 2026-09-13: the two halves used to be one rule, and putting the
   whole thing behind @media (hover: hover) would have taken the keyboard's
   focus ring away on every touch device with a keyboard attached. Only the
   pointer half is conditional. */
.promobtn:focus-visible {
  background: var(--ui-btn-hover);
  border-color: var(--ui-accent);
  outline: none;
}
@media (hover: hover) {
  .promobtn:hover {
    background: var(--ui-btn-hover);
    border-color: var(--ui-accent);
    outline: none;
  }
}
/* All three representations a piece set can use: an <svg> for a built-in set,
   an <img> for a folder set that named its own colours, and a <span> masked by
   the theme for one that did not. See renderPiece in board/pieces.js — missing
   the third would leave a folder set's silhouettes as invisible zero-sized
   spans, which looks exactly like a picker with no pieces in it. */
.promobtn svg,
.promobtn img,
.promobtn .piece-mask { width: 100%; height: 100%; display: block; }

/* `.autonext` and `.autonext.off` were here. The first dressed a wrapper round
   the auto-next number back when it sat on the studying screen's own row; the
   second dimmed it during an Intense sitting, which used to ignore it. The
   number moved into the ⚙ settings panel and wears `.field-label` with every
   other field there, and Intense honours it — so neither rule had anything
   left to style. See stopAfterVariation for the rule itself. */

/* ── the resize grip ────────────────────────────────────────────────────── */

.learnwrap .boardwrap { position: relative; }

/* The grip has to out-rank the board it overhangs. .cb-squares carries
   z-index: 1 and .boardwrap is positioned but sets no z-index of its own, so
   the two share a stacking context and the squares painted straight over the
   grip: elementsFromPoint at the grip's own centre returned .cb-square first,
   every pointerdown landed on the board, and no board in the app could
   actually be dragged to a new size. Above .cb-piece.cb-dragging (20) so a
   piece cannot cover it mid-drag either. */
.learnwrap .grip {
  position: absolute;
  z-index: 30;
  right: -0.3125rem;
  bottom: -0.3125rem;
  width: 1.25rem;
  height: 1.25rem;
  cursor: nwse-resize;
  border-right: 3px solid var(--ui-dim);
  border-bottom: 3px solid var(--ui-dim);
  border-bottom-right-radius: 0.3125rem;
  opacity: 0.5;
  transition: opacity var(--anim-fade) linear;
  touch-action: none;
}
.learnwrap .grip:focus-visible { opacity: 1; border-color: var(--ui-accent); }
@media (hover: hover) { .learnwrap .grip:hover { opacity: 1; border-color: var(--ui-accent); } }
/* While dragging, stop the pointer selecting text across the page. */
body.cb-resizing { user-select: none; }

/* The strip under the board. Kept deliberately tight: the board is capped at
   `100vh - --learn-chrome`, so every pixel spent here is a pixel taken off the
   thing being looked at. Prompt, countdown and verdict share ONE row for that
   reason; the row keeps a min-height so the board does not jump each time the
   verdict appears and disappears. */
.promptrow {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  min-height: 1.5em;
  /* The anchor for .flash, which is absolute so that it cannot change this
     row's height. See the note on .flash below. */
  position: relative;
  /* Room on the right for it to sit in without landing on the verdict. */
  padding-right: 0.25rem;
}
.prompt { font-size: 0.8125rem; /* 13px */ }
.verdict { font-weight: 600; font-size: 0.8125rem; /* 13px */ }
.verdict.right { color: var(--learn-right); }
.verdict.wrong { color: var(--learn-wrong); }
.verdict.shown { color: var(--learn-shown); }

/* The sitting's own notice — see #learnFlash in repertoire.html. Quiet and
   outlined rather than filled: it is news about the variation you have just
   left, and it must never compete with the verdict about the move in front of
   you.

   ABSOLUTE, and that is the whole point of this rule rather than a detail.
   In flow it was 23px tall against a 21px row, which pushed the document one
   pixel past the viewport, which summoned a vertical scrollbar, which took 15px
   off the page width, which moved the centred board 7px to the left — and then
   took it all back four seconds later. That is what "the board is shaky" was:
   not the board, and not an animation. Out of flow it cannot cost a single
   pixel however long the text is, so it cannot do it again.

   Anything else that appears and disappears on this screen has to follow the
   same rule. */
.flash {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  max-width: 55%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font: 600 max(0.6875rem, var(--text-min))/1.4 var(--ui-font); /* 11px */
  padding: 0.1875rem 0.5rem;
  border-radius: 0.625rem;
  border: 1px dashed var(--ui-line);
  /* Opaque, because out of flow it can pass over the verdict on a narrow
     window, and a legible notice on top beats two unreadable ones. */
  background: var(--ui-bg);
  color: var(--ui-dim);
}
.flash[hidden] { display: none; }

.learn .controls { display: flex; gap: 0.375rem; flex-wrap: wrap; }

.progress { height: 0.1875rem; background: var(--ui-line); border-radius: 2px; overflow: hidden; }
.progress .bar { height: 100%; width: 0; background: var(--ui-accent); transition: width var(--anim-fade) linear; }

.movelist {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.5rem;
  align-content: flex-start;
  font: 0.8125rem/1.6 var(--ui-mono); /* 13px */
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 0.75rem;
  min-height: 4.5rem;
}
.movelist .mv { color: var(--ui-text); }
.movelist .mv.dim { color: var(--ui-dim); }
.movelist .mv.right { color: var(--learn-right); }
.movelist .mv.shown { color: var(--learn-shown); }
.movelist .mv.pending { color: var(--ui-dim); opacity: 0.45; }
.movelist .no { color: var(--ui-dim); }

/* ── "start from here": the referential prefix, and the seam ───────────────
   Every colour below is an existing theme variable, and both are already
   defined in the light theme as well:

     --ui-accent   the app's own accent, used for "this is the one that is
                   selected" everywhere else on this page (.quickbtn.is-on,
                   the progress bar). Deliberately NOT --learn-right or
                   --learn-shown: those two mean "you got it" and "you were
                   shown it" on this very move list, and a third meaning on
                   the same green would be unreadable.
     --ui-dim      what this stylesheet already uses for text that is present
                   but not the point.

   And not colour alone. A referential move is dimmed AND italic; the seam is
   an actual ▶ glyph sitting between the two halves; the marked move in the
   picker also carries aria-pressed and a title. Somebody who cannot separate
   the two hues still sees a slanted run of moves, an arrow, and a sentence
   under the strip saying where reviews start. */
.movelist .mv.ref {
  color: var(--ui-dim);
  font-style: italic;
  opacity: 0.8;
}
.movelist .startcut,
.card .moves .startcut {
  color: var(--ui-accent);
  font-weight: 700;
}

/* The picker, inside the variation window. A move here is a button, so the
   reset is the same one .card .title .nm carries — it has to keep reading as
   a move in a move list, not as a form control. */
.movelist.startpick {
  min-height: 0;
  padding: 0.5rem 0.625rem;
  gap: 0.1875rem 0.375rem;
}
.movelist.startpick .mv.pick {
  font: inherit;
  color: var(--ui-text);
  background: none;
  border: 1px solid transparent;
  border-radius: 0.25rem;
  padding: 1px 0.25rem;
  cursor: pointer;
}
@media (hover: hover) {
  .movelist.startpick .mv.pick:hover:not(:disabled) {
    border-color: var(--ui-accent);
  }
}
.movelist.startpick .mv.pick:disabled {
  cursor: default;
  opacity: 0.5;
}
/* The colour change José asked for, so a click says it worked. */
.movelist.startpick .mv.pick.is-start {
  background: var(--ui-accent);
  color: var(--ui-accent-text);
  border-color: var(--ui-accent);
  font-weight: 700;
}
/* The `:hover` twin exists only to out-specify `.mv.pick:hover` above so the
   chosen move keeps its colour under the cursor. It is guarded the same way
   that rule is, on 2026-09-13 — guard one and not the other and the chosen
   move would go back to a grey border the moment you pointed at it. */
@media (hover: hover) {
  .movelist.startpick .mv.pick.is-start:hover:not(:disabled) {
    background: var(--ui-accent);
    color: var(--ui-accent-text);
    border-color: var(--ui-accent);
    font-weight: 700;
  }
}
/* Repeated rather than inherited from `.movelist .mv.ref`: `.movelist.startpick
   .mv.pick` is one class longer and would otherwise repaint these back to full
   strength. */
.movelist.startpick .mv.pick.ref {
  color: var(--ui-dim);
  font-style: italic;
  opacity: 0.8;
}

/* ── the end-of-sitting card ────────────────────────────────────────────────
   What a sitting came to, in the order somebody reads it: what it did to your
   levels, why it stopped, what it paid, and then the numbers.

   Redesigned 2026-09-08, after the previous commit took three rows off it and
   shortened the headline to four characters. José: *"bring titles and digits
   closer together, and fill the huge amount empty space that should be left by
   making information bigger and more colorful."*

   Both halves of that are one change. Every row used to be
   `justify-content: space-between` at 11px — the name pinned to the left edge
   of a 330px card and its number to the right, a ruler's width apart, so the
   eye had to travel to pair them up. Nothing here is space-between any more: a
   number and its name are ONE object, stacked, the number big on top and the
   name captioning it underneath. Closing that gap is also what frees the room
   to make the numbers twice the size they were.

   Colour is from theme.css and nothing here invents one — a promotion is
   --learn-right because that is already this app's "you got it right", a
   demotion is --learn-wrong for the same reason, and
   a difficult token is --lvl-0, the colour of the Difficult rung it belongs
   to. Colour is never the only signal: see .tally below. */

.summary {
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 1rem;
}

/* THE HEADLINE — the sitting's own tally, "+3 -1".
   Two boxes and not one string, because they are two facts pointing opposite
   ways and the card's whole job is to let you take them in at a glance.

   FOUR things separate up from down, and only one of them is colour:

     the sign     +3 / -0, kept from the string this replaced
     the arrow    ▲ / ▼, drawn by CSS since it is decoration for the word
     the word     PROMOTED / DEMOTED, which is the fact said in letters
     the order    up is always first, on the left

   So a reader who cannot tell --learn-right from --learn-wrong loses nothing.
   That is not a hypothetical: red-green is the most common colour deficiency
   there is, and this card is mostly a red number beside a green one. */
.summaryscore { margin: 0 0 0.75rem; }
/* When the headline is a SENTENCE rather than a tally — "Intense Mode
   complete", or the error card's own line — it is a heading and is styled
   like one. Both shapes have to look deliberate; only one of them is a score. */
.summaryscore.said { font: 700 0.9375rem/1.3 var(--ui-font); /* 15px */ }
.summaryscore.tally { display: flex; gap: 0.5rem; font-size: 0.875rem; /* 14px */ }

.tallybox {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1875rem;
  padding: 0.5625rem 0.625rem;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-btn);
}
.tallybox .n {
  font: 700 1.625rem/1 var(--ui-mono); /* 26px */
  letter-spacing: -0.01em;
  color: var(--ui-dim);
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
/* The arrow. A pseudo-element on purpose: it says nothing the word underneath
   does not already say, so it belongs to the drawing and not to the text a
   screen reader reads out. */
.tallybox .n::before { font-size: 0.8125rem; /* 13px */ line-height: 1; }
.tallybox.up .n::before { content: "\25B2"; }     /* ▲ */
.tallybox.down .n::before { content: "\25BC"; }   /* ▼ */
.tallybox .w {
  font: 700 max(0.59375rem, var(--text-min))/1.2 var(--ui-font); /* 9.5px */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ui-dim);
}
.tallybox.up:not(.is-zero) {
  border-color: color-mix(in srgb, var(--learn-right) 45%, transparent);
  background: color-mix(in srgb, var(--learn-right) 12%, transparent);
}
.tallybox.up:not(.is-zero) .n { color: var(--learn-right); }
.tallybox.down:not(.is-zero) {
  border-color: color-mix(in srgb, var(--learn-wrong) 45%, transparent);
  background: color-mix(in srgb, var(--learn-wrong) 12%, transparent);
}
.tallybox.down:not(.is-zero) .n { color: var(--learn-wrong); }
/* Nothing moved that way. Said quietly and in outline, so "+3 -0" reads as one
   loud half and one empty one rather than two claims of equal weight — and so
   the empty half is recognisable as empty without reading the digit. */
.tallybox.is-zero { background: transparent; border-style: dashed; }

/* "WHAT IT PAID" — the big "+N XP" line with a rule under it — stood here
   until XP was removed on 2026-09-14. */

/* THE NUMBERS. A grid rather than a list, because the card carries between one
   and a dozen of these depending on the sitting, and tiles that reflow are the
   only shape that is right for both. `auto-fit` puts two side by side on the
   ordinary two-number card and three on a multi-variation one. */
.summaryrows {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.25rem, 1fr));
  gap: 0.375rem;
}
.summaryrows:empty { display: none; }

/* A number with a name. `column-reverse` and not a reordered DOM: the value is
   drawn above its label, and read out after it, so the picture says "96%,
   accuracy" and a screen reader says "accuracy, 96%". */
.summaryrows .stat {
  display: flex;
  flex-direction: column-reverse;
  /* The end of a reversed column is its TOP, so this pins the numbers to a
     common line and lets the slack fall underneath. It matters as soon as one
     label wraps — "Session accuracy" does at this width, and in Spanish so does
     "Variaciones" — and without it that one tile's number floats up out of the
     row while the labels line up instead. The numbers are the row. */
  justify-content: flex-end;
  gap: 0.1875rem;
  min-width: 0;
  padding: 0.5rem 0.5625rem;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  background: var(--ui-btn);
}
.summaryrows .stat .v {
  font: 700 1.25rem/1 var(--ui-mono);
  color: var(--ui-text);
}
.summaryrows .stat .k,
.summaryrows .ledger .k,
.summaryrows .note .k {
  font: 700 max(0.59375rem, var(--text-min))/1.2 var(--ui-font); /* 9.5px */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ui-dim);
}
/* Accuracy, coloured by how it went — the same three colours the board itself
   uses while you are answering, so the card is not teaching a second language.
   The number is right there, so the colour is a second reading of a fact that
   is already legible, never the fact itself. */
.summaryrows .stat[data-tone="good"] .v { color: var(--learn-right); }
.summaryrows .stat[data-tone="fair"] .v { color: var(--learn-shown); }
.summaryrows .stat[data-tone="poor"] .v { color: var(--learn-wrong); }
.summaryrows .stat[data-tone="accent"] .v { color: var(--ui-accent); }

/* The rounds ledger — Intense Mode only. Its value is a phrase ("12 of 20
   clean"), not a number, so it takes the full width and sits on one line with
   its label. Still not space-between: the value follows the label immediately
   and the empty room is left at the end of the line, where it costs nothing. */
.summaryrows .ledger {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.375rem 0.5625rem;
  border-radius: var(--ui-radius);
  background: var(--ui-btn);
}
.summaryrows .ledger .k { flex: 0 0 auto; }
.summaryrows .ledger .v {
  min-width: 0;
  font: 600 0.78125rem/1.3 var(--ui-mono); /* 12.5px */
  color: var(--ui-text);
}
/* The round you are in the middle of, as opposed to the ones behind you. */
.summaryrows .ledger[data-tone="live"] {
  background: color-mix(in srgb, var(--ui-accent) 14%, transparent);
}
.summaryrows .ledger[data-tone="live"] .v { color: var(--ui-accent); }

/* A sentence: the difficult-token count and the "why no promotion" note. Full
   width, and quieter than the numbers — these explain, they do not score. */
.summaryrows .note {
  grid-column: 1 / -1;
  display: grid;
  gap: 2px;
  padding: 2px 1px 0;
}
.summaryrows .note .v { font-size: max(0.71875rem, var(--text-min)); /* 11.5px */ line-height: 1.45; color: var(--ui-text); }
.summaryrows .note[data-tone="warn"] .k { color: var(--lvl-0); }

.summary .row { margin-top: 0.875rem; }

/* ── dialogs ────────────────────────────────────────────────────────────── */

dialog {
  color: var(--ui-text);
  background: var(--ui-panel);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 1.125rem 1.25rem;
  width: min(32.5rem, 92vw);
}
dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
.dialogform h2 { margin: 0 0 0.375rem; font-size: 1rem; }
.sides { display: flex; gap: 0.875rem; flex-wrap: wrap; }

/* ── the Intense Mode quick sizes ──────────────────────────────────────────
   ×5 through ×25, and All. They write into the number field above them rather
   than replacing it: a repertoire with 37 difficult variations has answers
   these six do not cover, and the field is still the one that decides. */
.quickpick {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.5rem;
}
.quickbtn {
  font: 600 0.75rem/1 var(--ui-mono);
  color: var(--ui-text);
  background: var(--ui-btn);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  padding: 0.3125rem 0.625rem;
  cursor: pointer;
}
@media (hover: hover) { .quickbtn:hover:not(:disabled) { background: var(--ui-btn-hover); border-color: var(--ui-accent); } }
/* The one that matches the field. It is the same accent the primary buttons
   use, so "this is what is selected" looks the same everywhere on this page. */
.quickbtn.is-on {
  background: var(--ui-accent);
  color: var(--ui-accent-text);
  border-color: var(--ui-accent);
}
.quickbtn:disabled { opacity: 0.4; cursor: default; }
.advanced summary { cursor: pointer; color: var(--ui-dim); font-size: 0.75rem; margin-top: 0.75rem; }

/* ── the board-based Add/Edit variation dialog (item 12) ───────────────────
   Wider than the other dialogs, and a two-column layout once there is room
   for one: the board on the left (what you actually play on), the form on
   the right. Narrower than that, the board stacks above the form instead —
   a board that has been squeezed into a sliver of a dialog is worse than a
   taller dialog. */
dialog.variation-dialog { width: min(42.5rem, 95vw); }

.vd-layout {
  display: flex;
  flex-wrap: wrap;
  gap: 1.125rem;
  align-items: flex-start;
}

.vd-board {
  flex: 0 0 auto;
  width: min(17.5rem, 100%);
}
.vd-board #vBoard { width: 100%; aspect-ratio: 1; }

.vd-fields {
  flex: 1 1 16.25rem;
  min-width: min(15rem, 100%);
}
/* First child inside the fields column already gets .field-label's own
   margin; this just stops it starting lower than the board beside it. */
.vd-fields > .field-label:first-child { margin-top: 0; }

.vd-controls {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}

.vd-board .ply {
  color: var(--ui-dim);
  font-family: var(--ui-mono);
  font-size: max(0.6875rem, var(--text-min)); /* 11px */
}

/* The Moves box mirrors the board and is not meant to be typed into directly
   — dimmer than an editable field says so before anyone tries. The "paste a
   PGN instead" box under Advanced is the real text box, for anyone bringing
   in a line from somewhere else. */
#vPgn { opacity: 0.75; cursor: default; }
.field-label .live {
  font-size: max(0.625rem, var(--text-min)); /* 10px */
  letter-spacing: 0;
  text-transform: none;
  color: var(--ui-accent);
  opacity: 0.8;
}

/* The backstop from shell.css, for the same reason: any `display` declaration
   in a stylesheet beats the browser's built-in [hidden] { display: none }, and
   a panel that is hidden but painted has cost this project a whole session. */
[hidden] { display: none !important; }

/* The "show more" control sits in the list's own grid track, so it stretches
   across rather than shrink-wrapping in the corner. */
.showmore { width: 100%; margin-top: 0.25rem; color: var(--ui-dim); }
@media (hover: hover) { .showmore:hover { color: var(--ui-text); } }

/* The "player prep" table was here — .pptable and its .pp* children, plus a
   #ppBoard sizing rule. It belonged to the lookup that asked what a named
   opponent plays and whether the repertoire answered it, which was deleted
   along with the Prep panel; nothing has produced one of these elements since.
   The Games tab answers the same question now, with the games attached. */


/* The empty-list message when it is NOT reporting an empty list. Reserved for
   the one case that means something is wrong rather than something is absent:
   a previous read of this account found variations and this one did not. */
#empty.alarm,
.hint.alarm {
  color: var(--learn-wrong);
  font-weight: 600;
  border: 1px solid var(--learn-wrong);
  border-radius: var(--ui-radius);
  padding: 0.75rem 0.875rem;
  background: color-mix(in srgb, var(--learn-wrong) 8%, transparent);
}

/* The side label, once it became a control. It has to keep reading as a chip —
   it is one — while saying that it can be clicked. */
.chip.sidebtn {
  border: 1px solid transparent;
  cursor: pointer;
  font: inherit;
  font-size: max(0.6875rem, var(--text-min)); /* 11px */
  line-height: 1;
}
@media (hover: hover) { .chip.sidebtn:hover:not(:disabled) { border-color: currentColor; filter: brightness(1.12); } }
.chip.sidebtn:disabled { opacity: .5; cursor: default; }

/* The review screen's title. Underlined only on hover, because the top of a
   review screen is not the place to shout.
   A <button> since 2026-09-01, where it was an <a target="_blank"> — same look,
   so the reset is the same one .card .title .nm carries: the heading must go on
   reading as a heading. */
.learnlink {
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  text-decoration: none;
  cursor: pointer;
}
@media (hover: hover) { .learnlink:hover { text-decoration: underline; } }
/* On a touchscreen the line's name is a thumb-sized target, 2026-09-14. It is
   the way into the variation's own page and was 21px tall. */
@media (pointer: coarse) { .learnlink { padding-block: 0.625rem; } }

/* Delete, in the variation window. Set apart from Save and Cancel by the
   `.grow` before it and coloured like what it does — it is the only control in
   this dialog that cannot be taken back.

   `.btn.vd-delete`, not `.vd-delete`: `.btn.ghost:hover` is three classes and
   would otherwise repaint the hover state grey. */
.btn.vd-delete { color: var(--learn-wrong); }
@media (hover: hover) {
  .btn.vd-delete:hover {
    color: var(--learn-wrong);
    border-color: var(--learn-wrong);
    background: color-mix(in srgb, var(--learn-wrong) 14%, transparent);
  }
}

/* ── calculated lines: the review screen ───────────────────────────────────
   A list on the left, the line on a board on the right. Filing a variation you
   have not looked at is the one thing this screen exists to prevent, so the
   board is part of the screen and not somewhere else to navigate to. */

.stagedsplit {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.125rem;
  align-items: start;
}

/* Side by side only when there is genuinely room for a board next to a list.
   Below this the board goes underneath, which is still better than a board
   squeezed to 180px. */
@media (min-width: 900px) {
  .stagedsplit { grid-template-columns: minmax(0, 1fr) auto; }
}

.stagedboard {
  width: min(22.5rem, 100%);
  position: sticky;
  top: 0.75rem;
}

.stagedboard .stitle {
  font-size: 0.8125rem; /* 13px */
  font-weight: 600;
  color: var(--ui-text);
  margin-bottom: 0.5rem;
  overflow-wrap: anywhere;
}

.stagedboard .controls {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin-top: 0.5rem;
}

/* The row is now clickable, so it has to look it. */
.stagedrow .sbody { cursor: pointer; }

.stagedrow .stitle {
  font-weight: 600;
  color: var(--ui-text);
}

.stagedrow .sline {
  font-family: var(--ui-mono);
  font-size: max(0.6875rem, var(--text-min)); /* 11px */
  color: var(--ui-dim);
  overflow-wrap: anywhere;
}

/* ── the variation counter ────────────────────────────────────────────────
   How many variations to do today. One big number, because that is the whole
   question; the week is the bar and the seven cells under it. */

/* ── the day's fraction, and the sentence beside it ────────────────────────
   The panel is 230px of content in a 260px sidebar, and "350 / 285" beside
   "done — today is finished" wants 256 of it. So the row WRAPS: the label
   drops under the numbers when it cannot sit next to them, and stays next to
   them when it can ("8 / 10 · 2 to go" needs 150).

   What it must never do is break the fraction, which is what it was doing —
   see .countpair. */
.counter .counterhead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 0.5rem;
  margin: 0.375rem 0 0.5rem;
}

/* The numbers are one unit. `nowrap` is the load-bearing line: the goal half
   is the string "/ 285", the space in it is a break opportunity, and a flex
   item is free to shrink to its longest word — so the row fitted by folding
   285 onto a second line and the fraction read as two numbers stacked. */
.countpair {
  display: flex;
  align-items: baseline;
  gap: 0.3125rem;
  white-space: nowrap;
}
.counter .countnum {
  font-size: 2.125rem; /* 34px */
  font-weight: 800;
  line-height: 1;
  color: var(--ui-accent);
  font-variant-numeric: tabular-nums;
}
/* The target half of "12 / 40". Same size as the number beside it — both are
   the answer — but dimmer, because the one that changes while you work is the
   one worth reading first. */
.counter .countgoal { color: var(--ui-dim); }
.counter .countlabel { font-size: 0.75rem; color: var(--ui-dim); }

/* A long fraction, in smaller type, so it still fits on one line.
   The weekly goal goes up to 10,000, so "10,000 / 1,429" is reachable and is
   237px at 34px — seven more than there are. `is-wide` is set by paintCounter
   from the rendered length, which is the only thing that decides this: tabular
   figures mean every digit is the same width, so counting characters IS
   measuring it. */
.counter.is-wide .countnum { font-size: 1.625rem; /* 26px */ }

/* A day off is not a failure and must not be painted like one; a finished day
   is a success and should be obvious from across the room. */
.counter.is-off .countnum,
.counter.is-dayoff .countnum { color: var(--ui-dim); }
.counter.is-done .countnum { color: var(--status-up, var(--ui-accent)); }

.counter .cbar {
  height: 0.3125rem;
  border-radius: 0.1875rem;
  background: var(--ui-line);
  overflow: hidden;
}
.counter .cfill {
  height: 100%;
  width: 0;
  background: var(--ui-accent);
  transition: width 200ms ease;
}
.counter.is-done .cfill { background: var(--status-up, var(--ui-accent)); }

/* The week, at a glance: which days you study, and what you actually did. */
.weekstrip {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.1875rem;
  margin: 0.5rem 0 0.25rem;
}
.wday {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 0.1875rem 0;
  border-radius: var(--ui-radius);
  border: 1px solid transparent;
  font-size: max(0.625rem, var(--text-min)); /* 10px */
  line-height: 1.2;
  color: var(--ui-dim);
}
/* A study day is marked, so "why is today asking me for 70" is answerable by
   counting the marked cells left in the week. */
.wday.is-study { background: var(--ui-btn); color: var(--ui-text); }
.wday.is-today { border-color: var(--ui-accent); }
.wday .wn { font-variant-numeric: tabular-nums; font-weight: 700; }
.wday.has-done .wn { color: var(--ui-accent); }

/* ── Monday's suggestion about last week ───────────────────────────────────
   A suggestion, not an alert. No colour, no border, no badge: the app is being
   pleased with you, and a boxed notice in a 230px sidebar reads as a warning.

   A rule and some air are the whole separation. It has to be separated from
   something, because everything above it in this panel is THIS week and this
   row is last week — but the separation is a hairline, not a card. Built by
   paintGoalSuggestion, and absent from the DOM on every week that did not earn
   it, so there is no empty box here for the other fifty-one. */
.goalsuggest {
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--ui-line);
}

/* The two sentences are one paragraph, not two hints that happen to be
   adjacent: "Last week you did 220 against a goal of 200" is the reason the
   line under it exists. `.hint`'s own margins would set them the same distance
   apart as two unrelated notes. */
.goalsuggest .hint { margin: 0; }
.goalsuggest .hint + .hint { margin-top: 2px; }

/* And the buttons belong to that paragraph. `.row` is spaced for controls
   standing on their own in a form, which is 10px too far from a sentence they
   are the answer to. */
.goalsuggest .row { margin-top: 0.375rem; }

/* Monday to Sunday, as buttons. Same shape as the strip above it on purpose:
   what you are choosing and what you are looking at are the same seven days. */
.daypick { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.1875rem; margin: 0.25rem 0 0.5rem; }
.daybtn {
  padding: 0.3125rem 0;
  font: inherit;
  font-size: max(0.6875rem, var(--text-min)); /* 11px */
  background: var(--ui-input-bg);
  color: var(--ui-dim);
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius);
  cursor: pointer;
}
@media (hover: hover) { .daybtn:hover { border-color: var(--ui-accent); } }
.daybtn.is-on {
  background: var(--ui-accent);
  color: var(--ui-accent-text);
  border-color: transparent;
  font-weight: 700;
}

/* Why the sitting stopped. Quiet — it is context, not a verdict — but present,
   which the card was not until 2026-09-03. See paintSummary. */
.summarywhy {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--ui-dim);
  margin: -0.25rem 0 0.75rem;
  /* It is a reason, not a number, so it is marked as an aside rather than
     given a size of its own — the accent edge is what stops it reading as a
     caption belonging to the tally above it. */
  padding-left: 0.5625rem;
  border-left: 2px solid var(--ui-line);
}
.summarywhy:empty { display: none; }


/* ═══════════════════════════════════════════════════════════════════════════
   CALLING YOUR MOVES

   Three things: the button, the line that shows what was heard, and the
   picker. Everything here is drawn from theme.css variables and nothing here
   is a new colour, for the same reason the rest of this file is not — a
   feature that has to look right in both themes cannot own a hex value.
   ═════════════════════════════════════════════════════════════════════════ */

/* The button, while it is listening. It PULSES rather than simply turning a
   colour, because "the microphone is on" is a thing somebody has to be able
   to tell at a glance from across a desk, and a static red button reads as a
   button you could press to make something red. */
.voicebtn.is-live {
  border-color: var(--due-now);
  color: var(--due-now);
  animation: voicepulse 1.6s ease-in-out infinite;
}
@keyframes voicepulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--due-now) 55%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--due-now) 0%, transparent); }
}
/* Somebody who has asked not to see motion gets the colour and no pulse. */
@media (prefers-reduced-motion: reduce) {
  .voicebtn.is-live { animation: none; }
}

/* What it heard. Kept to one line and clipped rather than wrapping: it sits
   directly under the prompt, and a sentence that grows to three lines pushes
   the buttons under it around while somebody is reaching for them. */
.voiceheard {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 1.25rem;
  font-size: 0.75rem;
  color: var(--ui-dim);
}
.voiceheard[hidden] { display: none; }
.voicetext {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--ui-mono);
}
/* Words still arriving, as opposed to a finished sentence. */
.voicetext.interim { opacity: 0.65; font-style: italic; }
.voicetext.miss { color: var(--learn-wrong); }

.voicedot {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--ui-line);
}
.voicedot.is-live { background: var(--due-now); }

/* The two buttons for a sentence that describes two legal moves. Rare, and
   deliberately not a dialog: it is a question about the move you are already
   looking at, and it must not stop you simply saying it again more precisely. */
.voiceask { display: flex; gap: 0.375rem; flex: 0 0 auto; }
.voiceask:empty { display: none; }
.voiceask button {
  font: 600 0.75rem/1 var(--ui-mono);
  padding: 0.25rem 0.5rem;
  background: var(--ui-btn);
  color: var(--ui-text);
  border: 1px solid var(--ui-accent);
  border-radius: var(--ui-radius);
  cursor: pointer;
}
@media (hover: hover) { .voiceask button:hover { background: var(--ui-btn-hover); } }

/* The picker. `.learnsettings` gives it its box; this is only what it adds. */
.voicepanel select { width: 100%; }
.voicepanel .hint { line-height: 1.5; }

/* The level bar. Not a decoration — see the comment on the panel in
   repertoire.html. It is the only thing on screen that can prove the chosen
   microphone is the one hearing you. */
.meter {
  height: 0.5rem;
  margin-top: 0.375rem;
  border-radius: 62.4375rem;
  background: var(--ui-btn);
  border: 1px solid var(--ui-line);
  overflow: hidden;
}
.meterbar {
  height: 100%;
  width: 0%;
  background: var(--learn-right);
  /* Fast up, slow down: a bar that eases in both directions lags behind the
     voice and reads as a broken microphone. */
  transition: width 90ms linear;
}
