body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  line-height: 1.6;
  color: #111827;
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
  /* Light theme, three surface levels:
       page   #eceef1  a barely-cool off-white, not pure white — pure white
                       leaves cards nothing to sit against, and is tiring
                       to read against on a bright screen
       panel  #fafbfc  the raised surface
       table  #fdfdfe  content, separated by hairlines rather than fills
     Text is #111827 rather than black and #6b7280 for secondary. Red is
     reserved for identity — the title rule, headings, primary action —
     so it reads as a brand colour instead of decoration. */
  background-color: #eceef1;
}
h1 {
  text-align: center;
  color: #111827;
  letter-spacing: -0.02em;
  border-bottom: 3px solid #e74c3c;
  padding-bottom: 12px;
  margin-bottom: 5px;
  font-size: 2.53em;
}
/* Desktop only: drop the browser's default h1 top margin so the title lines
   up with the two fixed control clusters flanking it.
     theme-bar   top 26 + (8 padding + 34 button + 8 padding) → centre ~52px
     flag stack  top 30 + ~47 tall                            → centre ~53px
   With margin-top:0 the title's line box runs from the body's 20px padding
   to ~85px, putting its centre at ~52px. Removing the ~27px default margin
   is also what pulls the whole page up.
   Mobile is excluded: there the header controls are absolutely positioned
   below the search panel, not beside the title, so there is nothing to
   align to and the extra breathing room is wanted. */
@media (min-width: 769px) {
  h1 {
    margin-top: 0;
  }
}
h2 {
  color: #1d3557;
  margin-top: 40px;
  border-bottom: 1px solid #e5e7eb;
  padding-bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* 15% below the browser default of 1.5em. The nested .toggle-btn is
     sized in em, so it scales down with the heading automatically. */
  font-size: 1.275em;
  gap: 12px;
}
/* Keeps the whole chapter title as one flex item even after a search
   highlight splits its text into several nodes. */
/* The title itself toggles its chapter, so it needs a pointer — but nothing
   else. No underline and no weight or size change on hover: an <h2> is
   already the largest text on the page, and shifting it would reflow the
   whole heading row on every mouse-over. The cursor is the only affordance. */
.chapter-title {
  min-width: 0;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.chapter-title:hover,
.chapter-title:focus {
  text-decoration: none;
  font-weight: inherit;
  font-size: inherit;
  font-style: inherit;
}
/* Speaker button appended to each chapter title, reads the chapter's hanzi */
.chapter-audio-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 3px;
  margin-left: 8px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  transition: background-color 0.2s, transform 0.1s;
  outline: none;
}
.chapter-audio-btn:hover {
  background-color: #fadbd8;
  transform: scale(1.15);
}
.chapter-audio-btn svg {
  width: 16px;
  height: 16px;
  fill: #1d3557;
}
/* Night and paper: the heading speaker matches the ones in the tables and
   grammar boxes, so every audio control on the page is one colour. Day
   keeps navy, where the heading and its icon are deliberately a set. */
body.dark-mode .chapter-audio-btn svg,
body.sepia-mode .chapter-audio-btn svg {
  fill: #e74c3c;
}
.meta-info {
  text-align: center;
  font-style: italic;
  color: #6b7280;
  margin-bottom: 18px;
  font-size: 1.16em;
}
/* Caption for the level filter above it, so it sits closer to that bar
   than the bars sit to each other. */
.meta-hint {
  font-size: 0.85em;
  margin-top: -6px;
  margin-bottom: 16px;
}
/* Runs on from the first sentence on desktop; broken onto its own line on
   mobile, where both sentences are held to one line each. */
.meta-line-2 {
  display: inline;
}
/* Desktop-only part of the title. Hidden on mobile where the full string
   would wrap, but kept in the markup so the h1 still carries the words
   "Chinese Mandarin" for search engines at every width. */
.title-prefix {
  display: inline;
}

/* Control Panel (Search & Bulk Toggle) */
.control-panel {
  /* Light theme surfaces. The page is 20% down from white and the panels
     15%, which leaves the panels the lighter of the two — a surface should
     always sit above the page it rests on, never below it. All three
     levels stay in one barely-cool neutral family so the reds in the
     headings remain the only colour on screen. */
  background: #fafbfc;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
  /* Matches .level-filter-bar so the three stacked panels — search,
     flashcard, level filter — sit an equal distance apart. */
  margin-bottom: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 10px;
  z-index: 100;
  border: 1px solid #e5e7eb;
}
.search-box {
  flex: 1;
  min-width: 280px;
  position: relative;
}
.search-box input {
  width: 100%;
  padding: 12px 20px;
  font-size: 1em;
  background: #f2f4f6;
  border: 1px solid #e5e7eb;
  border-radius: 30px;
  outline: none;
  transition: border-color 0.3s, box-shadow 0.3s;
  box-sizing: border-box;
}
.search-box input:focus {
  border-color: #e74c3c;
  box-shadow: 0 0 8px rgba(231, 76, 60, 0.2);
}
.btn-group {
  display: flex;
  gap: 10px;
}
/* Expand All / Collapse All Chapters — scaled down ~15% */
.btn {
  padding: 8px 17px;
  border: none;
  border-radius: 21px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.765em;
  /* Named properties rather than "all". "all" makes the browser watch
     every animatable property on the element for changes, including
     layout-affecting ones it will never actually need to animate. */
  transition: background-color 0.2s, transform 0.2s;
}
/* Deep navy for the chapter controls, so expand/collapse reads as a
   structural action rather than competing with the red used for identity. */
.btn-primary {
  background-color: #1d3557;
  color: #fff;
}
.btn-primary:hover {
  background-color: #16283f;
  transform: translateY(-1px);
}
.btn-secondary {
  background-color: #2c5282;
  color: #fff;
}
.btn-secondary:hover {
  background-color: #23426a;
  transform: translateY(-1px);
}
.toggle-btn {
  background: #1d3557;
  color: #fff;
  border: none;
  padding: 6px 14px;
  cursor: pointer;
  /* px, not em: sized in em it scaled with each h2, and without
     flex-shrink:0 it was squeezed by long chapter titles — so the button
     came out a different size in every chapter. */
  font-size: 13px;
  border-radius: 20px;
  flex-shrink: 0;
  white-space: nowrap;
  transition: background 0.2s;
}
.toggle-btn:hover {
  background: #16283f;
}

/* Grammar Tips Panel */
/* Same lightness as before, hue moved from warm red to blue so it belongs
   with the navy controls. */
.grammar-tip {
  background-color: #e2eaf5;
  border-left: 3px solid #2c5282;
  padding: 18px 22px;
  margin: 15px 0 25px 0;
  border-radius: 0 10px 10px 0;
  box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.usage-line {
  margin: 0 0 10px 0;
  font-size: 0.9em;
  color: #7f5539;
  background: #fdf2e9;
  display: inline-block;
  padding: 4px 12px;
  border-radius: 14px;
}
.flashcard-label {
  margin: 0;
  color: #fff;
  background: #1a3a6b;
  padding: 6px 14px;
  font-weight: 700;
  display: inline-block;
  border-radius: 14px;
  font-size: 0.9em;
}
/* Dark green pill sitting inline with the h1. Sized in em so it scales
   against the title rather than the body. */
.notice-label {
  /* position + overflow are what let the shine pseudo-element be clipped
     to the pill instead of sweeping across the whole heading. */
  position: relative;
  overflow: hidden;
  isolation: isolate;
  margin: 0 0 0 12px;
  color: #fff;
  background: #1e6b3a;
  padding: 2px 6px;
  font-weight: 400;
  display: inline-block;
  border-radius: 6px;
  font-size: 0.36em;
  vertical-align: middle;
  white-space: nowrap;
  /* Same drop shadow as the Engineering / I.T. chips. */
  box-shadow: -3px 4px 7px rgba(0,0,0,0.32);
  /* Two independent animations on one element: a slow breath on the whole
     pill, and a highlight sweep on the ::after below. Both are
     transform/opacity only, so they run on the compositor and never
     trigger layout — this sits next to the h1 on every page view. */
  animation: notice-pulse 3.2s ease-in-out infinite;
  will-change: transform;
}
/* The shine. A narrow diagonal band of light dragged across the pill, then
   a long pause before it repeats — 14% of a 6s cycle is the sweep, the rest
   is rest, which is what makes it read as an occasional glint rather than a
   loop. */
.notice-label::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 45%;
  height: 100%;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,0.55) 50%,
    rgba(255,255,255,0) 100%
  );
  transform: translateX(-220%) skewX(-18deg);
  animation: notice-shine 6s ease-in-out infinite;
}
@keyframes notice-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.045); }
}
@keyframes notice-shine {
  /* Held off screen for the first two thirds of the cycle. */
  0%, 62%  { transform: translateX(-220%) skewX(-18deg); }
  76%      { transform: translateX(320%) skewX(-18deg); }
  100%     { transform: translateX(320%) skewX(-18deg); }
}
/* Anyone who has asked their system to reduce motion gets the shadow and
   the colour, and nothing that moves. */
@media (prefers-reduced-motion: reduce) {
  .notice-label { animation: none; }
  .notice-label::after { animation: none; opacity: 0; }
}

/* Flashcard On/Off sliding toggle switch */
.fc-toggle {
  position: relative;
  display: inline-block;
  width: 86px;
  height: 32px;
  vertical-align: middle;
}
.fc-toggle input {
  display: none;
}
.fc-slider {
  position: absolute;
  inset: 0;
  background: #ccc;
  border-radius: 16px;
  cursor: pointer;
  transition: background-color 0.2s;
}
.fc-toggle input:checked + .fc-slider {
  background: #1a3a6b;
}
.fc-slider-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  background: #fff;
  border-radius: 50%;
  /* transform, not left. Animating "left" re-runs layout on every frame of
     the travel — the single most expensive way to move an element. transform
     is handled by the compositor and never touches layout, so the knob
     animates off the main thread. */
  transition: transform 0.13s cubic-bezier(0.22, 1, 0.36, 1);
  box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.fc-toggle input:checked + .fc-slider .fc-slider-knob {
  /* Travel only: width (86) − knob (26) − both 3px gutters = 54 */
  transform: translateX(54px);
}
.fc-slider-text {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.72em;
  font-weight: 700;
  color: #fff;
  transition: opacity 0.15s;
  pointer-events: none;
}
.fc-off-text {
  right: 9px;
  color: #666;
  opacity: 1;
}
.fc-on-text {
  left: 9px;
  opacity: 0;
}
.fc-toggle input:checked + .fc-slider .fc-on-text {
  opacity: 1;
}
.fc-toggle input:checked + .fc-slider .fc-off-text {
  opacity: 0;
}

/* With three of eight columns hidden, a table still stretched to width:100%
   would hand the freed space to the five that remain, so every cell changed
   size the moment the toggle was pressed. width:auto makes the table only
   as wide as the frozen column widths, so nothing moves. */
.chapter-wrapper table.fc-on {
  width: auto;
  margin-left: auto;
  margin-right: auto;
}

/* Flashcard Mode: hide English, Example, Examples columns */
table.fc-on th:nth-child(6),
table.fc-on td:nth-child(6),
table.fc-on th:nth-child(7),
table.fc-on td:nth-child(7),
table.fc-on th:nth-child(8),
table.fc-on td:nth-child(8) {
  display: none;
}

/* Per-row Show/Hide flashcard buttons */
/* Placeholder for the Show/Hide pair, swapped for the real buttons when
   its chapter scrolls into view. Occupies nothing until then. */
i.rt {
  display: none;
}
.row-toggle {
  margin-left: 6px;
  display: inline-flex;
  gap: 4px;
}
.row-show-btn, .row-hide-btn {
  font-size: 0.7em;
  font-weight: 700;
  border: none;
  border-radius: 10px;
  padding: 2px 8px;
  cursor: pointer;
}
.row-show-btn {
  background: #1a3a6b;
  color: #fff;
}
.row-hide-btn {
  background: #ecf0f1;
  color: #555;
}
/* Default: buttons only make sense in Flashcard mode (columns hidden) —
   "Show" is visible, "Hide" stays hidden, until a per-row override says
   otherwise. Outside Flashcard mode, columns are already visible, so
   "Hide" is what's offered by default. */
table:not(.fc-on) .row-toggle .row-show-btn { display: none; }
table:not(.fc-on) .row-toggle .row-hide-btn { display: inline-block; }
table.fc-on .row-toggle .row-show-btn { display: inline-block; }
table.fc-on .row-toggle .row-hide-btn { display: none; }

/* Per-row override: forces that single row's answer columns open/closed
   regardless of the global Flashcard state, and flips its own buttons. */
tr.row-force-show td:nth-child(6),
tr.row-force-show td:nth-child(7),
tr.row-force-show td:nth-child(8) {
  display: table-cell !important;
}
tr.row-force-hide td:nth-child(6),
tr.row-force-hide td:nth-child(7),
tr.row-force-hide td:nth-child(8) {
  display: none !important;
}
/* A force-shown row renders 8 cells while the flashcard header renders only
   5. That mismatch, plus min-width: 200px on the example cells and
   overflow: hidden on the table, squeezes the Example/Examples columns out
   of view. Bringing the headers back keeps the column count consistent.
   .row-forced-open is maintained by syncForcedHeaders(). */
table.fc-on.row-forced-open th:nth-child(6),
table.fc-on.row-forced-open th:nth-child(7),
table.fc-on.row-forced-open th:nth-child(8) {
  display: table-cell;
}
tr.row-force-show .row-toggle .row-show-btn { display: none !important; }
tr.row-force-show .row-toggle .row-hide-btn { display: inline-block !important; }
tr.row-force-hide .row-toggle .row-show-btn { display: inline-block !important; }
tr.row-force-hide .row-toggle .row-hide-btn { display: none !important; }
.grammar-tip h4 {
  margin: 0 0 10px 0;
  color: #1d3557;
  font-size: 1.1em;
  display: flex;
  align-items: center;
  gap: 8px;
}
.grammar-tip p {
  margin: 0 0 10px 0;
  font-size: 0.95em;
  color: #4a4a4a;
}
.grammar-tip p:last-child {
  margin-bottom: 0;
}
.grammar-tip code {
  background-color: #f8f8f8;
  padding: 2px 6px;
  border-radius: 4px;
  font-family: Consolas, Monaco, monospace;
  font-size: 0.9em;
  color: #c0392b;
}

/* Chapter and Table Styling */
.chapter-content {
  display: block;
  margin-top: 10px;
}
.page-break {
  page-break-before: always;
}
table {
  width: 100%;
  border-collapse: collapse;
  margin: 15px 0 40px 0;
  background: #fff;
  box-shadow: 0 2px 10px rgba(0,0,0,0.04);
  border-radius: 8px;
  overflow: hidden;
}
th, td {
  text-align: center;
  vertical-align: middle;
  border: 1px solid #f1f2f6;
  padding: 14px 18px;
  text-align: left;
  vertical-align: middle;
}
td:nth-child(2) {
  white-space: nowrap;
  min-width: 90px;
}
/* A solid red header band across every table is a lot of saturated colour
   for what is only a label row. Neutral ground with a small, tracked-out
   uppercase label reads as current, and it gives the red back its meaning
   elsewhere on the page. */
th {
  background-color: #1d3557;
  color: #fff;
  font-weight: 600;
  font-size: 0.78em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-bottom: 1px solid #16283f !important;
}
tr:nth-child(even) {
  background-color: #fcfdfe;
}
tr:hover {
  background-color: #f6f8f9;
}
.char {
  font-size: 1.4em;
  font-weight: bold;
  color: #111827;
}
.pinyin {
  color: #2563eb;
  font-weight: 600;
}
.tag-hsk1 {
  background-color: #ffebee;
  color: #c62828;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.tag-hsk2 {
  background-color: #e8f5e9;
  color: #2e7d32;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.tag-hsk3 {
  background-color: #e3f2fd;
  color: #1565c0;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.tag-hsk4 {
  background-color: #fff3e0;
  color: #ef6c00;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.tag-hsk5 {
  background-color: #f3e5f5;
  color: #6a1b9a;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.tag-hsk6 {
  background-color: #eceff1;
  color: #37474f;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 0.75em;
  font-weight: bold;
  display: inline-block;
}
.level-tag {
  cursor: pointer;
  /* box-shadow removed from the transition: it repaints a blurred region
     on every frame, and there are ~5,000 of these on the page. */
  transition: transform 0.15s;
  user-select: none;
  white-space: nowrap;
  display: inline-block;
}
.level-tag:hover {
  transform: scale(1.08);
  box-shadow: 0 2px 6px rgba(0,0,0,0.18);
}
.level-tag:active {
  transform: scale(0.96);
}

/* Level Filter Bar */
.level-filter-bar {
  background: #fafbfc;
  padding: 16px 20px;
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
  /* Same value as .control-panel — see note there */
  margin-bottom: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  border: 1px solid #e5e7eb;
}
/* Rows are separated by hairlines, not fills. Heavy zebra striping is the
   single most dated thing about a data table — a 1px divider and a hover
   state do the same job without adding two more tones to the palette. */
table { background: #fdfdfe; }
tr:nth-child(even) { background-color: #fafbfc; }
tr:hover { background-color: #f2f4f6; }
th, td { border-color: #eaecef; }
.level-filter-bar span.filter-label {
  font-weight: 600;
  color: #2c3e50;
  margin-right: 4px;
}
.level-btn {
  padding: 8px 18px;
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 20px;
  cursor: pointer;
  font-weight: 700;
  font-size: 0.9em;
  background: #fff;
  color: #555;
  transition: background-color 0.2s, border-color 0.2s, transform 0.2s;
}
.level-btn:hover {
  transform: translateY(-1px);
}
.level-btn.active {
  color: #fff;
  border-color: transparent;
}
.level-btn[data-level="all"].active { background: #2c3e50; }
.level-btn[data-level="1"].active { background: #c62828; }
.level-btn[data-level="2"].active { background: #2e7d32; }
.level-btn[data-level="3"].active { background: #1565c0; }
.level-btn[data-level="4"].active { background: #ef6c00; }
.level-btn[data-level="5"].active { background: #6a1b9a; }
.level-btn[data-level="6"].active { background: #37474f; }
.level-count {
  color: #7f8c8d;
  font-size: 0.85em;
  margin-left: auto;
}

/* Highlight search terms */
.highlight {
  background-color: #ffeb3b;
  padding: 2px;
  border-radius: 2px;
}
/* Yellow-on-black is harsh at night — dark grey block, light grey letters.
   !important because body.dark-mode .char / .ex-hanzi / td set their own
   colours at equal specificity and appear later in the sheet. */
body.dark-mode .highlight {
  background-color: #1e3a5f !important;
  color: #c9ccd1 !important;
}

/* Floating Flashcard switch, bottom-left, mirroring the jump buttons on the
   opposite side and using the same gutter maths on desktop. */
.fc-float {
  position: fixed;
  left: 10px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 1200;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 14px;
  border-radius: 26px;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  backdrop-filter: blur(2px);
}
.fc-float-label {
  font-weight: 700;
  font-size: 12px;
  color: #2c3e50;
  white-space: nowrap;
}
body.dark-mode .fc-float {
  background: rgba(62, 67, 76, 0.93);
}
body.dark-mode .fc-float-label {
  color: #dcdcdc;
}
@media (min-width: 769px) {
  .fc-float {
    left: max(10px, calc(50% - 860px));
    bottom: 28px;
  }
}

/* Mobile-only jump buttons, floating over the content bottom-right */
.jump-bar {
  display: none;
  position: fixed;
  right: 10px;
  /* env() clears the iOS home indicator / Safari bottom toolbar, which was
     cutting the buttons off; falls back to the plain value elsewhere. */
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 1200;
  gap: 8px;
  /* translateZ(0) is enough to give the bar its own compositor layer, which
     stops iOS repainting it on the main thread during the URL-bar
     animation. will-change is deliberately not used: it holds the layer in
     GPU memory for the whole session, and this element never animates. */
  transform: translateZ(0);
}
.jump-btn {
  /* #ffd8a8 taken 15% darker, at 72% opacity so the content shows through */
  background: rgba(217, 184, 143, 0.72);
  color: #6b3600;
  border: 1px solid rgba(208, 163, 104, 0.75);
  backdrop-filter: blur(2px);
  border-radius: 20px;
  font-family: inherit;
  font-weight: 700;
  font-size: 10px;
  line-height: 1;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0,0,0,0.22);
}
.jump-btn:active {
  background: rgba(196, 160, 116, 0.95);
}
/* Desktop: side by side in the right gutter. body is max-width 1400 + 20px
   padding, so its right edge sits at 50% + 720px; anchoring 140px beyond
   that keeps the pair clear of the table. max() stops them running off
   screen on narrower windows, where they tuck against the edge instead.
   row-reverse puts "Top of the page" on the right without reordering the
   markup. */
@media (min-width: 769px) {
  .jump-bar {
    display: flex;
    flex-direction: row-reverse;
    align-items: center;
    left: auto;
    right: max(10px, calc(50% - 860px));
    bottom: 28px;
  }
  .jump-btn {
    font-size: 12px;
    padding: 9px 14px;
    background: rgba(232, 232, 232, 0.9);
    color: #000;
    border-color: transparent;
  }
  .jump-btn:active {
    background: rgba(210, 210, 210, 0.95);
  }
}

/* Desktop table geometry.
   With the default table-layout: auto the browser derives column widths
   from the content that is currently visible — so filtering to one HSK
   level removes rows, the longest English gloss changes, and every column
   resizes. Fixed layout with declared proportions makes the geometry
   independent of the rows on screen, so filtering only ever adds or
   removes rows. It also keeps all 27 chapters in alignment with each
   other, which auto layout never did. */
@media (min-width: 769px) {
  .chapter-wrapper table { table-layout: fixed; }
  .chapter-wrapper th:nth-child(1), .chapter-wrapper td:nth-child(1) { width: 5%; }
  .chapter-wrapper th:nth-child(2), .chapter-wrapper td:nth-child(2) { width: 7%; }
  .chapter-wrapper th:nth-child(3), .chapter-wrapper td:nth-child(3) { width: 6%; }
  .chapter-wrapper th:nth-child(4), .chapter-wrapper td:nth-child(4) { width: 9%; }
  .chapter-wrapper th:nth-child(5), .chapter-wrapper td:nth-child(5) { width: 11%; }
  .chapter-wrapper th:nth-child(6), .chapter-wrapper td:nth-child(6) { width: 16%; }
  .chapter-wrapper th:nth-child(7), .chapter-wrapper td:nth-child(7) { width: 22%; }
  .chapter-wrapper th:nth-child(8), .chapter-wrapper td:nth-child(8) { width: 24%; }
  /* Fixed layout cannot widen a cell to fit, so long text must wrap. */
  .chapter-wrapper td { word-break: break-word; }
  .chapter-wrapper .ex-en { white-space: normal; }
}

/* Shown while a level is being fetched. Animated with transform only, so
   it runs on the compositor and costs nothing while the merge happens. */
body.levels-loading::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: #e74c3c;
  z-index: 9999;
  animation: level-load 0.9s ease-in-out infinite;
}
@keyframes level-load {
  0%   { transform: scaleX(0);   transform-origin: left; }
  50%  { transform: scaleX(1);   transform-origin: left; }
  51%  { transform: scaleX(1);   transform-origin: right; }
  100% { transform: scaleX(0);   transform-origin: right; }
}

/* Chapter index */
.chapter-nav {
  background: #fafbfc;
  border: 1px solid #eaeaea;
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
  padding: 12px 18px;
  margin-bottom: 18px;
}
.chapter-nav > summary {
  cursor: pointer;
  font-weight: 600;
  color: #2c3e50;
  font-size: 0.92em;
  list-style: none;
}
.chapter-nav > summary::-webkit-details-marker { display: none; }
.chapter-nav > summary::before {
  content: '▸';
  display: inline-block;
  margin-right: 8px;
  transition: transform 0.15s;
}
.chapter-nav[open] > summary::before {
  transform: rotate(90deg);
}
.chapter-nav nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}
.chapter-nav a {
  background: #f3f4f6;
  color: #374151;
  text-decoration: none;
  font-size: 0.78em;
  padding: 4px 11px;
  border-radius: 14px;
  white-space: nowrap;
  transition: background-color 0.15s;
}
.chapter-nav a:hover {
  background: #e74c3c;
  color: #fff;
}
body.dark-mode .chapter-nav {
  background: #23262d;
  border-color: #3a3d44;
}
body.dark-mode .chapter-nav > summary { color: #dcdcdc; }
body.dark-mode .chapter-nav a {
  background: #2f333b;
  color: #dcdcdc;
}
body.dark-mode .chapter-nav a:hover {
  background: #e74c3c;
  color: #fff;
}

/* Anchor targets. Without this an incoming #hash scrolls the heading to
   the very top of the viewport, which puts it behind the sticky search
   panel. scroll-margin-top reserves that space during the jump. */
.chapter-wrapper {
  scroll-margin-top: 110px;
}

/* Mobile-only hint above each word table */
.swipe-hint {
  display: none;
  /* ~68 characters now, so the size scales with screen width to hold it on
     one line; nowrap guarantees it. */
  font-size: clamp(8px, 3vw, 13px);
  white-space: nowrap;
  font-style: italic;
  color: #7f8c8d;
  margin: 0 0 6px 2px;
}
body.dark-mode .swipe-hint {
  color: #9aa0a6;
}

/* Audio Button Styling */
.audio-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 6px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s, transform 0.1s;
  outline: none;
}
.audio-btn:hover {
  background-color: #fadbd8;
  transform: scale(1.1);
}
.audio-btn:active {
  transform: scale(0.95);
}
.audio-btn svg {
  width: 20px;
  height: 20px;
  fill: #e74c3c;
}

/* Smaller audio button used inline in Grammar Tips, Example, and Examples text */
.audio-btn-sm {
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  transition: background-color 0.2s, transform 0.1s;
  outline: none;
  margin-right: 2px;
}
.audio-btn-sm:hover {
  background-color: #fadbd8;
  transform: scale(1.15);
}
.audio-btn-sm svg {
  width: 12px;
  height: 12px;
  fill: #e74c3c;
}

/* Theme Toggle Bar (top-left, mirrors the flag bar on top-right) */
.theme-bar {
  position: fixed;
  top: 26px;
  left: 20px;
  z-index: 1000;
  display: flex;
  /* Widened at runtime to the Feature roadmap button's width (see the
     sizing block in the script), so the two pills stack flush. The three
     buttons spread out across whatever width that turns out to be rather
     than clustering at the left. */
  justify-content: space-around;
  gap: 6px;
  box-sizing: border-box;
  background: rgba(255,255,255,0.95);
  padding: 8px 10px;
  border-radius: 30px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  border: 1px solid #e5e7eb;
}
.theme-btn {
  background: none;
  border: 2px solid transparent;
  border-radius: 50%;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s;
  padding: 0;
}
.theme-btn:hover {
  transform: scale(1.15);
}
.theme-btn.active-theme {
  border-color: #2c3e50;
  background: #f0f0f0;
}
.theme-btn svg {
  width: 19px;
  height: 19px;
}
.theme-btn.sun-btn svg { fill: #f39c12; }
.theme-btn.paper-btn svg { fill: #a9603c; }
.theme-btn.moon-btn svg { fill: #34495e; }

/* ---------------------------------------------------------------
   Announcements for features that do not exist yet. Two of them: a dead
   twin of the Flashcard switch, and an extra chip in the chapter index.
   Both are inert by construction rather than by a disabled attribute —
   neither carries a control or an href — so there is nothing to click,
   nothing in the tab order, and no handler to write.
   --------------------------------------------------------------- */

/* The dead toggle. Reuses .fc-toggle/.fc-slider unchanged, so it is the
   same grey as the real switch sitting in its off state — only the cursor
   and the knob's transition are dropped, since neither ever applies. */
/* ---------------------------------------------------------------
   About / Study tips — two stacked disclosure buttons under the theme
   toggle. Same pill as Donate, but the panels sit in NORMAL FLOW inside
   the column rather than position:absolute like Donate's. That is the
   whole point: an open panel pushes the button beneath it down instead of
   covering it, and the column collapses back when it closes.
   --------------------------------------------------------------- */
.info-stack {
  position: fixed;
  /* theme-bar: top 26 + 8 padding + 34 button + 8 padding = ends at 76. */
  top: 86px;
  left: 20px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 9px;
  max-width: calc(100vw - 40px);
}
.info-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
/* Matches .donate-btn exactly — height, padding, radius, shadow, hover. */
.info-btn {
  display: inline-flex;
  align-items: center;
  /* Icon and label packed left, so the two pills' icons line up in a
     column. Study tips is given the About button's width at runtime and
     has spare room; the arrow takes it up via margin-left:auto below.
     About is content-sized, so there is no free space for that auto margin
     to absorb and its layout is unchanged. */
  justify-content: flex-start;
  gap: 5px;
  height: 30px;
  padding: 0 11px;
  box-sizing: border-box;
  background: rgba(255,255,255,0.95);
  color: #2c3e50;
  border: 1px solid #eaeaea;
  border-radius: 30px;
  font-family: inherit;
  font-weight: 700;
  font-size: 0.75em;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  transition: transform 0.15s, background-color 0.15s;
}
.info-btn:hover {
  transform: scale(1.05);
}
.info-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}
.info-btn.about-btn .info-icon { fill: #2e7d5b; }
.info-btn.tips-btn .info-icon { fill: #b5761f; }
.info-arrow {
  width: 12px;
  height: 12px;
  fill: #7f8c8d;
  flex-shrink: 0;
  /* Takes up whatever width the label leaves over, so the chevron sits at
     the right edge of the pill rather than trailing the text. */
  margin-left: auto;
  transition: transform 0.2s;
}
.info-btn[aria-expanded="true"] .info-arrow {
  transform: rotate(180deg);
}
/* position:static is load-bearing. Give this absolute positioning and the
   button below would be overlapped instead of displaced. */
.info-panel {
  position: static;
  margin-top: 8px;
  width: 250px;
  max-width: calc(100vw - 48px);
  background: rgba(255,255,255,0.98);
  border: 1px solid #eaeaea;
  border-radius: 10px;
  padding: 10px 12px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  font-size: 0.72em;
  line-height: 1.55;
  color: #374151;
}
.info-panel[hidden] {
  display: none;
}
.info-panel p { margin: 0 0 8px; }
.info-panel p:last-child { margin-bottom: 0; }
/* Orange, no underline, and no font-size of its own — it inherits the
   panel's, so it sits at exactly the weight of the text around it. The
   footer link is the reference for the colour; the rule it carries is
   dropped here because this sits at the end of a short panel rather than
   inside running body copy. */
.info-link {
  color: #c4700f;
  text-decoration: none;
  font-weight: 700;
  transition: color 0.15s;
}
.info-link:hover,
.info-link:focus {
  color: #e08a2e;
  text-decoration: none;
}
body.dark-mode .info-link { color: #e8a34d; }
body.dark-mode .info-link:hover,
body.dark-mode .info-link:focus { color: #f0b872; }
body.sepia-mode .info-link { color: #a9643f; }
/* "Coming soon" notes inside the tips list. Same orange as the syllabus
   link so the two read as one accent colour, but bold and not a link —
   there is nothing to click.
   display:block puts each on its own line, and because it is a block child
   of the <li> its left edge is the <li>'s content edge — the same x as the
   bold lead-in above it, not indented under the text. */
.info-soon {
  display: block;
  margin-top: 3px;
  color: #c4700f;
  font-weight: 700;
}
body.dark-mode .info-soon { color: #e8a34d; }
body.sepia-mode .info-soon { color: #a9643f; }
.info-panel ul { margin: 0; padding-left: 16px; }
.info-panel li { margin-bottom: 5px; }
.info-panel li:last-child { margin-bottom: 0; }
body.dark-mode .info-btn {
  background: rgba(35,38,45,0.95);
  border-color: #3a3d44;
  color: #dcdcdc;
}
body.dark-mode .info-panel {
  background: rgba(35,38,45,0.98);
  border-color: #3a3d44;
  color: #c9ccd1;
}
body.sepia-mode .info-btn {
  background: #faf5ea;
  border-color: #ded0b8;
  color: #4d4437;
}
body.sepia-mode .info-panel {
  background: #faf5ea;
  border-color: #ded0b8;
  color: #4d4437;
}

.fc-toggle-soon,
.fc-toggle-soon .fc-slider {
  cursor: default;
}
.fc-toggle-soon .fc-slider-knob {
  /* No transition and no checked state — the knob never travels. */
  transition: none;
}
/* Dark grey and semi-transparent, so the two unreleased switches read as
   inactive next to the live blue Flashcard label rather than competing
   with it. Opacity is on the background only — dropping the whole element
   to 60% would take the white text with it. */
.soon-label {
  margin-left: 18px;
  background: rgba(74, 79, 87, 0.55);
}
body.dark-mode .soon-label { background: rgba(120, 126, 136, 0.4); }
body.sepia-mode .soon-label { background: rgba(109, 98, 82, 0.5); }
/* Only does anything in the mobile layout — see the media query. */
.fc-break {
  display: none;
}

/* The dead chapter chip. Same pill as .chapter-nav a, gold instead of grey,
   and no hover shift since it does not respond to anything. The drop shadow
   lifts it off the panel so it reads as a marker rather than another link
   in the row. */
.chapter-soon {
  background: #a67c0a;
  color: #fff;
  font-size: 0.78em;
  padding: 4px 11px;
  border-radius: 14px;
  white-space: nowrap;
  cursor: default;
  user-select: none;
  -webkit-text-fill-color: #fff;
  forced-color-adjust: none;
  /* Negative x throws the shadow to the left, positive y drops it down —
     so the light reads as coming from the upper right. */
  box-shadow: -3px 4px 7px rgba(0,0,0,0.32);
}
/* Held at the same gold in every theme — it is a status marker, and a
   shifting colour would read as a different state. Only the shadow changes,
   to stay visible against each surface. */
body.dark-mode .chapter-soon { box-shadow: -3px 4px 8px rgba(0,0,0,0.55); }
body.sepia-mode .chapter-soon { box-shadow: -3px 4px 7px rgba(90,70,30,0.4); }
/* Orange variant of the shared hover tip, used by the announcements above.
   Kept translucent like the default tip, with the blur retained so the
   label stays readable over whatever it happens to sit on.
   The grey is forced three ways because a browser force-dark pass repaints
   dark text on a light chip as white, and -webkit-text-fill-color is the
   one Chrome honours in that mode. */
.hover-tip.tip-soon {
  background: rgba(255, 159, 28, 0.45);
  color: #fff !important;
  -webkit-text-fill-color: #fff;
  forced-color-adjust: none;
  /* White on a 45%-opacity chip has little to hold it against a light
     background, so the label carries its own shadow rather than relying on
     the backdrop blur alone. */
  text-shadow: 0 1px 2px rgba(0,0,0,0.45);
  font-weight: 600;
  /* Slower than the 0.12s the hover tips use. This one is dismissed by a
     tap or by its own timer rather than by the pointer leaving, so the fade
     is the only signal that the dismissal registered — the same reasoning
     behind the Pix code's 250ms. */
  transition: opacity 0.25s ease;
}

/* ---------------------------------------------------------------
   Sepia Mode — warm paper tones, inverted from the other themes: the
   page is the deeper shade and the panels sit lighter on top of it,
   like sheets of paper on a desk. Contrast is kept deliberately low
   so it reads as easy on the eye rather than stark.
   --------------------------------------------------------------- */
body.sepia-mode {
  background-color: #e8dcc8;
  color: #4d4437;
}
body.sepia-mode h1 { color: #453424; border-bottom-color: #a9643f; }
body.sepia-mode h2 { color: #9c5c38; }
body.sepia-mode .control-panel,
body.sepia-mode .level-filter-bar,
body.sepia-mode .chapter-nav {
  background: #faf5ea;
  border-color: #ded0b8;
  /* Brown-tinted rather than grey. A neutral shadow is what makes warm
     palettes look dusty. */
  box-shadow: 0 1px 3px rgba(101, 78, 45, 0.08);
}
body.sepia-mode .theme-bar,
body.sepia-mode .flag-bar {
  background: rgba(250, 245, 234, 0.96);
  border-color: #ded0b8;
}
body.sepia-mode table { background: #fffdf7; }
body.sepia-mode th { background-color: #a9643f; color: #fff9f0; }
body.sepia-mode th, body.sepia-mode td { border-color: #eee4d2; }
body.sepia-mode tr:nth-child(even) { background-color: #f9f3e6; }
body.sepia-mode tr:hover { background-color: #f4ecdb; }
body.sepia-mode td { color: #4d4437; }
body.sepia-mode .pinyin { color: #7c5b3b; }
body.sepia-mode .char { color: #33291c; }
body.sepia-mode .ex-hanzi { color: #3a2e1f; }
body.sepia-mode .ex-sub { color: #857763; }
body.sepia-mode .grammar-tip {
  background-color: #f7f1e4;
  border-left-color: #a9643f;
}
body.sepia-mode .grammar-tip h4 { color: #9c5c38; }
body.sepia-mode .grammar-tip p { color: #4d4437; }
body.sepia-mode .grammar-tip code { background-color: #eee4d2; color: #453424; }
body.sepia-mode .usage-line { background: #f2e6cd; color: #7d5729; }
body.sepia-mode input[type="text"] {
  background: #fffdf7;
  color: #4d4437;
  border-color: #ded0b8;
}
body.sepia-mode .filter-label,
body.sepia-mode .chapter-nav > summary { color: #453424; }
body.sepia-mode .level-count,
body.sepia-mode .meta-info,
body.sepia-mode .swipe-hint { color: #857763; }
body.sepia-mode .chapter-nav a { background: #f0e7d6; color: #453424; }
body.sepia-mode .chapter-nav a:hover { background: #a9643f; color: #fff9f0; }
body.sepia-mode .highlight { background-color: #e6cd9a !important; color: #33291c !important; }
body.sepia-mode .fc-float { background: rgba(250, 245, 234, 0.97); }
body.sepia-mode .fc-float-label { color: #453424; }
body.sepia-mode .theme-btn.active-theme { border-color: #9c5c38; background: #eee4d2; }
body.sepia-mode .ex-line { border-bottom-color: #eee4d2; }
body.sepia-mode .notice-label { background: #6f7a42; }
/* Both expand controls take the chapter heading's terracotta, so the
   heading and the button that opens it read as the same object. */
body.sepia-mode .btn-primary,
body.sepia-mode .toggle-btn {
  background-color: #9c5c38;
  color: #fff9f0;
}
body.sepia-mode .btn-primary:hover,
body.sepia-mode .toggle-btn:hover {
  background-color: #874d2d;
}
body.sepia-mode .btn-secondary { background-color: #ece1cb; color: #453424; }
body.sepia-mode .btn-secondary:hover { background-color: #e0d3b6; }
body.sepia-mode .jump-btn {
  background: rgba(232, 220, 200, 0.9);
  color: #453424;
  border-color: rgba(169, 100, 63, 0.45);
}

/* Dark Mode overrides */
body.dark-mode {
  background-color: #181a1f;
  color: #dcdcdc;
}
body.dark-mode h1 {
  color: #f1f1f1;
  border-bottom-color: #e74c3c;
}
body.dark-mode h2 {
  color: #ff8a80;
  border-bottom-color: #3a3d44;
}
body.dark-mode .meta-info {
  color: #9aa0a6;
}
body.dark-mode .control-panel,
body.dark-mode .level-filter-bar {
  background: #23262d;
  border-color: #3a3d44;
}
body.dark-mode .filter-label {
  color: #dcdcdc;
}
body.dark-mode input[type="text"] {
  background: #181a1f;
  color: #dcdcdc;
  border-color: #3a3d44;
}
body.dark-mode .grammar-tip {
  background-color: #23262d;
  border-left-color: #e74c3c;
}
body.dark-mode .grammar-tip h4 {
  color: #ff8a80;
}
body.dark-mode .grammar-tip p {
  color: #dcdcdc;
}
/* The base .grammar-tip code rule sets a near-white #f4f4f4 chip with no
   dark-mode counterpart, so at night it was light text on a light box.
   Lighter than the panel behind it, but dark enough to read against. */
body.dark-mode .grammar-tip code {
  background-color: #3f434b;
  color: #f0f0f0;
}
body.dark-mode .usage-line {
  background: #3a2f26;
  color: #f0c9a0;
}
body.dark-mode table {
  background: #23262d;
  box-shadow: none;
}
body.dark-mode th, body.dark-mode td {
  border-color: #3a3d44;
}
/* Burnt orange rather than the lighter #e67e22. Deeper and more saturated,
   and dark enough that white text clears the 4.5:1 contrast threshold —
   white on a mid orange does not, which is why the text was dark before. */
body.dark-mode th {
  background-color: #bf5700;
  color: #fff;
  border-bottom-color: #a34a00 !important;
}
/* Expand orange, collapse neutral, so the pair is not two equal-weight
   actions competing for attention. */
body.dark-mode .btn-primary {
  background-color: #bf5700;
  color: #fff;
}
body.dark-mode .btn-primary:hover {
  background-color: #a84c00;
}
body.dark-mode .btn-secondary {
  background-color: #3a3d44;
  color: #dcdcdc;
}
body.dark-mode .btn-secondary:hover {
  background-color: #4a4e57;
}
body.dark-mode .toggle-btn {
  background: #bf5700;
  color: #fff;
}
body.dark-mode .toggle-btn:hover {
  background: #a84c00;
}
body.dark-mode tr:nth-child(even) {
  background-color: #1f2127;
}
body.dark-mode tr:hover {
  background-color: #2a2d34;
}
body.dark-mode .char {
  color: #f1f1f1;
}
body.dark-mode .pinyin,
body.dark-mode td {
  color: #dcdcdc;
}
body.dark-mode .ex-hanzi {
  color: #f1f1f1;
}
body.dark-mode .ex-sub {
  color: #9aa0a6;
}
body.dark-mode .theme-bar,
body.dark-mode .flag-bar {
  background: rgba(35,38,45,0.95);
  border-color: #3a3d44;
}

/* Example & Examples columns */
.example-cell, .examples-cell {
  min-width: 200px;
  font-size: 0.88em;
}
.ex-line {
  margin-bottom: 6px;
  padding-bottom: 6px;
  border-bottom: 1px dashed #eee;
}
/* Pinyin and English each get their own line, no dash between them */
.ex-pinyin,
.ex-en {
  display: block;
  white-space: nowrap;
}
/* Every cell and header in a chapter table is centred, including the
   nested example blocks which carry their own alignment. */
.chapter-wrapper th,
.chapter-wrapper td,
.ex-line,
.examples-list,
.example-cell,
.examples-cell,
.ex-hanzi,
.ex-pinyin,
.ex-en,
.ex-sub,
.char,
.pinyin,
.usage-line {
  text-align: center;
}
.examples-header,
.ex-step {
  justify-content: center;
}
.ex-line:last-child {
  margin-bottom: 0;
  border-bottom: none;
}
.ex-hanzi {
  color: #2c3e50;
  font-weight: 600;
}
.ex-sub {
  display: block;
  color: #7f8c8d;
  font-size: 0.92em;
  margin-top: 1px;
}
.examples-header {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 4px;
}
.ex-all-btn {
  background: #ecf0f1;
  color: #2c3e50;
  border: none;
  border-radius: 10px;
  font-size: 0.72em;
  font-weight: 700;
  padding: 2px 9px;
  cursor: pointer;
  transition: background 0.15s;
}
.ex-all-btn:hover { background: #dfe6e9; }
/* Hover tip. A pseudo-element on the button was being clipped: it sits
   inside .table-scroll, which has overflow-x:auto, and inside the table,
   which has overflow:hidden — an absolutely positioned child cannot escape
   either. So it is a single fixed-position element parented to <body>
   instead, positioned by script. */
.hover-tip {
  position: fixed;
  z-index: 9999;
  background: rgba(58, 58, 58, 0.5);
  color: #d8d8d8;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.2px;
  padding: 5px 10px;
  border-radius: 5px;
  white-space: nowrap;
  pointer-events: none;
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity 0.12s ease;
  /* Keeps the text legible against whatever it happens to be over at 50%
     opacity. */
  backdrop-filter: blur(3px);
}
.hover-tip.is-on { opacity: 1; }
.ex-all-btn.ex-all-active { background: #2c3e50; color: #fff; }
.ex-add-btn {
  display: block;
  margin: 4px auto 0;
  width: 22px;
  height: 22px;
  line-height: 20px;
  text-align: center;
  border-radius: 50%;
  border: 1px solid #ddd;
  background: #fff;
  color: #e74c3c;
  font-weight: 700;
  font-size: 0.95em;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, transform 0.15s;
}
.ex-add-btn:hover:not(:disabled) { background: #fff5f4; transform: scale(1.1); }
.ex-add-btn:disabled {
  opacity: 0.3;
  cursor: default;
  transform: none;
}

/* Stepper row holding the + / − example controls */
.ex-step {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 4px;
}
/* Neutralise .ex-add-btn's own block layout now that it sits in a flex row */
.ex-step .ex-add-btn { margin: 0; }
.ex-remove-btn {
  display: block;
  width: 22px;
  height: 22px;
  line-height: 20px;
  text-align: center;
  border-radius: 50%;
  border: 1px solid #ddd;
  background: #fff;
  color: #7f8c8d;
  font-weight: 700;
  font-size: 0.95em;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, transform 0.15s;
}
.ex-remove-btn:hover:not(:disabled) { background: #f4f6f6; transform: scale(1.1); }
.ex-remove-btn:disabled {
  opacity: 0.3;
  cursor: default;
  transform: none;
}

/* Fixed overlays are positioned so their vertical centre lands on the
   search input of the sticky .control-panel (top: 10px + 21px of panel
   padding/border + half of the 47px input ≈ 54px from the viewport top). */

/* Flags on top, Ko-fi button beneath them. The stack is anchored so the
   flag bar — the first item — lands on the search bar's centre line; the
   Ko-fi button then hangs below it. */
.top-right-stack {
  position: fixed;
  top: 30px;
  right: 20px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 9px;
  max-width: calc(100vw - 40px);
}
/* Language Flag Bar */
.flag-bar {
  display: flex;
  gap: 6px;
  background: rgba(255,255,255,0.95);
  padding: 8px 10px;
  border-radius: 30px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  border: 1px solid #e5e7eb;
}
.kofi-bar {
  display: flex;
  justify-content: flex-end;
}
/* Donate + Request a feature share a row beneath the flags */
.action-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.donate-wrap {
  position: relative;
}
.donate-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 30px;
  padding: 0 11px;
  background: rgba(255,255,255,0.95);
  color: #2c3e50;
  border: 1px solid #eaeaea;
  border-radius: 30px;
  font-family: inherit;
  font-weight: 700;
  font-size: 0.75em;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  transition: transform 0.15s, background-color 0.15s;
}
.donate-btn:hover {
  transform: scale(1.05);
}
/* Coin */
.donate-check {
  width: 14px;
  height: 14px;
  fill: #e0a400;
  flex-shrink: 0;
}
.donate-arrow {
  width: 12px;
  height: 12px;
  fill: #7f8c8d;
  flex-shrink: 0;
  transition: transform 0.2s;
}
.donate-btn[aria-expanded="true"] .donate-arrow {
  transform: rotate(180deg);
}
/* Absolutely positioned so opening it cannot shift the bars above */
.donate-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: rgba(255,255,255,0.98);
  border: 1px solid #eaeaea;
  border-radius: 10px;
  padding: 10px 12px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
}
.donate-panel[hidden] {
  display: none;
}
/* Kills the descender gap under PayPal's image button */
.donate-panel form {
  line-height: 0;
}
/* PayPal's GIF is 74x21 natively; pinning the width keeps it and the Pix
   button below it the same size at every breakpoint. */
.donate-panel input[type="image"] {
  display: block;
  width: 74px;
  height: auto;
}
/* Shrink-wraps the button, so left:50% on .pix-code is the button's centre */
.pix-wrap {
  position: relative;
  display: inline-block;
  margin-top: 8px;
}
/* PayPal's yellow, but a pill like the Ko-fi button and black label */
.pix-btn {
  display: block;
  min-width: 74px;
  background: #FFC439;
  /* !important so no dark-mode or injected stylesheet can lighten it.
     -webkit-text-fill-color is what browser force-dark modes actually
     override; plain color alone does not stop them. color-scheme: only
     opts this element out of automatic darkening. */
  color: #000 !important;
  -webkit-text-fill-color: #000 !important;
  color-scheme: only light;
  forced-color-adjust: none;
  border: none;
  border-radius: 30px;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  padding: 5px 16px;
  cursor: pointer;
  transition: background-color 0.15s;
}
.pix-btn:hover {
  background: #f0b72f;
}
/* Floats free, centred under the Pix button — absolute, so showing it never
   resizes or reflows the Donate panel above. */
.pix-code {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: rgba(55,58,64,0.85);
  color: #fff;
  font-family: 'Courier New', monospace;
  font-size: 11px;
  line-height: 1.4;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
  text-align: center;
  box-shadow: 0 4px 14px rgba(0,0,0,0.18);
  opacity: 0;
  transition: opacity 0.25s ease;
}
.pix-code.is-visible {
  opacity: 1;
}
.pix-code[hidden] {
  display: none;
}
body.dark-mode .donate-btn,
body.dark-mode .donate-panel {
  background: rgba(35,38,45,0.95);
  border-color: #3a3d44;
}
body.dark-mode .donate-btn {
  color: #dcdcdc;
}
/* Small syllabus caption, above the flags in the top-right corner.
   position: fixed pins it to the viewport, so it stays put while the page
   scrolls underneath — deliberately unlike the flag bar and search panel,
   which travel with the content. pointer-events is NOT set to none here:
   the caption is a link now and has to stay clickable. */
.syllabus-note {
  position: fixed;
  top: 6px;
  right: 24px;
  z-index: 1000;
  font-size: 11px;
  color: #7f8c8d;
  letter-spacing: 0.2px;
}
/* Deliberately undecorated — it sits above the flag bar as a caption, and
   a rule under it read as clutter at 11px. The hover colour is the only
   affordance. */
.syllabus-note a {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s ease;
}
.syllabus-note a:hover,
.syllabus-note a:focus {
  color: #f0a05a;
  text-decoration: none;
}
body.dark-mode .syllabus-note {
  color: #9aa0a6;
}
body.sepia-mode .syllabus-note {
  color: #7d6f5c;
}
/* On small screens the corner has no room to spare once the flag bar is
   in place, so the caption moves into the footer instead — see
   .footer-syllabus-note. */
@media (max-width: 900px) {
  .syllabus-note { display: none; }
}

/* ---------------------------------------------------------------
   Page footer. Mirrors the h1 treatment — same 3px orange rule, same
   centred column — so the page is bracketed by the identical device
   top and bottom. Font size matches .meta-hint (the "Click on any
   button..." caption) on both breakpoints.
   --------------------------------------------------------------- */
/* Full content width, so the rule spans exactly what the h1 rule and the
   tables span — body is the shared container (max-width 1400px, 20px
   padding) and nothing here narrows it. Border matches the h1 declaration
   character for character: 3px solid #e74c3c. */
.site-footer {
  margin: 48px 0 32px;
  padding-top: 18px;
  border-top: 3px solid #e74c3c;
  text-align: center;
  color: #6b7280;
  /* Same as the table body text, which carries no font-size of its own and
     so renders at the inherited 1em. */
  font-size: 1em;
  font-weight: 400;
  line-height: 1.7;
}
.site-footer p {
  margin: 0;
  font-weight: 400;
}
/* Blank-line spacing expressed as real height rather than empty <br>s, so
   it scales with the font instead of drifting. 1.7em is one line at this
   line-height; .footer-gap is two. */
.site-footer .footer-gap {
  height: 3.4em;
}
.site-footer .footer-gap-sm {
  height: 1.7em;
}
.site-footer a {
  color: #c0392b;
  text-decoration: none;
  border-bottom: 1px solid rgba(192,57,43,0.35);
}
.site-footer a:hover,
.site-footer a:focus {
  border-bottom-color: #c0392b;
}
.site-version {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.3px;
}
/* Desktop keeps the caption pinned in the top-right corner, so the footer
   copy of it is redundant there. */
.footer-syllabus-note {
  display: none;
}
body.dark-mode .site-footer {
  color: #9aa0a6;
  border-top-color: #e74c3c;
}
body.dark-mode .site-footer a {
  color: #ff8a80;
  border-bottom-color: rgba(255,138,128,0.35);
}
body.sepia-mode .site-footer {
  color: #6b6151;
  border-top-color: #a9643f;
}
body.sepia-mode .site-footer a {
  color: #9c5c38;
  border-bottom-color: rgba(156,92,56,0.35);
}
/* The Ko-fi widget injects its own stylesheet, so these overrides need
   !important. Sizing is left at Ko-fi's defaults; only the corner radius
   is changed, to the same 30px pill as the flag bar. */
.kofi-bar a {
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  border-radius: 30px !important;
  overflow: hidden;
  transition: transform 0.15s;
}
.kofi-bar a:hover {
  transform: scale(1.05);
}
.kofi-bar .kofitext {
  /* Scaled down to sit level with the Donate button; Ko-fi's defaults are
     line-height 33px and padding 0 12px. */
  line-height: 30px !important;
  padding: 0 14px !important;
  font-size: 0.8em !important;
}
.kofi-bar .kofiimg {
  height: 11px !important;        /* Ko-fi default 13px */
  width: 16px !important;         /* Ko-fi default 20px */
  margin-bottom: 2px !important;
}
.kofi-bar .btn-container {
  line-height: 0;
}
.flag-btn {
  background: none;
  border: 2px solid transparent;
  border-radius: 50%;
  font-size: 1.3em;
  line-height: 1;
  padding: 4px;
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s;
}
.flag-btn:hover {
  transform: scale(1.15);
}
.flag-btn.active-flag {
  border-color: #e74c3c;
  background: #fff5f4;
}
.flag-btn img {
  display: block;
  width: 26px;
  height: 19px;
  object-fit: cover;
  border-radius: 3px;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.15);
  pointer-events: none;
}
/* Hide the visible default Google Translate widget; we drive it via our flags */
#google_translate_element { display: none !important; }
.goog-te-banner-frame { display: none !important; }
body { top: 0 !important; }
.goog-tooltip, .goog-tooltip:hover { display: none !important; }
.goog-text-highlight { background: none !important; box-shadow: none !important; }

/* ==========================================================
   Responsive layout
   ========================================================== */

/* Each table gets this wrapper at runtime (wrapTablesForScroll). The 8
   columns cannot shrink below their content, so on a narrow screen the
   table scrolls sideways inside the wrapper instead of forcing the whole
   page to scroll and pushing the header bars out of reach. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Lets the browser skip layout and paint for tables that are off screen.
     With ~5,000 rows this is the difference between the flashcard toggle
     reflowing the entire document and reflowing only what is visible.
     contain-intrinsic-size supplies a placeholder height so the scrollbar
     does not jump as tables come into view. */
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
  /* Cross-fade when flashcard mode switches. Applied to the ~27 table
     wrappers rather than the ~15,000 answer cells — display:none cannot be
     animated, and transitioning every cell would cost more than the reflow
     it is meant to disguise. */
  transition: opacity 0.04s ease;
}
body.fc-switching .table-scroll {
  opacity: 0.12;
}

@media (max-width: 768px) {
  /* Lock the document so it cannot scroll — that is what keeps the phone's
     URL bar from ever collapsing. All scrolling moves into #pageScroll. */
  html, body {
    height: 100%;
    overflow: hidden;
  }
  body {
    padding: 0;
    -webkit-text-size-adjust: 100%;
  }
  #pageScroll {
    position: relative;   /* anchor for the absolutely-placed header controls */
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    /* Vertical panning only, so a sideways swipe cannot drag the page and
       expose the margin. Tables re-enable horizontal panning below. */
    touch-action: pan-y;
    box-sizing: border-box;
    /* Clears the fixed search panel and the three header rows beneath it:
       controls (+14), Study tips / About (+44), Ko-fi (+108). */
    padding: calc(var(--panel-h, 96px) + 148px) 12px 24px;
  }
  /* The one place a sideways swipe is allowed */
  .table-scroll {
    touch-action: pan-x pan-y;
    overscroll-behavior-x: contain;
  }

  /* ---- Tap responsiveness ----
     touch-action: manipulation drops the ~300ms wait mobile browsers keep
     after a tap in case a second one follows (double-tap to zoom). That
     delay is the single biggest cause of buttons feeling sluggish. */
  button,
  a,
  input,
  select,
  label,
  [role="button"] {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
  }
  /* Stops a tap reading as a text/image selection: no blue highlight, no
     long-press callout menu, no draggable flag or icon images. This is what
     made the flashcard switch look like it was being selected rather than
     pressed. */
  button,
  label,
  .fc-toggle,
  .fc-slider,
  .fc-slider-text,
  .fc-slider-knob,
  .level-tag,
  .jump-btn,
  .swipe-hint {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  button img,
  .flag-btn img,
  .kofi-bar img {
    -webkit-user-drag: none;
    user-drag: none;
    pointer-events: none;
  }
  /* Press feedback for the switch, so it behaves like a button */
  .fc-toggle:active .fc-slider {
    transform: scale(0.95);
  }
  .fc-slider {
    transition: background-color 0.15s, transform 0.08s;
  }
  /* :hover latches on after a tap on touch screens, so a scale-up stays
     stuck until you tap elsewhere and reads as lag. Swap the hover growth
     for an instant :active response. */
  .flag-btn:hover,
  .theme-btn:hover,
  .donate-btn:hover,
  .jump-btn:hover,
  .kofi-bar a:hover,
  .audio-btn:hover,
  .audio-btn-sm:hover,
  .chapter-audio-btn:hover,
  .level-btn:hover,
  .btn:hover,
  .ex-add-btn:hover,
  .ex-remove-btn:hover {
    transform: none;
    box-shadow: none;
  }
  /* Tactile press: a small scale-down reads as physical depression and
     lands within one frame, which is what makes a tap feel immediate
     rather than merely fast. */
  button:active,
  .kofi-bar a:active,
  .level-tag:active,
  .jump-btn:active {
    transform: scale(0.93);
    opacity: 0.72;
  }
  .audio-btn:active,
  .audio-btn-sm:active,
  .chapter-audio-btn:active {
    transform: scale(0.86);
    opacity: 1;
  }
  /* Long transitions delay the feedback the tap is meant to give. Ease-out
     so the release settles rather than snapping back. */
  button,
  .kofi-bar a,
  .level-tag {
    transition: transform 0.05s ease-out, opacity 0.05s ease-out, background-color 0.05s ease-out !important;
  }
  /* Scroll smoothness: keep each chapter's layout work contained to itself,
     so a change in one cannot force the other 26 to be recalculated. */
  .chapter-wrapper {
    contain: layout style;
  }
  /* Skip layout and paint for grammar panels that are off screen too */
  .grammar-tip {
    content-visibility: auto;
    contain-intrinsic-size: auto 160px;
  }
  /* Row hover cannot be used on a touch screen, but the rule still makes
     the browser hit-test and re-evaluate style for rows under the finger
     while scrolling. */
  tr:hover {
    background-color: inherit;
  }
  /* Shadows on a scrolling table are repainted continuously for no benefit
     at this size. */
  table {
    box-shadow: none;
  }
  /* Paint containment on each table wrapper, so nothing inside one can
     trigger work outside it. */
  .table-scroll {
    contain: content;
  }
  h1 {
    font-size: 1.73em;
    padding-bottom: 8px;
  }
  .notice-label {
    font-size: 0.5em;
    margin-left: 8px;
    padding: 2px 7px;
  }
  /* Both strings run ~68 characters. Sizing in vw keeps them on one line at
     any phone width instead of picking a fixed size that only fits one
     device; nowrap guarantees it, clamp stops it going illegibly small on
     very narrow screens or oversized on large ones. */
  /* Both strings run ~69 characters. At roughly 0.45em average glyph width
     that puts the ceiling near 3vw before they would need to wrap; 2.85vw
     leaves a little headroom for font substitution. */
  .meta-info,
  .meta-hint {
    font-size: clamp(8px, 2.85vw, 16px);
    white-space: nowrap;
    margin-bottom: 16px;
  }
  /* Sits lower, clear of the filter box above it */
  .meta-hint {
    margin-top: 2px;
    margin-bottom: 18px;
  }
  /* Matches the mobile table cells (th, td at 0.8em). Full width like the
     tables, and wrapping freely — it holds a sentence, not a caption, so
     the nowrap used on .meta-hint must not apply here. */
  .site-footer {
    font-size: 0.8em;
    margin: 34px 0 24px;
    padding-top: 14px;
  }
  .site-footer .footer-gap {
    height: 3.4em;
  }
  .site-footer .footer-gap-sm {
    height: 1.7em;
  }
  /* Directly under the syllabus link, no gap between them. */
  .footer-syllabus-note {
    display: block;
  }
  .meta-line-2 {
    display: block;
    margin-top: 2px;
  }
  .title-prefix {
    display: none;
  }

  /* The three stacked panels — search, flashcard, level filter — share one
     set of box metrics so their edges line up as a single column.
     border-box matters here: without it their differing padding would make
     them render at different widths. */
  .control-panel,
  .level-filter-bar {
    box-sizing: border-box;
    width: 100%;
    max-width: 100%;
    margin-left: 0;
    margin-right: 0;
    margin-bottom: 12px;
    padding: 12px 14px;
  }

  /* Header layout:
       row 1 — theme toggle, then Donate, both on the left; flags on the right
       row 2 — Request a feature, right edge flush with the flags
     position:absolute rather than fixed, so the whole header scrolls away
     with the page instead of staying stuck over the content. body already
     has position:relative, so these anchor to the top of the document.
     display:contents dissolves the desktop wrappers so each control can be
     positioned on its own. */
  .top-right-stack,
  .action-row {
    display: contents;
  }
  /* Offsets start below the fixed search panel, then scroll away normally.
     Donate sits first, theme toggle to its right. */
  .donate-wrap {
    position: absolute;
    top: calc(var(--panel-h, 96px) + 14px);
    left: 6px;
    z-index: 1000;
    margin-top: 0;
  }
  /* Centred in the gap between Donate (ends ~93px) and the flag bar
     (starts ~169px on a 375px screen): midpoint 131, minus half the bar. */
  .theme-bar {
    position: absolute;
    top: calc(var(--panel-h, 96px) + 14px);
    left: 104px;
    padding: 4px 6px;
    gap: 4px;
  }
  .flag-bar {
    position: absolute;
    top: calc(var(--panel-h, 96px) + 14px);
    right: 6px;
    z-index: 1000;
    flex-wrap: nowrap;
    padding: 5px 6px;
    gap: 3px;
  }
  /* Third row, under the Study tips / About row, dropped a further line so
     it clears them cleanly. #pageScroll's padding below moves with it, so
     the page content follows rather than being overlapped. */
  .kofi-bar {
    position: absolute;
    top: calc(var(--panel-h, 96px) + 108px);
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    z-index: 1000;
  }
  /* Donate's panel opens directly over where Study tips sits, so the button
     steps aside while the panel is open and comes back when it closes.
     The class is set in Donate's own setOpen(). */
  body.donate-open .tips-wrap {
    display: none;
  }
  /* The column dissolves on mobile so its two halves can sit on opposite
     sides of the same row: Study tips under Donate on the left, About under
     the flags on the right. display:contents removes the wrapper from the
     box tree without removing its children. */
  .info-stack {
    display: contents;
  }
  .tips-wrap,
  .about-wrap {
    position: absolute;
    /* Donate and the flag bar sit at +14 and stand 24px tall. */
    top: calc(var(--panel-h, 96px) + 44px);
    /* Above the Ko-fi button, which is z-index 1000 — otherwise "Request a
       feature" renders over an open panel and sits in the reading. */
    z-index: 1500;
  }
  .tips-wrap {
    left: 6px;
  }
  /* Right-aligned to match the flags above it, and align-items:flex-end
     makes the panel hang from the right edge rather than the left.
     The extra 5px pushes it down onto the same line as Study tips: the
     flag bar is taller than the Donate pill it sits beside, so measuring
     both from the same +44px left About riding high. */
  .about-wrap {
    top: calc(var(--panel-h, 96px) + 49px);
    right: 6px;
    align-items: flex-end;
  }
  .info-btn {
    height: 24px;
    padding: 0 9px;
    font-size: 0.6em;
    gap: 4px;
  }
  .info-icon {
    width: 11px;
    height: 11px;
  }
  .info-arrow {
    width: 10px;
    height: 10px;
  }
  .info-panel {
    width: calc(100vw - 24px);
    max-width: 340px;
    margin-top: 6px;
    padding: 8px 10px;
    font-size: 0.62em;
  }

  /* Theme toggle at 50% */
  .theme-btn {
    width: 17px;
    height: 17px;
    border-width: 1px;
  }
  .theme-btn svg {
    width: 12px;
    height: 12px;
  }

  /* Everything else at 80% */
  .flag-btn {
    padding: 2px;
    border-width: 2px;
  }
  .flag-btn img {
    width: 16px;
    height: 12px;
  }
  .donate-btn {
    height: 24px;
    padding: 0 9px;
    font-size: 0.6em;
    gap: 4px;
  }
  .donate-check {
    width: 11px;
    height: 11px;
  }
  .donate-arrow {
    width: 10px;
    height: 10px;
  }
  /* Three label+switch pairs on one phone-width row. At the desktop sizes
     (86px track, 0.9em label) they need roughly 560px; these reductions
     bring the set inside ~350px. The bar already wraps, so a very narrow
     screen drops the third pair onto a second line rather than clipping. */
  #flashcardBar {
    gap: 4px 6px;
    /* Left-aligned so the second row starts under the first rather than
       being centred independently of it. */
    justify-content: flex-start;
  }
  .fc-break {
    display: block;
    flex-basis: 100%;
    height: 0;
  }
  #flashcardBar .flashcard-label {
    font-size: 0.6em;
    padding: 4px 8px;
    border-radius: 11px;
  }
  .soon-label {
    margin-left: 0;
  }
  /* Track shrinks from 86x32 to 58x24, so the knob and its travel have to
     be recomputed: travel = width − knob − both gutters = 58 − 20 − 4. */
  #flashcardBar .fc-toggle {
    width: 58px;
    height: 24px;
  }
  #flashcardBar .fc-slider {
    border-radius: 12px;
  }
  #flashcardBar .fc-slider-knob {
    width: 20px;
    height: 20px;
    top: 2px;
    left: 2px;
  }
  #flashcardBar .fc-toggle input:checked + .fc-slider .fc-slider-knob {
    transform: translateX(34px);
  }
  #flashcardBar .fc-slider-text {
    font-size: 0.58em;
  }
  #flashcardBar .fc-off-text { right: 6px; }
  #flashcardBar .fc-on-text { left: 6px; }
  /* Request a feature. Ko-fi's anchor carries its own padding and leading,
     which held the orange box open no matter how small the label got.
     Zeroing both and making it an inline-flex box means the pill is exactly
     as tall as the text line inside it. */
  .kofi-bar a {
    display: inline-flex !important;
    align-items: center;
    padding: 0 !important;
    line-height: 0 !important;
  }
  .kofi-bar .kofitext {
    display: inline-block;
    line-height: 16px !important;
    padding: 0 7px !important;
    font-size: 0.82em !important;
  }
  .kofi-bar .kofiimg {
    height: 7px !important;
    width: 10px !important;
    margin: 0 3px 0 0 !important;
  }
  /* Kept in step with the PayPal image above it */
  .donate-panel input[type="image"] {
    width: 62px;
  }
  .pix-btn {
    min-width: 62px;
    font-size: 9px;
    padding: 4px 10px;
  }
  .audio-btn svg {
    width: 16px;
    height: 16px;
  }
  .audio-btn-sm svg {
    width: 10px;
    height: 10px;
  }
  .ex-all-btn {
    font-size: 0.58em;
    padding: 2px 7px;
  }
  .ex-add-btn, .ex-remove-btn {
    width: 18px;
    height: 18px;
    line-height: 16px;
    font-size: 0.76em;
  }
  .row-show-btn, .row-hide-btn {
    font-size: 0.66em;
    padding: 3px 8px;
  }
  .level-btn {
    font-size: 0.72em;
    padding: 5px 11px;
  }
  /* margin-left:auto pushes this to the far right on desktop. On mobile,
     width:100% breaks it onto its own line in the wrapping flex row and
     margin-left:0 lines it up with the "Filter by Level:" label. */
  .level-count {
    font-size: 0.72em;
    width: 100%;
    margin-left: 0;
    margin-top: 2px;
    text-align: center;
  }
  /* No transform here. A transformed ancestor becomes the containing block
     for position:fixed descendants, which is what was clipping the Pix code
     box. Donate is at the left edge now, so plain left alignment works. */
  .donate-panel {
    right: auto;
    left: 0;
    transform: none;
  }
  /* Sits clear of the fixed search panel and above everything else */
  .pix-code {
    top: calc(var(--panel-h, 96px) + 118px);
    z-index: 1400;
  }

  /* Expand / Collapse All Chapters */
  .btn {
    font-size: 0.63em;
    padding: 5px 10px;
    border-radius: 12px;
    flex: 1;
  }

  /* The header scrolls away, so this parks near the top edge. z-index keeps
     it above the absolutely-positioned header controls it passes over, so
     it settles at the same 12px whether you are scrolling up or down. */
  /* position:fixed, not sticky. Sticky is recalculated on every scroll
     frame, which is what let it drift; a fixed element is painted once
     against the viewport and cannot move. It sits outside the scroll flow,
     so #pageScroll reserves space for it via --panel-h, measured in JS
     because the panel's height depends on how its buttons wrap. */
  .control-panel {
    position: fixed;
    top: 6px;
    left: 8px;
    right: 8px;
    width: auto;
    margin: 0;
    gap: 10px;
    z-index: 1300;
  }
  /* Level filter: centres the wrapped rows, so HSK 3-6 sit mid-screen */
  #levelFilterBar {
    justify-content: center;
  }
  .search-box {
    min-width: 100%;
  }
  .search-box input {
    font-size: 0.8em;
    padding: 10px 16px;
  }
  .btn-group {
    width: 100%;
  }

  .swipe-hint {
    display: block;
  }
  .jump-bar {
    display: flex;
  }
  /* backdrop-filter forces the browser to re-sample everything behind these
     elements on every scrolled frame, which is one of the most expensive
     things a fixed overlay can do. Dropped on mobile in favour of a more
     opaque background — the visual difference is slight, the scrolling
     difference is not. */
  .fc-float,
  .jump-btn {
    backdrop-filter: none;
  }

  /* A real switch takes a moment to travel and settles at the end rather
     than stopping dead. 0.33s with a slight overshoot in the curve gives
     the knob that weight. The On/Off labels cross-fade a touch quicker so
     they read as following the knob rather than trailing it.
     !important because the base rules already set these transitions. */
  .fc-slider-knob {
    transition: transform 0.33s cubic-bezier(0.34, 1.15, 0.64, 1) !important;
  }
  .fc-slider-text {
    transition: opacity 0.22s ease !important;
  }
  .fc-slider {
    transition: background-color 0.33s ease, transform 0.08s ease !important;
  }
  .fc-float {
    background: rgba(255, 255, 255, 0.96);
    padding: 5px 10px 5px 12px;
    gap: 6px;
  }
  body.dark-mode .fc-float {
    background: rgba(62, 67, 76, 0.97);
  }
  .jump-btn {
    background: rgba(217, 184, 143, 0.92);
  }
  .fc-float-label {
    font-size: 11px;
  }
  .fc-float .fc-toggle {
    width: 62px;
    height: 24px;
  }
  .fc-float .fc-slider-knob {
    width: 18px;
    height: 18px;
    top: 3px;
    left: 3px;
  }
  .fc-float .fc-toggle input:checked + .fc-slider .fc-slider-knob {
    /* 62 − 18 − both 3px gutters = 38 */
    transform: translateX(38px);
  }
  .fc-float .fc-slider-text {
    font-size: 0.6em;
  }
  .fc-float .fc-off-text { right: 7px; }
  .fc-float .fc-on-text { left: 7px; }

  /* Anchored to the viewport rather than centred on the Pix button — the
     36-character key is wider than the popover, so centring pushed its left
     end off the edge of the screen. */
  .pix-code {
    position: fixed;
    top: 112px;
    left: 8px;
    right: 8px;
    transform: none;
  }

  /* Chapter titles at 80%. The Hide/Show button is px-sized and
     flex-shrink:0, so it stays identical across every chapter. */
  .chapter-audio-btn {
    margin-left: 6px;
  }
  .chapter-audio-btn svg {
    width: 13px;
    height: 13px;
  }
  h2 {
    font-size: 0.88em;
    margin-top: 28px;
  }
  .toggle-btn {
    font-size: 11px;
    padding: 5px 11px;
  }

  /* One size for headers, word cells and example cells alike — the example
     cells previously had their own 0.88em, rendering smaller than the rest. */
  th, td {
    padding: 7px 5px;
    font-size: 0.8em;
    text-align: center;
    vertical-align: middle;
  }
  /* Uppercase with tracking reads much larger than its point size, and at
     these column widths it was crowding out the labels. */
  th {
    font-size: 0.58em;
    letter-spacing: 0.03em;
    padding: 6px 3px;
  }
  .ex-line,
  .examples-list,
  .examples-header {
    text-align: center;
  }
  .examples-header {
    justify-content: center;
  }
  /* Column budget so No. / Level / Listen / Chinese / Pinyin together leave
     the English column visible without swiping. border-box makes these
     widths mean the whole cell, padding included. Example and Examples keep
     their min-width and simply extend past the edge, which is what the
     horizontal swipe is for. */
  /* table-layout: fixed is what makes the widths below mean something. With
     the default auto layout a width is only a hint and the browser resizes
     columns to fit their content, which is why the distribution kept coming
     out uneven no matter what numbers were set. Fixed layout honours them
     exactly and wraps the content instead.
     The explicit table width is the sum of all eight columns; the last two
     extend past the screen into the swipeable area. */
  .chapter-wrapper table {
    table-layout: fixed;
    /* The six visible columns fill exactly the content width (100vw minus
       12px page padding each side), so the English column's right edge
       lands flush with the grammar box above it. Default width covers a
       six-column table. */
    width: calc(100vw - 24px);
  }
  /* Tables that also carry Example and Examples extend a further 551px past
     the screen edge, into the swipeable area. data-cols is set by
     fixTableHeaders() — without this split, a six-column table would have
     its columns stretched to fill space reserved for columns it lacks. */
  .chapter-wrapper table[data-cols="8"] {
    width: calc(100vw + 527px);
  }
  /* Flashcard on: only five columns remain, so the table is pulled back to
     the content width and those five share it out. Everything on screen,
     right edge flush with the grammar box, nothing to swipe. */
  .chapter-wrapper table.fc-on {
    width: calc(100vw - 24px);
    margin-left: 0;
    margin-right: 0;
  }
  /* A row opened with Show brings English, Example and Examples back for
     that table, so it returns to full width — English ends at the screen
     edge with its divider visible, and the rest is a swipe away. */
  .chapter-wrapper table.fc-on.row-forced-open[data-cols="8"] {
    width: calc(100vw + 527px);
  }
  th, td {
    box-sizing: border-box;
  }
  /* The six visible columns are budgeted to 349px, inside the 351px of
     usable width on a 375px screen, so the English column is fully on
     screen with no swiping. Shared out as evenly as the content allows:
     the four short columns take a comparable slice each, and the two that
     carry words take the remainder.

     Note that table layout is auto, so a width is only a hint — a column
     can never render narrower than its own content. That is why the row
     number gets a smaller font: four digits were themselves ~32px wide, so
     no width below that could take effect. */
  /* Chinese, Pinyin and English keep fixed widths — 253px together. The
     three narrow columns then split whatever is left of the content width,
     so they grow on wider phones instead of leaving a gap on the right and
     the English column always ends flush with the grammar box.
     277px = 24px of page padding + those 253px. */
  th:nth-child(1), td:nth-child(1) {
    width: calc((100vw - 277px) * 0.30);
    font-size: 0.62em;
    padding-left: 1px;
    padding-right: 1px;
  }
  th:nth-child(2), td:nth-child(2) {
    width: calc((100vw - 277px) * 0.35);
    padding-left: 0;
    padding-right: 0;
  }
  th:nth-child(3), td:nth-child(3) {
    width: calc((100vw - 277px) * 0.35);
    padding-left: 0;
    padding-right: 0;
    text-align: center;
  }
  th:nth-child(4), td:nth-child(4) {
    width: 60px;
    text-align: center;
  }
  th:nth-child(5), td:nth-child(5) {
    width: 66px;
  }
  th:nth-child(6), td:nth-child(6) {
    width: 127px;
  }
  td:nth-child(6) {
    white-space: normal;
    word-break: break-word;
  }
  /* The two example columns take the remaining 551px of the 900px table */
  th:nth-child(7), td:nth-child(7) {
    width: 265px;
  }
  th:nth-child(8), td:nth-child(8) {
    width: 286px;
  }
  /* Under fixed layout an unwrappable line would spill into the next column
     rather than widening its own, so the English half of each example is
     allowed to wrap. Chinese and pinyin are short enough to stay on one. */
  .ex-en {
    white-space: normal;
  }
  /* nowrap means these can never break a line whatever the size — the cell
     simply widens into the swipeable area. So the sizes are set for
     legibility instead: Chinese needs more height per glyph than Latin
     text, pinyin and English sit a step smaller and match each other. */
  .ex-hanzi {
    font-size: 15px;
  }
  .ex-pinyin,
  .ex-en,
  .ex-sub {
    font-size: 12px;
  }
  .ex-hanzi,
  .ex-pinyin,
  .ex-en,
  .ex-sub {
    white-space: nowrap;
  }
  /* Sized to fit the narrower Level column on one line */
  .level-tag {
    font-size: 0.55em;
    padding: 1px 1px;
  }
  /* 10% down from the base 1.4em */
  td.char {
    text-align: center;
    font-size: 1.26em;
  }
  td.pinyin {
    white-space: nowrap;
  }
  .row-toggle {
    white-space: nowrap;
    margin-left: 4px;
  }
  /* The per-row Show/Hide pair stays available on mobile in both modes, so
     every word can be covered individually straight from its pinyin cell
     without switching the whole table into flashcard mode first. It was
     previously suppressed outside flashcard mode because it pushed English
     off screen; the button is now compact enough (see below) that it fits. */
  .row-toggle {
    display: inline-flex;
  }
  /* Narrower than the desktop pill: the mobile row has far less width to
     give away, and the label is short enough to stay legible. */
  .row-show-btn, .row-hide-btn {
    font-size: 0.62em;
    padding: 1px 5px;
    border-radius: 8px;
  }
  .example-cell, .examples-cell {
    min-width: 150px;
    font-size: 1em;
  }
  .grammar-tip {
    padding: 12px;
    font-size: 0.85em;
  }
}

@media (max-width: 480px) {
  h1 {
    font-size: 1.5em;
  }
  .flag-bar {
    padding: 4px 5px;
    gap: 2px;
  }
  th, td {
    padding: 6px 4px;
  }
}


/* ==========================================================================
   Added by build.py. Replaces markup that used to repeat on every row.
   Visually identical to the original.
   ========================================================================== */

/* Speaker icon drawn by CSS instead of an <svg> inside all 36,113 buttons.
   mask-image keeps it a solid shape we can colour, exactly as fill did. */
:root { --ico-audio: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M3%209v6h4l5%205V4L7%209H3zm13.5%203c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73%202.5-2.25%202.5-4.02zM14%203.23v2.06c2.89.86%205%203.54%205%206.71s-2.11%205.85-5%206.71v2.06c4.01-.91%207-4.49%207-8.77s-2.99-7.86-7-8.77z%22%2F%3E%3C%2Fsvg%3E"); }

/* :empty is doing real work here. The Grammar Tips buttons keep their inline
   <svg> — their spoken text is not recoverable from the DOM — so drawing a
   second icon behind them showed two speakers side by side. Only the buttons
   this script actually emptied get the CSS icon. */
.audio-btn:empty::before,
.audio-btn-sm:empty::before {
  content: "";
  display: block;
  background-color: #e74c3c;
  -webkit-mask: var(--ico-audio) center / contain no-repeat;
          mask: var(--ico-audio) center / contain no-repeat;
}
.audio-btn:empty::before    { width: 20px; height: 20px; }
.audio-btn-sm:empty::before { width: 12px; height: 12px; }

@media (max-width: 768px) {
  .audio-btn:empty::before    { width: 16px; height: 16px; }
  .audio-btn-sm:empty::before { width: 10px; height: 10px; }
}

/* Which examples are visible is now one attribute on the list rather than an
   inline style on each of the five lines. */
.examples-list > .ex-line { display: none; }
.examples-list[data-shown="1"] > .ex-line:nth-child(-n+1),
.examples-list[data-shown="2"] > .ex-line:nth-child(-n+2),
.examples-list[data-shown="3"] > .ex-line:nth-child(-n+3),
.examples-list[data-shown="4"] > .ex-line:nth-child(-n+4),
.examples-list[data-shown="5"] > .ex-line:nth-child(-n+5) { display: block; }

/* Shown while a level is being fetched. Deliberately quiet — the rows arrive
   in well under a second on any normal connection, and a spinner that flashes
   is worse than no spinner. */
body.levels-loading #vocabSearch { background-image: none; }
body.levels-loading .level-btn { cursor: progress; }

/* FAQ pill. Everything structural is inherited from .info-btn / .info-panel,
   which About and Study tips already use — only the lamp needs a colour. Amber
   reads as a lightbulb without competing with the red used for identity. */
.info-btn.faq-btn .info-icon { fill: #c9821a; }

/* The FAQ question sits on its own line above its answer. Done by making the
   <strong> a block rather than by adding a <br>, so the markup stays the same
   shape as Study tips and there is no stray element to style around. */
.faq-wrap .info-panel li strong {
  display: block;
  margin-bottom: 3px;
}
.faq-wrap .info-panel li { margin-bottom: 9px; }
.faq-wrap .info-panel li:last-child { margin-bottom: 0; }

/* The per-level breakdown inside the first answer. A nested list, so the
   bullets sit in from the answer text and the whole block still belongs to
   its question. Tighter than the outer list — these are one line each. */
.faq-wrap .info-panel .faq-lead { display: block; margin-bottom: 4px; }
.faq-wrap .info-panel ul.faq-sub {
  margin: 0;
  padding-left: 15px;
  list-style: disc;
}
.faq-wrap .info-panel ul.faq-sub li { margin-bottom: 1px; }

/* ---------------------------------------------------------------------------
   Open panels: one gold field with a dark blue edge, identical in all three
   themes. The panel is a deliberate constant — the same object wherever you
   meet it — rather than something that restyles itself with the page.

   Every theme is named in each selector on purpose. The originals set these
   under body.dark-mode and body.sepia-mode, which outrank a bare .info-panel;
   matching that specificity is what stops the old greys reappearing when the
   theme changes.

   Text colours are set explicitly rather than inherited. The panels used to
   sit on near-white and near-black, and the colours chosen for those do not
   have the contrast to work on gold.
   --------------------------------------------------------------------------- */
/* Slightly transparent, so the page reads through without the text losing
   contrast — 0.94 is enough to feel like a layer above the page rather than a
   hole cut into it. The blur is what keeps it legible: at this alpha, body text
   or a table rule passing behind the panel would otherwise show through and
   fight the reading. Browsers without backdrop-filter simply get the tint,
   which is still perfectly readable. */
.info-panel,
body.dark-mode .info-panel,
body.sepia-mode .info-panel {
  background: rgba(227, 203, 140, 0.94);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
  border: 2px solid #1d3557;
  color: #2b2718;
  box-shadow: 0 3px 14px rgba(45, 36, 14, 0.22);
}

.info-panel strong,
body.dark-mode .info-panel strong,
body.sepia-mode .info-panel strong { color: #1c1809; }

.info-panel a,
.info-panel .info-link,
body.dark-mode .info-panel a,
body.dark-mode .info-panel .info-link,
body.sepia-mode .info-panel a,
body.sepia-mode .info-panel .info-link {
  color: #1d4f80;
  text-decoration: underline;
}

.info-panel a:hover,
body.dark-mode .info-panel a:hover,
body.sepia-mode .info-panel a:hover { color: #123659; }

/* The two "available soon" notes read as blue rather than as another shade of
   the gold field — they announce something that does not exist yet, so they
   should not look like the surrounding copy. Same blue family as the border,
   darkened enough to stay legible on the deeper gold. */
.info-panel .info-soon,
body.dark-mode .info-panel .info-soon,
body.sepia-mode .info-panel .info-soon { color: #143a68; }

/* ---------------------------------------------------------------------------
   Mobile: place the FAQ pill, and centre the panels.

   On mobile .info-stack becomes display:contents and its children are placed
   absolutely in two rows — Donate / theme / flags at +14, then Study tips and
   About at +44. The FAQ wrap was added later and had no rule at all, so it fell
   out of that scheme into normal flow far down the page. It belongs in the
   second row beside Study tips, left-aligned under the theme toggle.
   --------------------------------------------------------------------------- */
/* Hidden by default, shown in the mobile block below.
   This declaration MUST come before that block. A media query adds nothing to
   specificity, so a plain `.info-close { display: none }` written afterwards
   beats the `display: flex` inside the query and the button disappears at every
   width — which is exactly what happened. Order is the only thing separating
   them. */
.info-close { display: none; }

/* ---- per-word anchors ----------------------------------------------------
   Arriving at /hsk-3/#w-yijing should make it obvious which row was meant, or
   the jump just looks like the page loaded at a strange scroll position.
   A left bar and a wash rather than a background swap, so the row still reads
   as part of its table and the pinyin and hanzi colours are untouched. */
tr[id^="w-"]:target > td {
  background: rgba(29, 53, 87, 0.07);
}
tr[id^="w-"]:target > td:first-child {
  box-shadow: inset 3px 0 0 #1d3557;
}
body.dark-mode tr[id^="w-"]:target > td {
  background: rgba(191, 87, 0, 0.16);
}
body.dark-mode tr[id^="w-"]:target > td:first-child {
  box-shadow: inset 3px 0 0 #bf5700;
}
body.sepia-mode tr[id^="w-"]:target > td {
  background: rgba(156, 92, 56, 0.12);
}
body.sepia-mode tr[id^="w-"]:target > td:first-child {
  box-shadow: inset 3px 0 0 #9c5c38;
}

/* The link itself lives in the No. cell. Held at low contrast until the row is
   hovered or it takes focus, so 5,159 of them do not turn the first column into
   a wall of symbols. Focus is included deliberately: keyboard users cannot
   hover, and an invisible focus target is a trap. */
.word-link {
  text-decoration: none;
  color: inherit;
  opacity: 0;
  margin-left: 4px;
  font-weight: 700;
  transition: opacity 0.12s;
}
tr:hover .word-link,
.word-link:focus {
  opacity: 0.55;
}
.word-link:hover { opacity: 1; }

/* Nothing to hover on a touch screen, so they stay visible but faint. */
@media (hover: none) {
  .word-link { opacity: 0.35; }
}

/* The six level filters are anchors now, for crawlers. Every .level-btn rule in
   the original sheet is class-based, so the styling carries over untouched —
   these three lines only undo what a browser adds to a link by default, so the
   filter bar looks exactly as it did when they were <button> elements. */
a.level-btn {
  text-decoration: none;
  color: inherit;
  /* Buttons never inherit the text cursor; anchors do not either, but being
     explicit keeps the two states identical if the base rule ever changes. */
  cursor: pointer;
}

/* The startupbar widget must not alter the page. Belt to the braces of shipping
   the loader inert: if it is ever switched to executing, whatever it injects
   stays invisible without another change here.

   Matched on substrings because the loader's markup is not published and can
   change under us; the `i` flag covers a capitalised class name. Costs nothing
   when the loader never runs — these selectors simply match no elements. */
[id*="startupbar" i],
[class*="startupbar" i],
[data-startupbar],
iframe[src*="startupbar.co"],
a[href*="startupbar.co"] { display: none !important; }

@media (max-width: 768px) {
  .faq-wrap {
    position: absolute;
    top: calc(var(--panel-h, 96px) + 44px);
    /* .theme-bar is also at left:104px, so the two rounded boxes already lined
       up — but the bar is a container: its 1px border plus 6px padding put the
       first toggle icon at 111px, and that icon is what reads as the left edge
       of the control. Matching the icon rather than the box is the alignment
       that is actually visible. Derived from .theme-bar, so if its padding
       changes this needs the same arithmetic again. */
    left: 111px;
    z-index: 1500;            /* same as its two siblings */
  }

  /* The row's Show/Hide control drops onto its own line under the pinyin
     instead of trailing it, and centres in the cell.

     Only one of the two buttons is displayed at a time — "Hide" in normal
     reading, "Show" once flashcard mode blanks the row — so centring the
     container centres whichever one is currently visible, and the column does
     not jump sideways when the mode changes.

     margin-left is zeroed deliberately: the 6px (4px on mobile) that separated
     the buttons from the pinyin made sense inline, but on a centred line it
     just pushes the pair off-centre by that much.

     Selected as td.pinyin .row-toggle rather than plain .row-toggle so it wins
     on specificity instead of on source order — there are five .row-toggle
     rules in the original sheet, two of them inside media queries. */
  td.pinyin .row-toggle {
    display: flex;
    justify-content: center;
    margin-left: 0;
    margin-top: 3px;
  }

  /* Centred across the screen, but hanging one line below the pill that opened
     it rather than sitting in the middle of the viewport. Dead-centre lost the
     connection between the tap and what appeared — the panel could cover the
     very button you had just pressed. Anchoring the top to the pill keeps that
     link while centring still avoids the screen edges a pill-relative panel
     runs into.

     Only the vertical offset needs measuring, because it depends on which pill
     was clicked and the pills move as the header wraps; placePanel() sets
     --panel-top. Horizontal centring stays in CSS. The 50% fallback keeps the
     panel on screen if that JS never runs.

     position:fixed is against the viewport, not #pageScroll, so an open panel
     stays put while the page scrolls underneath — and it means placePanel can
     use getBoundingClientRect() values directly, with no scroll correction. */
  .info-wrap .info-panel {
    position: fixed;
    top: var(--panel-top, 50%);
    left: 50%;
    /* X only: the Y offset is the measured value above, not a centring nudge. */
    transform: translateX(-50%);
    /* 25% up from the previous 62vw / 215px. */
    width: min(78vw, 269px);
    max-width: none;
    margin: 0;
    z-index: 3000;
    padding: 14px 15px;
    /* Left at the smaller-box value on purpose: a wider box at the same text
       size shows more of the answer, which is the point. Ask separately if the
       type itself should grow. */
    font-size: 0.72em;
    /* Visible, not auto: the close button straddles this corner and a scroll
       container would clip the half that hangs outside. The scrolling moved to
       .info-panel-body, which JS wraps the content in. */
    overflow: visible;
  }

  .info-panel-body {
    /* Whatever room is left between the pill and the bottom of the screen.
       Anchoring to the pill means the space available is no longer a constant,
       so placePanel() measures it; 56vh is the fallback. */
    max-height: var(--panel-avail, 56vh);
    overflow-y: auto;
    /* Room so the last line never sits under the overhanging button. */
    padding-right: 2px;
  }

  /* 15% thicker than the 2px the three theme rules set.
     Every selector those rules use has to be repeated here. Two of them carry a
     body.<theme> prefix and so outrank a plain .info-wrap .info-panel, which
     would leave light mode at 2.3px and the other two themes at 2px — the same
     specificity trap that hid the close button. */
  .info-panel,
  body.dark-mode .info-panel,
  body.sepia-mode .info-panel { border-width: 2.3px; }

  /* Round, red, white X, mounted on the corner — half over the panel, half
     over the page. The ring is the panel's own border colour so it reads as
     fixed to the box rather than floating near it. Only shown on mobile: on
     desktop the pill that opened the panel is right beside it. */
  .info-close {
    display: flex;
    position: absolute;
    top: -11px;
    right: -11px;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: #e74c3c;
    color: #fff;
    /* The glyph nearly fills the circle. font-size alone leaves the × sitting
       high because of its own line box, so the line-height is pinned to the
       button height and the weight lifted to keep the strokes solid at 22px. */
    font-size: 21px;
    font-weight: 500;
    line-height: 22px;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 2;
  }
  .info-close:active { transform: scale(0.92); }
}

/* Level links in the footer. Quiet by design: they exist so the six level
   pages are reachable by a crawler, not to draw the eye. */
.footer-levels { font-size: .85em; opacity: .75; margin: 6px 0; }
.footer-levels a { color: inherit; text-decoration: none;
                   border-bottom: 1px dotted currentColor; }
.footer-levels a:hover { border-bottom-style: solid; }
