/* ---------- Font ---------- */
@font-face {
  font-family: 'Lexend';
  src: url('../fonts/Lexend-Variable.woff2') format('woff2-variations'),
       url('../fonts/Lexend-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}

/* ---------- Design tokens ---------- */
:root {
  --font-body: 'Lexend', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --bg: #faf8f3;
  --surface: #ffffff;
  --ink: #232323;
  --ink-soft: #55524c;
  --border: #93897a;

  /* Kept for the few UI accents still using them (active category
     button/mode banner, Copy's "copied" flash, the About/Credits
     placeholder note) after word-bank buttons stopped being
     color-coded by part of speech (2026-09, see .word-btn in the
     Word bank section below). */
  --grammatical: #4f46e5;
  --grammatical-bg: #dedaf9;
  --descriptive: #2563eb;
  --descriptive-bg: #cde0ff;
  --verb: #15803d;
  --verb-bg: #c5ecd5;

  /* Column header background: a warm neutral, deliberately not tied to
     any one category's color since it's shared by all four columns. */
  --header-bg: #f0ece0;

  /* Live binder-tab accent, repointed by app.js's renderCategoryBar to
     whichever category is active (default here matches its "common"
     color, so there's a sane value before JS runs). See .category-btn
     and .column. */
  --active-accent: #64748b;

  --focus-ring: #2563eb;

  --step: 1rem; /* base text-size step, adjusted by JS via --text-scale */
  --text-scale: 1;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--ink);
  font-size: calc(var(--step) * var(--text-scale));
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- Top bar ---------- */
/* The title, the writing box + its Copy/Undo, and the settings gear all
   share this one top row now (2026-09) -- a 3-column grid with two equal
   flexible outer tracks is what centers the middle (auto-sized) column
   on the page regardless of the title and gear being different widths;
   plain flex centering can't do that without also fixing the outer
   two's widths. */
.topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
  padding: 0.4rem 0.75rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex: 0 0 auto;
}

.app-name {
  justify-self: start;
  font-weight: 600;
  font-size: 1.05em;
}

button:focus-visible,
input:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ---------- App body (sidebar + main content) ---------- */
.app-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: row;
  min-height: 0;
}

/* ---------- Category bar ---------- */
/* Prototype: icon column for switching the word bank to a topic vocabulary
   (see js/predict.js's loadCategory/setActiveCategory). Only "Animals" is
   wired to real data right now -- the rest are visual placeholders
   (disabled) showing where future topics will sit. Runs down the left
   side of the page, full height, rather than a top row -- keeps the
   editor and word bank the full page width below the topbar. */
.category-bar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.4rem;
  /* Same top padding as .workspace below, deliberately -- that's what
     lines this bar's first tab (Common) up level with the top edge of
     the word bank's 4 columns, since both sit in the same row of
     .app-body with nothing else above either of them. */
  padding: 0.5rem;
  /* No border here (removed 2026-09) -- the boundary between this bar
     and the colored .workspace next to it now comes entirely from their
     background colors differing, which also lets the active tab erase
     that boundary at its own row (see .category-btn[aria-pressed] below)
     instead of a fixed line cutting across every tab the same way. */
  background: var(--surface);
  flex: 0 0 auto;
  width: 108px;
  overflow-y: auto;
}

/* Binder-tab metaphor: every tab keeps a left-edge stripe in its own
   category's color all the time (--tab-color, set once per button in
   app.js from CATEGORY_COLORS), like a real folder tab's color is always
   visible whether or not it's the one pulled forward. */
.category-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.5rem 0.4rem;
  border: 1px solid var(--border);
  border-left: 5px solid var(--tab-color, var(--border));
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.85em;
  width: 100%;
}
.category-btn:hover:not(:disabled):not([aria-pressed="true"]) { background: var(--bg); }
.category-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
/* The active tab fills solid with the same color as .workspace's whole
   background (both read --active-accent, repointed by app.js on every
   switch) -- and then physically bridges the gap into it: no right
   border, square right corners, and a negative right margin that cancels
   this bar's own right padding, so the tab's colored edge touches
   .workspace's colored edge with nothing between them. The two same-color
   fills read as one continuous shape, like the tab is a handle sticking
   out of the box rather than a separate control pointing at it. */
.category-btn[aria-pressed="true"] {
  position: relative;
  background: var(--active-accent);
  border-color: var(--active-accent);
  border-right: none;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  /* A plain `width:100%` button (see .category-btn above) is already a
     fixed 92px -- a negative margin alone doesn't push a fixed-width
     box's own right edge any further, it only changes the space left
     for whatever comes after it, so it had no visible effect here. This
     widens the box itself by exactly the bar's own right padding, so
     the right edge lands exactly on .category-bar's own boundary --
     right where .workspace's colored background begins. */
  width: calc(100% + 0.5rem);
  margin-right: -0.5rem;
  color: #ffffff;
  font-weight: 700;
}
.category-btn[aria-pressed="true"]::after {
  content: "✓";
  position: absolute;
  top: 2px;
  right: 12px;
  font-size: 0.85em;
  font-weight: 700;
  line-height: 1;
  color: #ffffff;
}
.category-btn.loading { opacity: 0.6; cursor: wait; }

/* ---------- Workspace layout ---------- */
.workspace {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  min-height: 0;
  /* Same top padding as .category-bar above -- see that rule's comment.
     The colored fill (see the comment on .category-btn[aria-pressed]
     above) covers this padding too, not just the columns inside it --
     that's the "whole box" the active tab is connected to, not just the
     4 column cards on their own. */
  padding: 0.5rem;
  background: var(--active-accent);
}

.editor-pane {
  /* Lives in the topbar's centered middle column now (2026-09) -- sized
     to its own content (the textarea's `rows="1"` plus the button
     column), not stretched to any particular width, so the grid's two
     equal outer tracks are what centers it on the page (see .topbar).
     align-items:center (not stretch) is what actually keeps the box one
     line tall -- stretch was silently overriding rows="1" by forcing
     #editor to match the taller stacked Copy/Undo column's height. */
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}

#editor {
  flex: 0 1 auto;
  width: 50ch;
  max-width: 100%;
  min-width: 0;
  resize: none;
  font-family: var(--font-body);
  font-size: calc(1.1em * var(--text-scale));
  line-height: 1.5;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink);
}

.editor-actions {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.3rem;
  flex: 0 0 auto;
  justify-content: center;
}

.copy-btn {
  flex: 0 0 auto;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1em;
  cursor: pointer;
  padding: 0.35rem 0.4rem;
}
.copy-btn:hover { background: var(--bg); }
.copy-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.copy-btn:disabled:hover { background: var(--surface); }
.copy-btn.copied { background: var(--verb-bg); border-color: var(--verb); }

/* Undo: deliberately not a Fitzgerald-Key category color, and not tied
   to the surface/border tokens either -- it isn't a word choice, it's a
   warning-flavored control, so it keeps this exact look regardless of
   theme (including high contrast). Dims via the shared :disabled rule
   above when there's nothing to undo. */
.copy-btn.undo-btn {
  background: #ffffff;
  border: 2px solid #b91c1c;
  color: #232323;
}
.copy-btn.undo-btn:hover:not(:disabled) { background: #fff5f5; }

/* Reserves the same width regardless of label, so toggling to "Copied!"
   never changes the button's width. */
#copyBtn { min-width: 10ch; }

/* ---------- Word bank ---------- */
.wordbank {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: row;
  gap: 0.5rem;
}

.column {
  flex: 1 1 25%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}

.column-title {
  margin: 0;
  padding: 0.4rem 0.4rem;
  font-size: 0.78em;
  font-weight: 600;
  text-align: center;
  color: var(--ink);
  background: var(--header-bg);
  flex: 0 0 auto;
  overflow-wrap: break-word;
}

.column-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* One neutral look for every word-bank button regardless of part of
   speech (2026-09) -- this used to be Fitzgerald-Key-style color-coded
   per grammatical subcategory (data-color attribute set by
   js/app.js's colorKeyFor); removed in favor of a single consistent
   background, so the button's color is no longer information the user
   has to read. */
.word-btn {
  position: relative;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
  min-height: 44px;
  padding: 0.5rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  font-family: var(--font-body);
  font-size: calc(0.95em * var(--text-scale));
  text-align: center;
  cursor: pointer;
  color: var(--ink);
}

/* Marks a root button as opening more forms, before there's any other
   way to tell -- a small chevron pointing at where those forms will
   appear (directly below). currentColor keys off the button's own ink
   color, so it stays legible in high-contrast mode without its own
   per-color rules. */
.word-btn .expand-hint {
  position: absolute;
  bottom: 3px;
  right: 5px;
  width: 12px;
  height: 12px;
  color: currentColor;
  opacity: 0.55;
  pointer-events: none;
}
.word-btn .expand-hint svg { width: 100%; height: 100%; display: block; }

.word-btn:hover { background: var(--bg); }

/* The "Show all animals"-style topic buttons (see renderTopicButtons in
   app.js): the same size and shape as a word button, in the topic tab's
   own color. */
.word-btn.topic-btn,
.word-btn.topic-btn:hover {
  background: var(--topic-color, var(--ink));
  border-color: var(--topic-color, var(--ink));
  color: #ffffff;
  font-weight: 600;
}
.word-btn.topic-btn:hover { filter: brightness(1.12); }
body.high-contrast .word-btn.topic-btn,
body.high-contrast .word-btn.topic-btn:hover {
  background: #000000;
  border-color: #ffffff;
  color: #ffffff;
}
.word-btn:active { filter: brightness(0.9); }

/* Picture symbol (ARASAAC) to the LEFT of the word. Always the same (largest)
   size, whatever the text-size setting is. Buttons without a picture are
   unchanged. */
.word-btn.has-picture {
  flex-direction: row;
  gap: 0.55rem;
}
.word-btn .word-icon {
  flex: 0 0 auto;
  width: 60px;
  height: 60px;
  display: block;
}
.word-btn .word-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* A blanked-out button (see .whiteout) hides its picture too. */
.word-btn.whiteout .word-icon { visibility: hidden; }

.word-btn .word-label {
  hyphens: auto;
  overflow-wrap: anywhere;
}

/* ---------- Grammar picker (root word + its s/ed/ing forms) ---------- */
.grouped-cluster {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

/* Every other word-bank button while a root's forms are revealed: same
   slot, same size, blanked out completely (matching the column's own
   background, no border) -- not removed, so nothing else reflows. */
.word-btn.whiteout {
  background: var(--surface);
  color: transparent;
  pointer-events: none;
  border: none;
}
/* The root and every one of its revealed forms: stays fully visible and
   tappable, ringed as a set so it reads as one offer of choices rather
   than plain word buttons scattered in the column. */
.word-btn.revealed {
  box-shadow: 0 0 0 2px var(--focus-ring) inset;
}

/* ---------- Settings panel ---------- */
.hidden { display: none !important; }

.settings-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.35);
  z-index: 10;
}

.settings-panel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 11;
  background: var(--surface);
  border-radius: 14px;
  padding: 1.25rem;
  width: min(90vw, 360px);
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}

.settings-panel h2 {
  margin-top: 0;
  font-size: 1.1em;
}

.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
}
.setting-row label { font-size: 0.95em; }
.setting-row input[type="checkbox"] {
  width: 24px;
  height: 24px;
}
.setting-row input[type="range"] {
  width: 140px;
  height: 44px;
}
.setting-row select {
  min-height: 44px;
  font-size: 0.95em;
  font-family: inherit;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  padding: 0 0.5rem;
  min-width: 0;
  max-width: 10.5rem;
}

.close-settings-btn {
  margin-top: 1rem;
  width: 100%;
  min-height: 44px;
  border: none;
  border-radius: 10px;
  background: var(--descriptive);
  color: #fff;
  font-family: var(--font-body);
  font-size: 1em;
  cursor: pointer;
}

/* ---------- Static content pages (About, Credits) ---------- */
.page {
  flex: 1 1 auto;
  max-width: 640px;
  width: 100%;
  margin: 0 auto;
  padding: 1.5rem 1.25rem;
  line-height: 1.6;
}
.page h1 { margin-top: 0; }
.page h2 { font-size: 1.05em; margin-top: 2rem; }
.page a { color: var(--descriptive); }
.placeholder-note {
  background: var(--grammatical-bg);
  border-left: 4px solid var(--grammatical);
  border-radius: 8px;
  padding: 0.75rem 1rem;
}
.credits-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.credits-list li {
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
}
.credits-list li:last-child { border-bottom: none; }

.license-text {
  margin: 0.75rem 0 1.5rem;
  font-size: 0.9em;
}
.license-text summary {
  cursor: pointer;
  color: var(--descriptive);
}
.license-text pre {
  white-space: pre-wrap;
  word-wrap: break-word;
  background: var(--grammatical-bg);
  border-left: 4px solid var(--grammatical);
  border-radius: 8px;
  padding: 0.75rem 1rem;
  margin-top: 0.5rem;
  font-size: 0.85em;
  line-height: 1.5;
}

/* Boxed off visually so a reader can tell at a glance this part switches
   audience -- technical/IT language, not the plain-language rest of the
   page (see the Privacy page's "Technical summary" section). Same boxed
   look as .license-text's <pre> above, but for ordinary prose instead of
   literal license text, and with a monospace-flavored font to read as
   "technical register" the way the rest of the page's body font doesn't. */
.tech-note {
  background: var(--grammatical-bg);
  border-left: 4px solid var(--grammatical);
  border-radius: 8px;
  padding: 0.75rem 1rem;
  margin: 0.5rem 0 1.5rem;
}
.tech-note h2 {
  margin: 0 0 0.75rem;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
}
.tech-note p {
  margin: 0;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 0.8em;
  line-height: 1.6;
}

.credits-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 10px;
  margin: 1rem 0 2rem;
}
.credits-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 520px;
  font-size: 0.85em;
}
.credits-table th,
.credits-table td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.credits-table tbody tr:last-child td { border-bottom: none; }
/* Images are pulled from the site for now -- drop this rule to bring the
   Icon column back (see also SHOW_ICONS in js/app.js). */
.credits-table th:first-child,
.credits-table td:first-child {
  display: none;
}
.credits-thumb {
  width: 28px;
  height: 28px;
  object-fit: contain;
  display: block;
}
.credits-pos {
  color: var(--ink-soft);
  font-size: 0.9em;
}

/* ---------- Footer ---------- */
.site-footer {
  flex: 0 0 auto;
  display: flex;
  gap: 1rem;
  justify-content: center;
  padding: 0.35rem;
  font-size: 0.8em;
}
.site-footer a { color: var(--ink-soft); }

/* ---------- High contrast mode ---------- */
body.high-contrast {
  --bg: #000000;
  --surface: #000000;
  --ink: #ffffff;
  --ink-soft: #ffffff;
  --border: #ffffff;

  --grammatical: #ffffff;
  --grammatical-bg: #1a1a1a;
  --descriptive: #7db8ff;
  --descriptive-bg: #1a1a1a;
  --verb: #6bdc9c;
  --verb-bg: #1a1a1a;

  --focus-ring: #ffff00;
}
body.high-contrast .column-title { color: #000; background: #fff !important; }
/* The per-category binder-tab colors (--tab-color/--active-accent) are
   tuned for a light background and don't reliably hold contrast against
   the high-contrast theme's forced black -- flattened to plain white/
   black here instead of reused as-is, same reasoning as the rest of this
   theme's overrides. */
body.high-contrast .category-btn { border-left-color: #ffffff; }
body.high-contrast .category-btn[aria-pressed="true"] {
  background: #ffffff;
  border-color: #ffffff;
  color: #000000;
}
body.high-contrast .category-btn[aria-pressed="true"]::after { color: #000000; }
body.high-contrast .workspace { background: #ffffff; }
/* The pictures are black line art on a transparent background, which would
   vanish on the black high-contrast theme -- give them a white backing. */
body.high-contrast .word-btn .word-icon { background: #ffffff; border-radius: 6px; }
body.high-contrast .close-settings-btn {
  background: #fff;
  color: #000;
  border: 2px solid #fff;
}

/* Phone-only links block inside the Settings panel -- the footer's About/
   Credits/Privacy move here on phones (see the media query below). */
.settings-links { display: none; }

/* ---------- Responsive: phones in portrait ---------- */
/* Everything here is scoped to html.app-page (index.html only) -- the static
   About/Credits/Privacy pages share this stylesheet and must keep scrolling
   normally. Layout, top to bottom: title (hidden while the on-screen
   keyboard is up), a one-line full-width writing box, Copy/Undo/Settings as
   three full-width buttons, one swipeable row of topic tabs, then the word
   bank -- the four syllable columns, ~2.2 visible at a time, swiped
   sideways. The page itself never scrolls: body is pinned to the visual
   viewport (see syncViewport in js/app.js, which sets --vvh/--vvt and the
   kbd-up class), so the word area always fills exactly what's left above
   the keyboard. */
@media (max-width: 700px) {
  html.app-page { overflow: hidden; }
  html.app-page body {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--vvh, 100dvh);
    min-height: 0;
    overflow: hidden;
    transform: translateY(var(--vvt, 0px));
  }

  .app-page .topbar {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0 12px;
    border-bottom: none;
  }
  .app-page .app-name {
    display: flex;
    align-items: center;
    height: 34px;
    padding: 0 2px;
  }
  .app-page.kbd-up .app-name { display: none; }

  .app-page .editor-pane {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 6px 0 0;
  }
  .app-page.kbd-up .editor-pane { padding-top: 10px; }
  .app-page #editor {
    flex: 0 0 auto;
    width: 100%;
    /* One line, no wrapping: it shows the end of what's been typed, and a
       sideways swipe scrolls back through the rest (scrollbar hidden --
       it would eat into the one line's height). */
    white-space: pre;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .app-page #editor::-webkit-scrollbar { display: none; }
  .app-page .editor-actions {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    padding-bottom: 0;
  }
  .app-page .copy-btn { min-width: 0; padding: 0.35rem 0.25rem; }
  .app-page #copyBtn { min-width: 0; }

  .app-page .app-body { flex-direction: column; }

  /* One swipeable row of tabs. The active one drops its bottom border and
     takes the tray's color, so it reads as joined to the word area right
     below it (same binder-tab idea as the desktop side rail, turned
     sideways). */
  .app-page .category-bar {
    flex-direction: row;
    align-items: flex-end;
    flex-wrap: nowrap;
    gap: 6px;
    width: auto;
    padding: 10px 12px 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .app-page .category-bar::-webkit-scrollbar { display: none; }
  .app-page .category-btn {
    flex: 0 0 auto;
    flex-direction: row;
    width: auto;
    height: 44px;
    padding: 0 14px;
    font-size: 0.95em;
    white-space: nowrap;
    border-bottom: none;
    border-radius: 10px 10px 0 0;
  }
  .app-page .category-btn[aria-pressed="true"] {
    width: auto;
    margin: 0;
    padding: 0 14px;
    border-right: 1px solid var(--active-accent);
    border-bottom: none;
    border-radius: 10px 10px 0 0;
  }
  .app-page .category-btn[aria-pressed="true"]::after { display: none; }

  .app-page .workspace {
    padding: 10px 0 0;
    min-height: 0;
    overflow: hidden;
  }

  .app-page .wordbank {
    gap: 8px;
    padding: 0 12px 12px;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-padding-left: 12px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .app-page .wordbank::-webkit-scrollbar { display: none; }
  /* Scroll containers drop their end padding in some browsers; a spacer
     item keeps the last column off the edge. */
  .app-page .wordbank::after { content: ''; flex: 0 0 4px; }

  .app-page .column {
    flex: 0 0 calc((100% - 16px) / 2.2);
    scroll-snap-align: start;
  }
  .app-page .column-title {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0 4px;
    font-weight: 700;
    cursor: pointer;
  }
  .app-page .column-list {
    position: relative;
    /* Keep the tapped word still when its forms are added below it. */
    overflow-anchor: none;
    max-height: none;
    padding: 8px;
    gap: 8px;
  }
  .app-page .grouped-cluster { gap: 8px; }

  /* Whole words, never hyphenated: a long word shrinks (fitWords in
     js/app.js, down to 12px) instead. */
  .app-page .word-btn {
    font-size: calc(17px * var(--text-scale));
    min-height: 44px;
    padding: 0 8px;
    flex-direction: row;
    white-space: nowrap;
    hyphens: none;
  }
  .app-page .word-btn .word-label {
    hyphens: none;
    overflow-wrap: normal;
  }
  /* Room beside the label for the chevron badge, so a long word never runs
     underneath it. */
  .app-page .word-btn.has-hint { padding: 0 13px; }
  /* Picture to the left of the word: a little smaller than on wide screens,
     and the button a touch taller so the picture isn't cramped. */
  .app-page .word-btn.has-picture { padding-top: 4px; padding-bottom: 4px; gap: 6px; min-height: 52px; }
  /* Also a fixed size on phones, independent of the text-size setting -- as large as the ~150px-wide
     columns allow without squeezing the word itself down to tiny type. */
  .app-page .word-btn .word-icon { width: 44px; height: 44px; }
  /* Last resort for a word that doesn't fit even at the smallest size. */
  .app-page .word-btn.wrap { white-space: normal; padding: 6px 8px; }
  .app-page .word-btn.wrap .word-label { overflow-wrap: anywhere; }

  .app-page .site-footer { display: none; }

  .app-page .settings-panel {
    max-height: calc(100% - 24px);
    overflow-y: auto;
  }
  .app-page .settings-links {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    margin: 12px 0 4px;
  }
  .app-page .settings-links a {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--bg);
    color: var(--ink);
    text-decoration: underline;
    font-size: 0.95em;
  }
}

/* ---------- Responsive: tablets with the on-screen keyboard up ---------- */
/* An iPad in landscape has the full desktop layout until its keyboard opens,
   which then covers more than half the screen. While html.kbd-up is set (see
   syncViewport in js/app.js; it only ever sets it on touch screens), the page
   is pinned to the space above the keyboard and the chrome is trimmed so more
   words fit: no title or footer, thinner top bar, tabs and column headers,
   tighter spacing, and a slightly smaller picture beside each word. */
@media (min-width: 701px) {
  html.app-page.kbd-up { overflow: hidden; }
  html.app-page.kbd-up body {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--vvh, 100dvh);
    min-height: 0;
    overflow: hidden;
    transform: translateY(var(--vvt, 0px));
  }

  .app-page.kbd-up .app-name,
  .app-page.kbd-up .site-footer { display: none; }

  .app-page.kbd-up .topbar {
    grid-template-columns: 1fr;
    justify-items: center;
    padding: 4px 12px;
  }
  .app-page.kbd-up #editor {
    padding: 0.25rem 0.75rem;
    line-height: 1.3;
  }
  .app-page.kbd-up .copy-btn { min-height: 40px; }

  .app-page.kbd-up .category-bar { width: 96px; padding: 0.35rem; gap: 0.3rem; }
  .app-page.kbd-up .category-btn { padding: 0.35rem 0.3rem; }
  .app-page.kbd-up .category-btn[aria-pressed="true"] {
    width: calc(100% + 0.35rem);
    margin-right: -0.35rem;
  }
  /* The checkmark would sit on top of the (now narrower) label; the filled
     tab already shows which topic is active. */
  .app-page.kbd-up .category-btn[aria-pressed="true"]::after { display: none; }

  .app-page.kbd-up .workspace { padding: 0.35rem; }
  .app-page.kbd-up .wordbank { gap: 0.35rem; }
  .app-page.kbd-up .column-title { padding: 0.2rem 0.3rem; font-size: 0.74em; }
  .app-page.kbd-up .column-list {
    padding: 0.25rem;
    gap: 0.25rem;
    /* Keep the tapped word still when its forms are added below it. */
    overflow-anchor: none;
  }
  .app-page.kbd-up .grouped-cluster { gap: 0.25rem; }

  .app-page.kbd-up .word-btn { min-height: 40px; padding: 0.2rem 0.4rem; }
  .app-page.kbd-up .word-btn.has-picture { min-height: 48px; gap: 0.45rem; }
  .app-page.kbd-up .word-btn .word-icon { width: 40px; height: 40px; }
}
