/* ═══════════════════════════════════════════
   FONTS — M PLUS Rounded 1c (OFL)
   Loaded via Google Fonts CDN (wght 400/500/700/800).
   No 600 weight in this family — use 500 for semibold roles.
   ═══════════════════════════════════════════ */

/* Material Symbols Rounded — UI icon helper */
.ms {
  font-family: 'Material Symbols Rounded';
  font-weight: normal;
  font-style: normal;
  font-size: 20px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  -webkit-font-smoothing: antialiased;
  user-select: none;
}

/* ═══════════════════════════════════════════
   RESET & VARIABLES
   ═══════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg: #f2f1ef;
  --surface: #ffffff;
  --surface2: #f9f9f7;
  --border: rgba(0,0,0,0.07);
  --text-primary: #1a1a1a;
  --text-secondary: #666;
  --text-tertiary: #999;
  /* READING TYPE SCALE — #79. Three steps, not a slider: 1.00 / 1.15 / 1.32.
     Set on documentElement by applyTextSize(); declared here so the default is
     correct before any JavaScript runs and a visitor never sees a reflow.
     It scales the surfaces a person READS — the modal's title, description,
     tags, file strip and Details panel — and deliberately NOT the app's chrome:
     nav, breadcrumbs and buttons are furniture, and growing them costs the
     reading area the space it was the point of the setting to give it. */
  --text-scale: 1;
  --nav-w: 252px;
  --radius-card: 20px;
  --shadow-card: 0 2px 16px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04);
  --shadow-card-hover: 0 10px 40px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06);
  --transition: 0.13s ease-out;

  /* ── Grid card background and size ────────────────────────────────────────
     Every illustration is a transparent PNG, so what sits behind it is a
     choice rather than a given. Both controls write ONLY these variables on
     :root, which is what makes them free at 1,960 cards and correct across
     infinite scroll — no per-card DOM work, and cards appended later inherit
     the current setting without being told about it.

     Card size is a THREE-WAY CHOICE, not a zoom stepper: there is no counter
     to accumulate, so re-pressing an option repaints the same values. The
     label scales GENTLER than the card (see --label-scale) because 0.75 × 12px
     is 9px, which is unreadable Japanese — and Guidelines/19 §3 makes the
     Qualifier part of the title rather than metadata about it, so the second
     label line is not the thing that gets sacrificed at small size. */
  --thumb-bg: #f5f4f2;
  --thumb-bg-image: none;
  --thumb-bg-size: auto;
  --thumb-bg-position: 0 0;
  --card-scale: 1;
  --label-scale: 1;
  /* Two breakpoints below pin an exact column COUNT rather than a minimum
     width (5 and 3), and a minmax() multiplier cannot reach them — the count
     is what scales there instead. Without these the control would silently do
     nothing on iPad and phone. */
  --card-cols-5: 5;
  --card-cols-3: 3;
  --font: "M PLUS Rounded 1c", "Hiragino Sans", "Yu Gothic UI", "Noto Sans JP", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
  --hub: #7c5cbf;
  --hub-light: #f3f0fb;
  /* The app's interactive colour: links, hover and active states, the loading
     spinner, the active-filter dot. Referenced in twelve rules since the app
     was written and never defined until 2026-07-29, so all twelve were silently
     inert — `var(--accent)` with no fallback resolves to the initial value, so
     `background` went transparent and `color` just inherited.

     School & Learning blue, which is already this app's interactive colour in
     two places: the :focus-visible ring is the same hue, and
     .top-bar-link--web is this exact value — so the info-panel links now match
     the top-bar links instead of quietly differing. 5.29:1 on --surface and
     4.68:1 on --bg, both AA. Deliberately not the Sports red, which already
     means "Clear" on the colour bar and "external link" in the modal. */
  --accent: hsl(205, 62%, 40%);
  /* Two more names that were referenced and never defined, found by the same
     sweep as --accent. Aliases rather than new colours: --surface2 is already
     this app's subtle raised surface, used by .search-btn:hover,
     .section-picker-item:hover and .color-bar-clear:hover. --hover backs the
     hamburger's hover state and the tinted panels on the How to Search and
     Visit Irasutoya pages, which had been rendering with no background at all;
     --bg-secondary backs one more of those panels. */
  --hover: var(--surface2);
  --bg-secondary: var(--surface2);
  /* Red button — the exact recipe .modal-action-link uses for the Irasutoya
     link in the detail modal, promoted to tokens so the two cannot drift.
     Sports & Recreation / Red (h:5 s:65) at the 60% tint, with that family's
     `text` value on top, per Guidelines/15-Irasutoya-Color-System-v1.md.
     The modal's own rules still carry the literals; see the note there. */
  --btn-red:        hsl(5, 65%, 92%);
  --btn-red-hover:  hsl(5, 65%, 86%);
  --btn-red-border: hsl(5, 58%, 83%);
  --btn-red-text:   hsl(5, 70%, 27%);
}

body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text-primary);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

/* ── Keyboard focus (a11y) — visible ring for Tab users only ── */
:focus-visible {
  outline: 3px solid hsl(205, 62%, 55%);
  outline-offset: 2px;
}
.ill-card:focus-visible, .big-card:focus-visible, .subcat-card:focus-visible {
  outline-offset: 0;
}
/* Dialog containers receive programmatic focus — no ring needed */
.modal-scene:focus, .info-panel:focus { outline: none; }

/* ═══════════════════════════════════════════
   NAV LOGO
   ═══════════════════════════════════════════ */
.nav-logo-wrap {
  padding: 22px 16px 16px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}
.nav-logo {
  width: 80%;
  height: auto;
  display: block;
}
.nav-logo-subtitle {
  font-size: 11px;
  font-weight: 500;
  color: var(--text-tertiary);
  text-align: center;
  letter-spacing: 0.1px;
  line-height: 1.4;
}
/* Sits directly under the subtitle, pulled up out of the wrapper's 8px gap so
   the two read as one block. Deliberately NOT faded: this is the one thing a
   first-time visitor should register, so it matches the subtitle's size and
   sits a step darker and heavier than it. */
.nav-logo-tagline {
  margin-top: -5px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  text-align: center;
  letter-spacing: 0.2px;
  line-height: 1.35;
}

/* ── Top bar ── */
.top-bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(242, 241, 239, 0.92);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-bottom: 1px solid var(--border);
  padding: 0 14px;
  height: 48px;
  display: grid;
  /* Was `1fr auto 1fr` with the wordmark in the middle cell. With it gone the
     bar is two cells: links take the free space, controls sit hard right. */
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 8px;
}
.top-bar-left {
  display: flex;
  align-items: center;
  gap: 4px;
}
.top-bar-right {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: flex-end;
  flex-shrink: 0;
}

/* ── Language picker ── */
.lang-flag-btn {
  width: 36px;
  height: 36px;
  border: none;
  background: none;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s, opacity 0.15s;
  padding: 0;
}
.lang-flag-btn:hover { transform: scale(1.08); }
.lang-flag-btn:active { transform: scale(0.96); }
/* The flags are Irasutoya's own illustrations rather than emoji, so they sit on
   the page like everything else here — no chrome around them. */
.lang-flag-img {
  width: 30px;
  height: 30px;
  object-fit: contain;
  display: block;
  pointer-events: none;
}

/* ── Search icon button (mobile + iPad only) ── */
.search-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 50%;
  width: 36px; height: 36px;
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--font);
  transition: background 0.15s;
  padding: 0;
}
.search-btn:hover { background: var(--surface2); }
.search-btn .ms { font-size: 20px; line-height: 1; }
/* Desktop: hide icon button, show input form instead */
@media (min-width: 1200px) { .search-btn { display: none; } }

/* ── Desktop search input (≥1200px) ── */
.search-bar-desktop {
  display: none;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  padding: 9px 12px;
  cursor: text;
  transition: border-color 0.15s, box-shadow 0.15s;
}
@media (min-width: 1200px) { .search-bar-desktop { display: flex; } }
.search-bar-desktop:focus-within {
  border-color: #aaa;
  box-shadow: 0 0 0 2px rgba(0,0,0,0.06);
}
.search-bar-desktop .ms {
  font-size: 18px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
.search-bar-input {
  border: none;
  background: none;
  font-family: var(--font);
  font-size: 13px;
  color: var(--text-primary);
  outline: none;
  width: 165px;
  /* Fills the remaining width so a click anywhere in the bar lands on the
     input rather than on dead padding. The form's onclick covers the rest. */
  flex: 1;
  min-width: 0;
}
.search-bar-input::placeholder { color: var(--text-tertiary); }
.search-bar-input::-webkit-search-cancel-button { display: none; }
.search-bar-kbd {
  font-family: var(--font);
  font-size: 11px;
  background: #f0f0f0;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 1px 5px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
/* The ⌘K hint STAYS while typing. Hiding it on focus removed a fixed-width
   element mid-interaction, so the bar visibly resized the moment you clicked
   into it. Dimmed instead: still legible as a hint, no longer competing with
   what is being typed, and the geometry never moves. */
.search-bar-desktop:focus-within .search-bar-kbd {
  opacity: 0.45;
  transition: opacity 0.15s;
}

/* ── Mobile/iPad search overlay ── */
.search-overlay {
  display: none;
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(242, 241, 239, 0.97);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  align-items: center;
  padding: 0 12px;
  gap: 8px;
  z-index: 60;
}
.search-overlay.open { display: flex; }
@media (min-width: 1200px) { .search-overlay { display: none !important; } }
.search-overlay .ms {
  font-size: 20px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
.search-overlay-input {
  flex: 1;
  border: none;
  background: none;
  font-family: var(--font);
  font-size: 15px;
  color: var(--text-primary);
  outline: none;
  min-width: 0;
}
.search-overlay-input::placeholder { color: var(--text-tertiary); }
.search-overlay-input::-webkit-search-cancel-button { display: none; }
.search-overlay-cancel {
  background: none;
  border: none;
  font-family: var(--font);
  font-size: 14px;
  color: var(--text-secondary);
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 6px;
  white-space: nowrap;
  flex-shrink: 0;
}
.search-overlay-cancel:hover { color: var(--text-primary); background: var(--surface2); }

/* ── Search results page ── */
.search-results-wrap { padding: 28px 24px; }
/* Autocomplete list under the search input. Anchored to the form, which is
   position:relative, so it follows the input in both the desktop bar and the
   mobile overlay. */
.search-suggest {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 1200;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
  max-height: 320px;
  overflow-y: auto;
}
.search-suggest[hidden] { display: none; }
.search-suggest-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 14px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text-primary);
}
.search-suggest-item:hover,
.search-suggest-item.selected { background: var(--surface2); }
.search-suggest-count {
  font-size: 12px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
.search-bar-desktop, .search-overlay form { position: relative; }

.search-corrected {
  margin: -4px 0 14px;
  font-size: 13px;
  color: var(--text-secondary);
}

.search-results-header {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.search-results-title {
  font-size: 22px;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0;
}
.search-results-count {
  font-size: 14px;
  color: var(--text-tertiary);
}
/* Quieter than .search-corrected: that one reports what the search DID, this
   one explains a word. Same column, no alarm colour — it is information, not a
   problem. */
.search-term-note {
  max-width: 640px;
  margin: 4px 0 14px;
  padding: 10px 14px;
  border-left: 3px solid var(--border);
  background: var(--bg-secondary);
  border-radius: 0 8px 8px 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.search-no-results, .search-loading {
  text-align: center;
  padding: 80px 20px;
  color: var(--text-tertiary);
  font-size: 15px;
}
.search-no-results-img {
  width: 120px; height: 120px;
  object-fit: contain;
  display: block;
  margin: 0 auto 16px;
}
.search-no-results-text { font-size: 15px; color: var(--text-tertiary); }
@media (max-width: 1024px) {
  .search-results-wrap { padding: 20px 14px; }
  .search-results-title { font-size: 18px; }
}

/* ═══════════════════════════════════════════
   APP BODY: SIDEBAR + CONTENT
   ═══════════════════════════════════════════ */
.app-body {
  display: flex;
  align-items: flex-start;
  min-height: 100vh;
}

/* ═══════════════════════════════════════════
   NAVIGATION SIDEBAR (waterfall)
   ═══════════════════════════════════════════ */
.nav-sidebar {
  width: var(--nav-w);
  flex-shrink: 0;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 0 0 40px;
}

/* Sidebar scrollbar */
.nav-sidebar::-webkit-scrollbar { width: 4px; }
.nav-sidebar::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 2px; }

/* ── Shared row base ── */
.nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  transition: background 0.1s;
  white-space: nowrap;
  overflow: hidden;
}
.nav-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.nav-row-count { flex-shrink: 0; font-variant-numeric: tabular-nums; }

/* ── Domain rows — section header style ── */
.nav-domain-row {
  padding: 14px 14px 5px 14px;
  gap: 7px;
  user-select: none;
}
.nav-domain-row:hover { background: var(--d60); }
.nav-domain-row.ancestor { background: var(--d60); }
.nav-domain-row.ancestor .nav-domain-name { color: var(--dtext); opacity: 0.85; }

.nav-arrow {
  font-size: 8px;
  color: #ccc;
  flex-shrink: 0;
  transition: transform 0.18s ease;
}
.nav-arrow.open { transform: rotate(90deg); color: #999; }

/* The icon sits in a fixed square and is centred inside it, so every domain name
   starts on the same column whether its icon is an illustration or an emoji, and
   whatever that icon's aspect ratio. Sized up from 16px on 2026-07-31: at 16px
   these read as texture rather than as pictures, which defeats the point of
   replacing the emoji. font-size carries the emoji fallback iconHtml still
   supports — kept in step with the box so a half-migrated sidebar stays aligned. */
.nav-domain-icon {
  font-size: 22px; line-height: 1; flex-shrink: 0;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
}
.nav-domain-icon img {
  width: 24px; height: 24px; object-fit: contain; display: block;
}
/* The modal breadcrumb is a different job — inline with 13px text, where 24px
   would tower over the line. It keeps the original optical-box sizing. */
.modal-taxon-domain img {
  width: 16px; height: 16px; object-fit: contain;
  vertical-align: -3px; display: inline-block;
}
.nav-domain-name {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1px;
  color: var(--text-tertiary);
  white-space: normal;
  text-overflow: clip;
  line-height: 1.3;
}
.nav-domain-count { font-size: 12px; color: #ccc; }

/* ── Category items ── */
.nav-cats { overflow: hidden; max-height: 0; transition: max-height 0.25s ease; }
.nav-cats.open { max-height: 1200px; }

.nav-cat-item {
  padding: 10px 14px 10px 18px;
  gap: 0;
}
.nav-cat-item:hover { background: var(--d60); }
/* Category is the selected leaf (no subcategory active) */
.nav-cat-item.active { background: var(--d100); }
.nav-cat-item.active .nav-row-name { color: var(--dtext); font-weight: 500; }
.nav-cat-item.active .nav-row-count { color: var(--dtext); opacity: 0.55; }
/* Category is an ancestor (subcategory below is the selection) */
.nav-cat-item.ancestor { background: var(--d80); }
.nav-cat-item.ancestor .nav-row-name { color: var(--dtext); font-weight: 500; }
.nav-cat-item.ancestor .nav-row-count { color: var(--dtext); opacity: 0.55; }

.nav-cat-item .nav-row-name {
  font-size: 15px;
  font-weight: 500;
  color: var(--text-primary);
}
.nav-cat-item .nav-row-count { font-size: 13px; color: #bbb; }

.nav-cat-arrow {
  font-size: 8px;
  color: #ddd;
  flex-shrink: 0;
  margin-right: 6px;
  transition: transform 0.18s ease;
}
.nav-cat-arrow.open { transform: rotate(90deg); color: #aaa; }
.nav-cat-item.active .nav-cat-arrow   { color: var(--dtext); opacity: 0.5; }
.nav-cat-item.ancestor .nav-cat-arrow { color: var(--dtext); opacity: 0.5; }

/* ── Subcategory items ── */
.nav-subcats { overflow: hidden; max-height: 0; transition: max-height 0.22s ease; }
.nav-subcats.open { max-height: 2000px; }

.nav-subcat-item {
  padding: 9px 14px 9px 28px;
}
.nav-subcat-item:hover { background: var(--d60); }
.nav-subcat-item.active { background: var(--d100); }
.nav-subcat-item.active .nav-row-name { color: var(--dtext); font-weight: 500; }
.nav-subcat-item.active .nav-row-count { color: var(--dtext); opacity: 0.55; }

.nav-subcat-item .nav-row-name {
  font-size: 14px;
  font-weight: 400;
  color: var(--text-secondary);
}
.nav-subcat-item .nav-row-count { font-size: 12px; color: #ccc; }

/* ═══════════════════════════════════════════
   CONTENT AREA
   ═══════════════════════════════════════════ */
.content-area {
  flex: 1;
  min-width: 0;
}
#main { padding-bottom: 48px; } /* clearance for fixed footer */

/* ── Footer — fixed to bottom of content area ── */
.site-footer {
  position: fixed;
  bottom: 0;
  left: var(--nav-w);
  right: 0;
  z-index: 40;
  padding: 11px 24px;
  border-top: 1px solid var(--border);
  background: rgba(242,241,239,0.92);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  text-align: center;
  font-size: 11.5px;
  color: var(--text-tertiary);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}
/* ── Offline indicator ── */
.offline-banner {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 400;
  background: #1a1a1a;
  color: #fff;
  text-align: center;
  font-size: 13px;
  padding: 8px 16px;
}
.offline-banner.visible { display: block; }

/* ── PWA install banner (Android Chrome only) ── */
.install-banner {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0; right: 0;
  z-index: 300;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 12px 16px;
  align-items: center;
  gap: 12px;
  box-shadow: 0 -4px 16px rgba(0,0,0,0.08);
}
.install-banner.visible { display: flex; }
.install-banner-icon {
  width: 40px; height: 40px;
  border-radius: 10px;
  flex-shrink: 0;
  object-fit: contain;
}
.install-banner-text { flex: 1; min-width: 0; }
.install-banner-title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.install-banner-sub   { font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
.install-banner-btn {
  flex-shrink: 0;
  padding: 7px 16px;
  border-radius: 20px;
  border: none;
  background: var(--text-primary);
  color: var(--surface);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.install-banner-dismiss {
  flex-shrink: 0;
  background: none; border: none;
  color: var(--text-tertiary);
  font-size: 20px;
  cursor: pointer;
  padding: 0 4px;
  line-height: 1;
}

.site-footer-sep { opacity: 0.4; }
.site-footer a {
  color: var(--text-tertiary);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  opacity: 0.8;
}
.site-footer a:hover { opacity: 1; }

/* ═══════════════════════════════════════════
   PAGE NAV TITLE (breadcrumb-as-title)
   ═══════════════════════════════════════════ */
.page-nav-title {
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.page-nav-home {
  font-size: 20px;
  cursor: pointer;
  line-height: 1;
  opacity: 0.75;
  transition: opacity 0.15s;
  user-select: none;
}
.page-nav-home:hover { opacity: 1; }
.page-nav-sep {
  color: #ccc;
  font-size: 22px;
  /* 400, not 300 — the loaded webfont has no weight below 400, so this always
     painted at 400 anyway. See the note on .cls-section-title. */
  font-weight: 400;
  line-height: 1;
  user-select: none;
}
.page-nav-ancestor {
  font-size: 20px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color 0.15s;
  line-height: 1.2;
}
.page-nav-ancestor:hover { color: var(--accent); }
/* Same size as the ancestor crumbs — it sits inline in the trail now rather
   than on its own title line, so it reads as the last step of the path. Colour
   and weight still mark it as where you are. */
.page-nav-current {
  font-size: 20px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.2;
}
/* Current title + count share a flex row */
.page-nav-current-wrap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  width: 100%;
  margin-top: 4px;
  gap: 12px;
}
.page-nav-count {
  font-size: 13px;
  font-weight: 400;
  color: var(--text-tertiary);
  white-space: nowrap;
  flex-shrink: 0;
}

/* ── Color picker button ──────────────────────────────────────────────────── */
.color-picker-btn {
  width: 32px; height: 32px;
  border-radius: 8px;
  border: none;
  background: none;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  flex-shrink: 0;
  font-size: 18px;
  position: relative;
  transition: transform 0.15s;
}
.color-picker-btn:hover { transform: scale(1.15); }
.section-color-btn { width: 44px !important; height: 44px !important; transform: none; }
.section-color-btn:hover { transform: scale(1.1); }
.color-picker-btn .ms { font-size: 20px; line-height: 1; color: var(--text-secondary); }
/* Small accent dot when a color filter is active */
.color-picker-btn.has-filter::after {
  content: '';
  position: absolute;
  top: 2px; right: 2px;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
  border: 1.5px solid var(--surface);
}

/* ── Colour filter bar ────────────────────────────────────────────────────────
   An inline bar in the page flow, not a floating panel: it stays open while you
   toggle swatches and while you navigate between subcategories, which is the
   whole point — colour filtering is exploratory and you rarely want just one.
   Swatches are grouped by hue with a hairline between groups; the row wraps as
   the viewport narrows, so no width is hardcoded and nothing is ever clipped. */
/* ── Facet reference page (#35) ──────────────────────────────────────────────
   A reference, not a filter: every facet and every value with its catalog count,
   each value a button that runs a search. Lives inside the info overlay, so it
   inherits that panel's scrolling and padding and needs no layout of its own. */
.facet-ref { margin-top: 4px; }
.facet-ref-group { margin: 0 0 22px; }
.facet-ref-group h3 {
  font-size: 14px;
  margin: 0 0 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* The axis (`glyph`) is not a filter dimension — its illustrations carry no
   subject to combine with — so it is labelled as a kind of thing instead. */
.facet-ref-axis {
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 7px;
}
.facet-ref-values {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.facet-ref-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font: inherit;
  font-size: 12px;
  line-height: 1.3;
  color: inherit;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  cursor: pointer;
}
.facet-ref-chip:hover { background: var(--hover); border-color: var(--text-tertiary); }
.facet-ref-chip-count { font-size: 10px; color: var(--text-tertiary); }
/* A general value labels GENERIC content — it is not a parent of the specific
   values beside it (P(animal|dog) is 6.2%), so it is set apart rather than made
   to look like a heading. Issue #45. */
.facet-ref-chip--general { background: var(--surface); border-style: dashed; }
.facet-ref-divider {
  width: 1px;
  align-self: stretch;
  min-height: 18px;
  background: var(--border);
  margin: 0 4px;
}
.facet-note { font-size: 12px; color: var(--text-tertiary); }
.facet-ref-link {
  font: inherit;
  color: inherit;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 14px;
  cursor: pointer;
}
.facet-ref-link:hover { background: var(--hover); border-color: var(--text-tertiary); }

.color-bar {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 10px 14px;
  /* Sits between a section header and its grid; the grid's own 18px top margin
     supplies the gap below, so this only needs the gap above. */
  margin: 18px 0 0;
  /* Full width of the section. The swatch groups below stretch to fill it, so
     there is no dead strip on the right and nothing to overflow. */
}
.color-bar[hidden] { display: none; }
/* The label's slot, kept out of the swatch grid so it never lands mid-row among
   the colours. Holds the label AND the Clear button, stacked in one grid cell
   and swapped with `visibility` rather than `display`: both keep contributing
   to the cell's intrinsic width, so the slot is always as wide as the wider of
   the two and selecting a colour never nudges the swatches sideways. */
.color-bar-aside {
  display: grid;
  align-items: center;
  justify-items: stretch;
  flex-shrink: 0;
  min-height: 24px;   /* same as a swatch, so the swap doesn't shift the row */
}
.color-bar-label,
.color-bar-clear { grid-area: 1 / 1; }
/* Not [hidden]/display:none — that would collapse the cell, which is the whole
   thing this layout exists to prevent. */
.color-bar-off { visibility: hidden; }
.color-bar-label {
  font-size: 13px; font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  line-height: 24px;   /* matches swatch height, so it sits on the first row */
}
/* Shaped to match .modal-action-btn — same radius, border, weight and type
   size, just a little less vertical padding so it fits the 24px swatch row. */
.color-bar-clear {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  font-size: 12px; font-weight: 500;
  color: var(--btn-red-text);
  background: var(--btn-red);
  border: 1px solid var(--btn-red-border);
  border-radius: 10px;
  cursor: pointer;
  font-family: var(--font);
  padding: 4px 13px;
  line-height: 16px;
  white-space: nowrap;
  transition: background 0.18s, border-color 0.18s;
}
.color-bar-clear:hover { background: var(--btn-red-hover); }
.color-bar-clear .ms { font-size: 15px; line-height: 1; }

/* Swatch groups sit in a grid with a fixed column count per range.

   A grid rather than wrapping flex, because the hairline dividers need to know
   which group starts a row and no selector can tell that when the break point
   moves freely — nth-child can, once the column count is fixed.

   `1fr` rather than `max-content`, which was the earlier mistake: max-content
   columns cannot shrink, so between two breakpoints the row kept its natural
   width and spilled out of the card. Equal fractional columns fill the bar
   exactly and rows always align, including a short final row.

   The counts below were measured, not guessed, against the real layout budget:
   viewport minus --nav-w (252/260/268/290 by range) minus .page-wrap's
   percentage padding (7%, or 4% once the sidebar becomes a drawer). One result
   is worth knowing: **available width is not monotonic**. A 1024px window has
   ~912px for this bar while a 1025px window has ~628px, because the sidebar
   collapses and the padding drops from 7% to 4%. The narrowest case in the
   whole app is a small laptop around 1025px, not a phone — which is why
   breakpoints chosen by eye kept overflowing there. Each count below is
   checked against the narrowest width in its own range. */
.color-bar-swatches {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  align-items: center;
  row-gap: 10px;
  flex: 1;
  min-width: 0;
}
.color-bar-group {
  display: flex;
  align-items: stretch;          /* chips fill the group's height */
  gap: 3px;
  --group-pad: 10px;             /* one source for the divider inset */
  padding: 0 var(--group-pad);
  border-left: 1px solid var(--border);
}
/* The merged skins+neutrals group is 7 swatches (184px) against a hue group's
   76px, so it cannot sit in an equal 1fr column. Spanning two keeps the columns
   equal AND keeps the nth-child maths below valid: hue groups 1-8 occupy slots
   1-8 and this one occupies slot 9, so group index still equals slot index. */
.color-bar-group.misc { grid-column: span 2; }

/* No divider at the start of a row. Each range below restores the previous
   range's rule and clears its own — the step must match the column count. */
.color-bar-swatches > .color-bar-group:nth-child(10n + 1) {
  padding-left: 0; border-left: none;
}
/* Rectangle chips, not circles.
   A circle has one size variable and a fixed footprint, so any column wider
   than the circle it holds becomes dead space — which is what left the ragged
   gaps when the bar was resized. A chip has width and height: it stretches to
   fill its group, the group fills its column, and the row reaches the divider
   at every width. It is also a much larger tap target.

   It collapses the breakpoint arithmetic too. A 3-chip group needs 3×minChip;
   the 7-chip misc group across two columns needs 7×minChip over 2 — the same
   per-column minimum of 68px. One number now governs every range, where the
   circles needed 204px for misc against 76px for a hue group. Chip widths come
   out within a pixel or two of each other across groups as a result. */
.color-picker-swatch {
  flex: 1 1 0;                   /* every chip in a group shares it equally */
  min-width: 0;
  height: 26px;
  border-radius: 4px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.1s;
  box-shadow: 0 1px 2px rgba(0,0,0,0.14);
  padding: 0;
}
/* The white/off-white chip is nearly the same value as --surface, so without a
   hairline it reads as an empty slot. Inset ring rather than `border`, since
   the border is what .active repurposes for its selected state. Declared before
   .active so that rule's box-shadow still wins at equal specificity. */
.color-picker-swatch[data-color-id="white"] {
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.16), 0 1px 2px rgba(0,0,0,0.14);
}
.color-picker-swatch:hover { transform: translateY(-2px); }
/* Colours absent from the current view. grayscale() rather than a flat grey fill
   so the swatch keeps its own lightness — a pale yellow and a dark navy stay
   distinguishable while both read as off, which a single grey would destroy.
   Opacity does the rest of the work of saying "not available". */
.color-picker-swatch.unavailable {
  filter: grayscale(1);
  opacity: 0.28;
  cursor: default;
}
.color-picker-swatch.unavailable:hover { transform: none; }
.color-picker-swatch.active {
  border-color: var(--surface);
  box-shadow: 0 0 0 2px var(--text-primary);
}
/* Three ranges, all governed by the same 68px-per-column minimum. Chips made
   that possible: with circles the misc group needed 204px against a hue
   group's 76px, which forced four ranges and put ten-across out of reach below
   1700px. Above 1399px the base rule's 10 columns apply — one row. */

/* 621–1399px → 5 across (2 rows). Binding case is still 1025px, the narrowest
   the bar ever gets — 496px of grid over 5 columns is 99px each, comfortably
   past the 68px floor. */
@media (max-width: 1399px) {
  .color-bar-swatches { grid-template-columns: repeat(5, 1fr); }
  .color-bar-swatches > .color-bar-group:nth-child(10n + 1) {
    padding-left: var(--group-pad); border-left: 1px solid var(--border);
  }
  .color-bar-swatches > .color-bar-group:nth-child(5n + 1) {
    padding-left: 0; border-left: none;
  }
}
/* ≤620px → the label stacks above, and the grid goes 4 across: the eight hues
   in two rows of four, the merged group taking the whole third row. A
   two-column span would straddle a row boundary at this count, so it spans the
   full row instead — which also gives the misc chips the most room they get
   anywhere, on the screen where fingers need it most. */
@media (max-width: 620px) {
  .color-bar { flex-direction: column; align-items: stretch; gap: 8px; }
  .color-bar-label { line-height: 1.4; }
  .color-bar-swatches { grid-template-columns: repeat(4, 1fr); }
  .color-bar-group.misc { grid-column: 1 / -1; }
  .color-bar-swatches > .color-bar-group:nth-child(5n + 1) {
    padding-left: var(--group-pad); border-left: 1px solid var(--border);
  }
  .color-bar-swatches > .color-bar-group:nth-child(4n + 1) {
    padding-left: 0; border-left: none;
  }
  /* It leads its own row, so it never carries a divider. */
  .color-bar-swatches > .color-bar-group.misc {
    padding-left: 0; border-left: none;
  }
  .color-picker-swatch { height: 30px; }   /* fatter tap target on a phone */
}
/* Chrome trims where space is tight. Placed after the range rules above, but
   `.color-bar-group` is less specific than their nth-child selectors, so the
   row-leading overrides still win. */
@media (max-width: 1024px) {
  .color-bar { gap: 12px; padding: 10px 12px; }
  .color-bar-group { --group-pad: 8px; }
  /* No chip size here: a chip's width comes from its column and its height is
     set once, then raised at 620px for touch. Fixing a width would reintroduce
     exactly the dead space chips exist to remove. */
}

/* ── Grid title row — removed 2026-07-29 ──────────────────────────────────────
   The subcategory name moved into the breadcrumb as its final crumb and the
   subcategory-wide total was dropped, so .grid-title-row, .grid-title-left,
   .grid-subcat-name and .grid-title-count went with it. .grid-title-chevron and
   .grid-section-btn outlived it as unreachable rules — a chevron for a row that
   no longer exists, and a fossil of the design where the section *name* opened
   the picker instead of the hamburger — and were deleted the same day. */

/* ── Section picker dropdown ──────────────────────────────────────────────── */
.cls-section-header {
  display: flex;
  align-items: center;
  gap: 10px;
  position: relative;
}
.section-hamburger-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* 48px, matching .sec-btn — it is the fifth control on this row, not a
     smaller relative of it. 44px beside 48px read as a mistake rather than a
     distinction. The glyph is sized to match too, and both follow the section
     title down at the same breakpoints (#78). */
  width: 48px; height: 48px; flex-shrink: 0;
  background: none; border: none; cursor: pointer;
  border-radius: 8px; color: var(--text-tertiary);
  transition: background 0.12s, color 0.12s;
}
.section-hamburger-btn:hover { background: var(--hover); color: var(--text-primary); }
.section-hamburger-btn .ms { font-size: 30px; }
@media (max-width: 900px) {
  .section-hamburger-btn { width: 42px; height: 42px; }
  .section-hamburger-btn .ms { font-size: 25px; }
}
@media (max-width: 600px) {
  .section-hamburger-btn { width: 38px; height: 38px; }
  .section-hamburger-btn .ms { font-size: 22px; }
}
.section-picker-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 4px); left: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.14);
  z-index: 100;
  min-width: 180px;
  padding: 6px;
}
.section-picker-dropdown.open { display: block; }
.section-picker-item {
  display: flex; align-items: center;
  justify-content: space-between;
  padding: 8px 10px; border-radius: 8px;
  cursor: pointer; font-size: 14px;
  font-family: var(--font);
  background: none; border: none; width: 100%;
  text-align: left; color: var(--text-primary); gap: 8px;
}
.section-picker-item:hover { background: var(--surface2); }
/* Selected row, matching .nav-subcat-item.active in the sidebar — a filled row
   in the domain's own colour rather than bold blue text, so "where I am" looks
   the same in both navigation surfaces.
   The fallbacks matter: --d100/--dtext only exist under [data-domain], which the
   grid view now sets, but this dropdown also renders in views that have no single
   domain (search results). There it degrades to a neutral filled row rather than
   an invisible one. */
.section-picker-item.active {
  background: var(--d100, var(--surface2));
  color: var(--dtext, var(--accent));
  font-weight: 500;
}
.section-picker-item.active:hover { background: var(--d100, var(--surface2)); }
.section-picker-item.active .section-picker-item-count {
  color: var(--dtext, var(--text-tertiary));
  opacity: 0.55;
}
.section-picker-item-count { font-size: 12px; color: var(--text-tertiary); }

/* ═══════════════════════════════════════════
   PAGE LAYOUT
   ═══════════════════════════════════════════ */
.page-wrap {
  padding: 18px 7% 60px;
}


.page-title {
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.6px;
  color: var(--text-primary);
  margin-bottom: 4px;
  line-height: 1.15;
}

.page-subtitle {
  font-size: 14px;
  color: var(--text-secondary);
  margin-bottom: 20px;
}

/* ═══════════════════════════════════════════
   2-COLUMN CARD GRID
   ═══════════════════════════════════════════ */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
  gap: 14px;
}

/* ── Responsive breakpoints ── */

/* Ultrawide (≥ 1800px) — expand sidebar, bigger ill cells */
@media (min-width: 1800px) {
  :root { --nav-w: 290px; }
  .ill-grid, .cls-section-grid { grid-template-columns: repeat(auto-fill, minmax(calc(204px * var(--card-scale)), 1fr)); }
  .page-wrap { padding: 28px 6.5% 80px; }
}

/* Large desktop (≥ 1440px) */
@media (min-width: 1440px) and (max-width: 1799px) {
  :root { --nav-w: 268px; }
  .ill-grid, .cls-section-grid { grid-template-columns: repeat(auto-fill, minmax(calc(192px * var(--card-scale)), 1fr)); }
}

/* Small desktop / iPad landscape (1025px – 1199px) */
@media (max-width: 1199px) and (min-width: 1025px) {
  :root { --nav-w: 260px; }
  .ill-grid, .cls-section-grid { grid-template-columns: repeat(var(--card-cols-5), 1fr); }
  .card-grid { grid-template-columns: repeat(2, 1fr); }
  /* Search: icon-only (same as mobile) */
  .search-btn {
    width: 36px; height: 36px; border-radius: 50%;
    border: none !important; background: none !important; padding: 0 !important;
    font-size: 18px;
  }
  .search-btn-text { display: none; }
  /* iPad nav menu */
  .ipad-nav-wrap { display: flex; align-items: center; }
  .top-bar-link--secondary { display: none !important; }
  .top-bar-sep--secondary  { display: none !important; }
}

/* ── Mobile menu dropdown — hidden on desktop ── */
.mobile-menu-wrap {
  display: none;
  position: relative;
  flex-shrink: 0;
}
.mobile-menu-btn {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: none;
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.mobile-menu-btn:active { background: var(--surface2); }
.mobile-menu-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.12);
  padding: 6px;
  z-index: 500;
}
.mobile-menu-dropdown.open { display: block; }
.mobile-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: none;
  background: none;
  border-radius: 8px;
  font-size: 14px;
  color: var(--text-secondary);
  cursor: pointer;
}
.mobile-menu-item:active { background: var(--surface2); }
.mobile-menu-item--web    { color: hsl(205,62%,40%); font-weight: 600; }
.mobile-menu-item--nature { color: hsl(128,42%,32%); font-weight: 600; }
.mobile-menu-item--sports { color: hsl(25,70%,40%);  font-weight: 600; }
.mobile-menu-item--lang   { color: var(--text-secondary); font-size: 13px; }
.mobile-menu-sep {
  height: 1px;
  background: var(--border);
  margin: 4px 6px;
}

/* Hamburger button — hidden on desktop, shown via media query below */
.hamburger-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: none;
  color: var(--text-primary);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  border-radius: 8px;
  transition: background 0.15s;
}
.hamburger-btn:hover { background: var(--surface2); }
.hamburger-btn svg { display: block; }
.hamburger-btn .ms { font-size: 22px; line-height: 1; }

/* Mobile + Tablet (≤ 1024px) — drawer nav, hamburger + menu button */
@media (max-width: 1024px) {
  /* Footer flows at bottom of page instead of fixed overlay */
  .site-footer { position: static; }
  #main { padding-bottom: 0; }
  /* Sidebar becomes a slide-in drawer */
  .nav-sidebar {
    position: fixed;
    top: 0; left: 0;
    height: 100dvh;
    z-index: 200;
    transform: translateX(-100%);
    transition: transform 0.28s cubic-bezier(0.4,0,0.2,1);
    box-shadow: 4px 0 24px rgba(0,0,0,0.15);
    width: 280px !important;
  }
  .nav-sidebar.drawer-open { transform: translateX(0); }

  /* Overlay behind the drawer */
  .nav-drawer-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 199;
    background: rgba(0,0,0,0.35);
    backdrop-filter: blur(2px);
  }
  .nav-drawer-overlay.visible { display: block; }

  /* Show hamburger + menu btn, hide the top-bar link list */
  .hamburger-btn { display: flex; }
  .mobile-menu-wrap { display: block; }
  .top-bar-links { display: none; }

  /* Hide ⌘K shortcut hint on touch devices */
  .search-btn kbd { display: none; }

  /* Grids */
  .card-grid { grid-template-columns: 1fr; }
  .ill-label { padding: 4px 5px 5px; }
  .ill-label-text, .ill-label-base, .ill-label-qual { font-size: var(--label-font-sm, 11px); }
  .page-wrap { padding: 14px 4% 60px; }

  /* Modal full-screen on mobile */
  .modal-overlay { padding: 0; align-items: flex-end; }
  .modal-scene {
    max-width: 100% !important;
    width: 100% !important;
    max-height: 92dvh;
    border-radius: 20px 20px 0 0;
  }
}


/* ═══════════════════════════════════════════
   DOMAIN / CATEGORY CARDS (horizontal layout)
   ═══════════════════════════════════════════ */
.big-card {
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  cursor: pointer;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  min-height: 148px;
  transition: transform var(--transition);
}
.big-card:hover {
  transform: scale(1.014) translateY(-2px);
  box-shadow: var(--shadow-card-hover);
}
.big-card:active { transform: scale(0.99); }

/* Left: square illustration — neutral gray matching modal */
.card-img-side {
  width: 148px;
  flex-shrink: 0;
  position: relative;
  overflow: hidden;
  background: #f5f4f2;
}
/* Domain color accent: left border on all card levels */
.big-card[data-domain],
.subcat-card[data-domain] {
  border-left: 10px solid var(--d100);
}
.card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 14px;
}

/* Right: title + subcategory names */
.card-content {
  flex: 1;
  min-width: 0;
  padding: 18px 18px 16px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.card-title-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.card-icon {
  font-size: 32px;
  line-height: 1;
  flex-shrink: 0;
  margin-top: 1px;
}

.card-title-group { flex: 1; min-width: 0; }

.card-title {
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.4px;
  line-height: 1.2;
  color: var(--text-primary);
}

.card-count {
  font-size: 13px;
  color: var(--text-tertiary);
  margin-top: 3px;
  font-weight: 500;
}
.card-cat-count {
  font-size: 13px;
  color: var(--text-tertiary);
  margin-top: 1px;
  font-weight: 400;
}

.card-subcats {
  font-size: 12px;
  color: var(--text-tertiary);
  line-height: 1.8;
  margin-top: 10px;
}
.card-subcats .card-subcat-line {
  display: block;
}

/* ═══════════════════════════════════════════
   SUBCATEGORY CARDS (category overview page)
   ═══════════════════════════════════════════ */
.subcat-card {
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  cursor: pointer;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  min-height: 130px;
  transition: transform var(--transition);
}
.subcat-card:hover {
  transform: scale(1.014) translateY(-2px);
  box-shadow: var(--shadow-card-hover);
}
.subcat-card:active { transform: scale(0.99); }

.subcat-card-img-side {
  width: 130px;
  flex-shrink: 0;
  position: relative;
  background: #f5f4f2;
  overflow: hidden;
}
.subcat-card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 12px;
}

.subcat-card-content {
  flex: 1;
  min-width: 0;
  padding: 16px 16px 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.subcat-card-name {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.3px;
  color: var(--text-primary);
  line-height: 1.25;
  margin-bottom: 6px;
}

.subcat-card-count {
  font-size: 12px;
  color: var(--text-tertiary);
  font-weight: 500;
}

/* ═══════════════════════════════════════════
   EXPANDED DOMAIN VIEW (single-page hierarchy)
   ═══════════════════════════════════════════ */
.domain-exp-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Each category section: header + subcat card grid stacked vertically */
.domain-exp-cat {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 28px 0;
  border-bottom: 1px solid var(--border);
}
.domain-exp-cat:last-child { border-bottom: none; }

/* Category header: image+title on top row, desc full width below */
.domain-exp-cat-header {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.domain-exp-cat-header-top {
  display: flex;
  align-items: center;
  gap: 20px;
}

.domain-exp-cat-img {
  width: 100px;
  height: 100px;
  flex-shrink: 0;
  border-radius: 14px;
  overflow: hidden;
  background: #f5f4f2;
  position: relative;
}
.domain-exp-cat-img img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 10px;
}

.domain-exp-cat-title-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.domain-exp-cat-title {
  font-size: 26px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.2;
}

.domain-exp-cat-count {
  font-size: 13px;
  font-weight: 400;
  color: var(--text-tertiary);
  white-space: nowrap;
}

.domain-exp-cat-desc {
  font-size: 15px;
  font-weight: 400;
  color: var(--text-secondary);
  line-height: 1.6;
}

/* ═══════════════════════════════════════════
   ILLUSTRATION GRID (leaf level)
   ═══════════════════════════════════════════ */
.grid-view {
  width: 100%;
}

.subcat-sidebar {
  display: none;
  width: 190px;
  flex-shrink: 0;
  position: sticky;
  top: 72px;
  max-height: calc(100vh - 88px);
  overflow-y: auto;
}

.subcat-sidebar::-webkit-scrollbar { width: 4px; }
.subcat-sidebar::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 2px; }

.subcat-sidebar-section {
  background: var(--surface);
  border-radius: 16px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.subcat-sidebar-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1px;
  color: var(--text-tertiary);
  padding: 12px 13px 6px;
}

.subcat-sidebar-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 13px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
  border-bottom: 1px solid var(--border);
}
.subcat-sidebar-item:last-child { border-bottom: none; }
.subcat-sidebar-item:hover { background: var(--surface2); color: var(--text-primary); }
.subcat-sidebar-item.active { background: #0066cc; color: white; font-weight: 500; }
.subcat-sidebar-item.active .ss-count { color: rgba(255,255,255,0.6); }

.ss-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss-count { font-size: 11px; color: #bbb; flex-shrink: 0; margin-left: 6px; }

.ill-grid-wrap { flex: 1; min-width: 0; }

.ill-count {
  font-size: 13px;
  color: var(--text-tertiary);
  margin-bottom: 12px;
  font-weight: 500;
}

.ill-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(180px * var(--card-scale)), 1fr));
  gap: 10px;
}
/* Section headings inside the SEARCH grid (#35). `grid-column: 1 / -1` is
   load-bearing, not cosmetic: without it a heading is placed as an ordinary
   grid cell and sits in the first column beside three illustrations, which
   reads as a broken card rather than a heading. First heading has no top
   margin — the results heading is already directly above it. */
.search-section-head {
  grid-column: 1 / -1;
  margin: 34px 0 2px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.search-section-head:first-child { margin-top: 6px; }
.search-section-head .cls-section-title { font-size: 28px; }
/* Phase-2 loading skeleton */
.grid-loading-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 260px;
  gap: 14px;
  color: var(--text-tertiary);
}
.grid-loading-spinner {
  width: 32px; height: 32px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.grid-loading-label { font-size: 14px; font-weight: 500; }

/* ═══════════════════════════════════════════
   GRID DISPLAY CONTROLS — card background + card size
   Two segmented three-way choices, sitting opposite the breadcrumb. Page-level
   rather than per-section, for the same reason the colour bar is: the setting
   applies to the whole grid, so one copy of it says what it actually does.
   ═══════════════════════════════════════════ */
.grid-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.grid-controls { display: flex; align-items: center; gap: 8px; }
.seg {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 11px;
}
.seg button {
  width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: 8px;
  background: transparent;
  cursor: pointer;
  display: grid; place-items: center;
  transition: background var(--transition), box-shadow var(--transition);
}
.seg button:hover { background: var(--hover, #f0ece6); }
/* Selection LIFTS rather than repaints — this file's own idiom (.rc, .big-card).
   And the selected option is INERT: both controls are three-way choices, so
   pressing the active one must not compound. Killing the cursor and the hover
   response says so before the click rather than after. */
.seg button[aria-checked="true"] {
  background: #fff;
  box-shadow: var(--shadow-card);
  cursor: default;
}
.seg button[aria-checked="true"]:hover { background: #fff; }
.seg button:focus-visible { outline: 2.5px solid var(--accent); outline-offset: 2px; }

/* Each button DRAWS what it does — the background swatch is the background it
   applies, the size glyph is the density it produces. No text, so the two
   groups read as one family and neither needs translating into the button. */
.seg-sw { width: 18px; height: 18px; border-radius: 5px; border: 1px solid rgba(0,0,0,0.14); }
.seg-sw.is-default { background: #f5f4f2; }
.seg-sw.is-dark    { background: hsl(35 6% 18%); }
.seg-sw.is-checker {
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #dcd7d0 25%, transparent 25%, transparent 75%, #dcd7d0 75%),
    linear-gradient(45deg, #dcd7d0 25%, transparent 25%, transparent 75%, #dcd7d0 75%);
  background-size: 10px 10px;
  background-position: 0 0, 5px 5px;
}
.seg-sz { display: grid; gap: 2px; width: 18px; height: 18px; }
.seg-sz i { background: #b7b0a8; border-radius: 1.5px; display: block; }
.seg-sz.is-s { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 1.5px; }
.seg-sz.is-m { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr); }
.seg-sz.is-l { grid-template-columns: 1fr; grid-template-rows: 1fr; }
.seg button[aria-checked="true"] .seg-sz i { background: var(--text-secondary); }

/* Reading type scale (#79). A letter at three sizes, in the same 18px box the
   background swatch and the card-size grid use, so the three view settings read
   as one row of equals. Fixed px and NOT var(--text-scale): this control is
   chrome, and a button that grew when pressed would shove its own row sideways. */
.seg-tx { width: 18px; height: 18px; display: grid; place-items: center;
          font-weight: 800; color: #b7b0a8; line-height: 1; font-family: var(--font); }
/* The swatch shows the STEP, so five steps need five sizes. They rise with the
   scale they stand for rather than evenly: the point of the row is that you can
   see which A is bigger without reading the label. */
.seg-tx.is-xs { font-size: 8px; }
.seg-tx.is-s  { font-size: 10px; }
.seg-tx.is-m  { font-size: 12px; }
.seg-tx.is-l  { font-size: 15px; }
.seg-tx.is-xl { font-size: 18px; }
.seg button[aria-checked="true"] .seg-tx { color: var(--text-secondary); }

.ill-card {
  background: var(--surface);
  border-radius: 14px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
  cursor: pointer;
  /* transform only — GPU composited, zero repaint per frame */
  transition: transform 0.12s ease-out;
  /* isolate each card's layout/style scope so hover recalc doesn't cascade */
  contain: layout style;
}
.ill-card:hover {
  transform: scale(1.04);
}
/* Ring appears instantly on hover start (one repaint, not per-frame) */
.ill-card[data-domain]:hover {
  outline: 2.5px solid var(--d100);
  outline-offset: 2px;
}
.ill-card:active { transform: scale(0.98); }

.ill-thumb-wrap {
  width: 100%;
  aspect-ratio: 1;
  /* Four longhands, not the `background` shorthand: the shorthand would reset
     background-image every time only the colour changed, and the checkered
     mode needs colour and image set together. */
  background-color: var(--thumb-bg);
  background-image: var(--thumb-bg-image);
  background-size: var(--thumb-bg-size);
  background-position: var(--thumb-bg-position);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative; /* containing block for .hub-badge */
}
.ill-thumb {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 6px;
}

/* A card label holds either ONE line (no Qualifier) or TWO (Base + Qualifier
   under a hairline), and every card must be the same height or the grid goes
   ragged. The height therefore budgets for the worst case: 2 Base lines + the
   separator + 2 Qualifier lines.

   FOUR LINES SINCE 2026-08-09, WAS THREE, and every clamp below follows the
   box rather than being chosen next to it. The single Qualifier line was a
   constant calibrated on 女性・NG・鼻 — three tokens, nine characters — and #87
   part 1 makes that line carry the distinction between siblings, in every
   language. English and Portuguese render the same fact as a phrase, so the
   first family through the pass reached the grid reading
   `female · incorrect · nos…` and `female · incorrect · …`: two cards that a
   reader could no longer tell apart, while the modal and the saved card were
   correct.

   MEASURED BEFORE GROWING IT, over the whole catalogue at the default card
   width. A second Qualifier line serves 452 EN, 819 PT and 34 JP cards. Holding
   the box at three and simply raising the clamp would have CLIPPED 169 EN and
   321 PT, where the Base already takes two lines — so Nepô's call, 2026-08-09,
   was to grow the box instead and clip nothing. It costs one line of height on
   all 35,727 cards, including the ~21,000 with no Qualifier at all, and that is
   the price of a grid whose rows still align.

   The Base clamp follows for free: 105 EN and 141 PT titles were being
   truncated at three lines and now are not.

   flex-direction is COLUMN, and that is load-bearing. This was a default-row
   flex container, which is indistinguishable from a block while it has a single
   child — adding the Qualifier line put the two side by side
   (「自動歩行 原始反射」) rather than stacked. */
.ill-label {
  padding: 5px 9px 7px;
  height: calc(var(--label-font, 12px) * 1.4 * 4 + 7px + 5px + 7px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.ill-label-text, .ill-label-base {
  font-size: var(--label-font, 12px);
  font-weight: 500;
  color: var(--text-secondary);
  line-height: 1.4;
  text-align: center;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* When a Qualifier is present the Base clamps shorter, so Base and Qualifier
   together stay inside the box: 2 + 2 = the four lines it now holds. */
.ill-label-base { -webkit-line-clamp: 2; }
/* Card qualifier line — Guidelines/14 → Anatomy, Guidelines/19 §7.
   Same size as the Base: the Qualifier is part of the title, not metadata about
   it, so shrinking it would demote the one fact that tells two set members
   apart. Separation comes from the hairline and a lighter colour.
   NOT a chip — the only chips are the hub badge here and tags in the modal, so
   a filled pill under a title would read as one.
   keep-all removes the between-character break Japanese allows by default,
   which is the actual defect; ・ stays legal, so a stacked Qualifier wraps at
   its own separator instead of shredding mid-word. */
.ill-label-qual {
  font-size: var(--label-font, 12px);
  color: var(--text-tertiary);
  max-width: 100%;
  line-height: 1.4;
  text-align: center;
  margin-top: 3px;
  padding-top: 2px;
  border-top: 1px solid var(--border);
  align-self: stretch;
  word-break: keep-all;
  overflow-wrap: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.sentinel { height: 80px; }

/* ── Classification sections (Characters / Things / Other) ── */
.cls-section { display: none; }
.cls-section.has-content { display: block; }

.cls-section-header {
  margin: 56px 0 0;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   #78 — THE CONTROLS SIT AT THE SECTION THEY ACT ON
   Ported from playground/grid-controls.html, where each rule below was
   measured against the running app before it came here (ADR-0012).
   ═══════════════════════════════════════════════════════════════════════════ */

/* The header is a row: hamburger, title, count, then the four controls hard
   right. margin-left:auto on the group needs the title to keep width:auto —
   with width:100% the title's box spans the row and swallows the pointer under
   the icons, which reads as the icons hovering the title. Those two rules must
   not be separated. */
.cls-section-header { display: flex; align-items: center; gap: 10px; position: relative; }
.cls-section-header .cls-section-toggle { width: auto; flex: 0 0 auto; }
.sec-controls { display: flex; align-items: flex-start; gap: 6px;
                margin-left: auto; flex-shrink: 0; }
.sec-item { display: flex; flex-direction: column; align-items: center; gap: 1px; }
/* The hamburger's size, so the whole row sits at one scale. */
.sec-btn { display: inline-flex; align-items: center; justify-content: center;
           width: 48px; height: 48px; flex-shrink: 0;
           background: none; border: none; cursor: pointer; border-radius: 8px;
           color: var(--text-tertiary);
           transition: background 0.12s, color 0.12s, opacity 0.12s; }
.sec-btn:hover { background: var(--hover); color: var(--text-primary); }
.sec-btn .ms { font-size: 30px; }
.sec-cap { font-size: 10.5px; font-weight: 600; color: var(--text-tertiary);
           white-space: nowrap; line-height: 1; }
/* Unavailable, and saying so — a real disabled attribute, so it is also
   unreachable by keyboard. Kept legible rather than nearly invisible: it is
   the control a visitor needs the moment they filter anything. */
.sec-btn[disabled] { opacity: 0.34; cursor: default; }
.sec-btn[disabled]:hover { background: none; color: var(--text-tertiary); }
.sec-clear.is-on { color: var(--accent); opacity: 1; }
/* THIS section is the one being filtered. Stronger than .is-on, because .is-on
   only means "something on this page is filtered" and every section shows it —
   it cannot distinguish the one section that is the reason. Filled rather than
   tinted for the same reason the app fills .facet-chip.on: at a glance a tint
   reads as a hover, and this has to survive being scanned rather than looked
   at. Deliberately NOT the domain palette (--d100/--dtext): those name a
   PLACE, and this is a state that comes and goes. */
.sec-clear.is-section { background: var(--text-primary); color: #fff; opacity: 1; }
.sec-clear.is-section:hover { background: var(--text-primary); color: #fff; }
.sec-item:has(.sec-clear.is-section) .sec-cap { color: var(--text-primary); font-weight: 700; }

@media (max-width: 900px) {
  .sec-btn { width: 42px; height: 42px; }
  .sec-btn .ms { font-size: 25px; }
  .sec-cap { font-size: 10px; }
}
@media (max-width: 600px) {
  .sec-btn { width: 38px; height: 38px; }
  .sec-btn .ms { font-size: 22px; }
  /* Four words under four icons is more row than a 390px header has; the
     buttons keep their titles. */
  .sec-cap { display: none; }
  .sec-controls { gap: 2px; }
}

/* The panels a section can open, all wearing the colour bar's chrome so the
   three read as one family. */
#color-bar, #facet-bar, .options-panel { margin-top: 12px; }

/* ── More/Clear sit hard right, as one pair (#78) ──────────────────────────
   .facet-more and .facet-clear each carry margin-left:140px — alignment under
   the label COLUMN of the app's stacked layout. Under a section that is not
   indentation, it is a 140px shove into the middle of the row.

   They pair at the right edge instead, which is where they cannot collide with
   a variable number of chips, and where the control that reveals the rest sits
   beside the control that clears it. */
/* :not([hidden]) IS LOAD-BEARING — this was the "facets show up on top" bug.
   The app hides the bar with .facet-bar[hidden]{display:none} (0,2,0). An ID
   selector is (1,0,0) and wins, so declaring display here made the bar render
   even while the hidden attribute was set: it was visible on every page load,
   before anything had been clicked, and the Type button only ever MOVED an
   element that was already on screen.

   Guarding the selector keeps [hidden] authoritative rather than adding a
   second !important-style override to fight it — the attribute is the state,
   and CSS should not be able to disagree with it.

   Same shape as the palette footer printing "arrow_upward" and the section
   title's pink pill: a rule of mine winning a specificity contest at paint
   time, invisible to every test because the markup was correct. */
/* ── The facet bar reads like the colour bar above it ──────────────────────
   A heading on the left, the choices in the middle, the controls that reveal
   or clear them on the right. Grid rather than flex, so the three parts hold
   their columns whether one facet row is showing or twelve — with flex the
   tail would be pushed down the moment the middle column grew. */
#facet-bar:not([hidden]) {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  /* CENTRED while collapsed. With align-items:start the heading and the
     more/clear pair pinned to the top of their cells while the chip row —
     taller, because a chip has padding and a border — centred 4px lower. Every
     part sat on the same line and none of them lined up.

     Expanded the rule inverts: with a dozen rows in the middle column, centring
     the heading would float it halfway down the list, so it goes back to the
     top and takes the first row's padding to meet it. */
  align-items: center;
  column-gap: 14px;
}
#facet-bar.is-expanded { align-items: start; }
#facet-bar.is-expanded .facet-aside,
#facet-bar.is-expanded .facet-tail { padding-top: 7px; }
.facet-aside { font-size: 13px; font-weight: 600; color: var(--text-secondary);
               white-space: nowrap; line-height: 26px; }
.facet-rows { min-width: 0; }
/* Collapsed, the single row must not wrap — fitFacetChips() decides how many
   chips fit, and wrapping would let the ones it "hid" simply drop to a second
   line instead, making the measurement decorative. */
#facet-bar:not(.is-expanded) .facet-rows .facet-row { flex-wrap: nowrap; overflow: hidden; }
/* THE PER-ROW NAME GOES WHILE COLLAPSED. "Filter by Type:" on the left already
   names what the bar does, so the row's own heading was saying the same thing
   twice — and expensively: .facet-label is a FIXED 128px column sized for the
   stacked layout, which on one line is 128px of chips nobody gets to see. On a
   811px bar that is a quarter of the space, and it is why the fit was cutting
   at three chips where there was room for twice as many.

   Expanded it comes back, because there each row IS a different facet and the
   name is the only thing telling them apart. */
#facet-bar:not(.is-expanded) .facet-label { display: none; }
#facet-bar:not(.is-expanded) .facet-chips { flex-wrap: nowrap; }
.facet-chip-hidden { display: none !important; }

/* A hairline between facets, on the ROW so the last one can drop it rather
   than leaving a rule with nothing beneath it. */
#facet-bar.is-expanded .facet-row { padding: 7px 0;
                                    border-bottom: 1px solid var(--border); }
#facet-bar.is-expanded .facet-row:last-child { border-bottom: 0; }
/* NOTE: .facet-rest no longer exists in the markup — see the rule further
   down, kept only so an older cached page does not lose its layout. */
/* PINNED TO THE RIGHT EDGE, in both states. The two labels are different
   widths — "19 more ways to narrow" against "Show Fewer Options" — so anchoring
   the LEFT edge would make the control jump sideways every time it is pressed.
   Its right edge is the fixed point, which is also the edge the eye returns to
   because it is the last thing on the row. */
#facet-bar .facet-tail { display: flex; align-items: center; gap: 10px;
                         line-height: 26px; justify-self: end; }
#facet-bar .facet-tail .facet-more,
#facet-bar .facet-tail .facet-clear { margin: 0; }

/* ── The Options panel ─────────────────────────────────────────────────────
   A column of rows, not a wrapping row: wrapping is greedy and leaves the last
   line nearly empty, so the rows are computed and the groups distributed. */
.options-panel { --opt-ctl: 38px;
  display: flex; flex-direction: column; align-items: stretch; gap: 14px;
  padding: 10px 14px; background: var(--surface);
  border: 1px solid var(--border); border-radius: 14px; }
.options-panel[hidden] { display: none; }
.options-panel.is-compact { --opt-ctl: 32px; gap: 10px; }
/* Laid out so it can be measured, invisible so the unsolved layout is never
   painted. NOT display:none — that has no box, and measuring is exactly what
   has to happen before the first paint. */
.options-panel.is-preparing { visibility: hidden; }

.opt-row { display: flex; align-items: stretch; justify-content: space-between;
           gap: 18px; width: 100%; }
.options-panel.is-compact .opt-row { gap: 10px; }
/* The leftover width goes INTO the controls, not between them: space-between
   made the row look full while every target stayed 32px. Growing them turns the
   same slack into hit area. min-width holds a square floor, so a control can
   only get wider than it was, never narrower. */
.opt-row > .opt-group { flex: 1 1 auto; min-width: 0; align-self: stretch; }
.opt-group { display: flex; flex-direction: column; align-items: center; gap: 4px;
             justify-content: flex-start; }
/* The label is pushed to the bottom, and every group is the same height, so the
   captions land on one line without anything needing to know how tall a given
   control is. */
.opt-label { margin-top: auto; padding-top: 2px;
             font-size: 10.5px; font-weight: 600; letter-spacing: 0.02em;
             color: var(--text-tertiary); white-space: nowrap; text-align: center; }
.options-panel.is-compact .opt-label { font-size: 10px; }
/* Text size still governs the modal and the info pages while captions are off,
   so it dims rather than disappears. */
.opt-group.is-muted { opacity: 0.4; }

.options-panel .seg { width: 100%; }
.options-panel .seg button { flex: 1 1 auto; width: auto;
                             min-width: var(--opt-ctl); height: var(--opt-ctl); }
/* The .seg TRACK is 8px taller than the button inside it (3px padding + 1px
   border each side), so the bare controls take the track's OUTER height and a
   segmented group and a single button present the same box. */
.opt-act { display: inline-flex; align-items: center; justify-content: center;
           width: calc(var(--opt-ctl) + 8px); height: calc(var(--opt-ctl) + 8px);
           border: 1px solid var(--border); border-radius: 9px;
           background: var(--surface); cursor: pointer; color: var(--text-tertiary); }
.opt-act:hover { background: var(--hover); color: var(--text-primary); }
/* ACTIVATED, not domain-coloured: --d100 names a PLACE, so a toggle wearing it
   says the wrong thing and a different wrong thing in every domain. This is the
   app's own on-state idiom, from .facet-chip.on. */
.opt-act.is-on { background: var(--text-primary); border-color: var(--text-primary);
                 color: #fff; }
.opt-act.is-on:hover { background: var(--text-primary); color: #fff; }
/* 28px — Nepo, 2026-08-06. These five are the only controls in the panel whose
   whole meaning is the glyph: the segmented groups draw their own swatches and
   the presets are words. Set on the GLYPH, not the button, so compact mode
   still buys its width by shrinking the box while the thing that carries the
   meaning holds its size. 28px still sits inside the 32px compact box. */
.opt-act .ms { font-size: 28px; }

.presets { display: flex; gap: 4px; width: 100%; }
.options-panel.is-compact .presets { gap: 3px; }
.preset { flex: 1 1 auto; min-width: 0; height: calc(var(--opt-ctl) + 8px);
          display: inline-flex; align-items: center; justify-content: center;
          text-align: center; padding: 0 13px;
          border: 1px solid var(--border); border-radius: 9px;
          background: var(--surface); cursor: pointer;
          font-family: var(--font); font-size: 12.5px;
          color: var(--text-secondary); white-space: nowrap; }
.options-panel.is-compact .preset { padding: 0 9px; font-size: 12px; }
.preset:hover { background: var(--hover); }
.preset.is-on { background: var(--text-primary); border-color: var(--text-primary);
                color: #fff; font-weight: 700; }
.preset.is-on:hover { background: var(--text-primary); }

/* Lock and Reset govern the panel rather than setting an option, so they sit
   apart from the settings they protect. A locked panel dims as ONE thing —
   dimming each control separately reads as five broken buttons. */
.opt-tail { display: contents; }
.options-panel.is-locked .opt-group { opacity: 0.42; }
/* THE LOCK STAYS BRIGHT — it is the way out, and a control that dims itself
   the moment it becomes the only usable one is a trap.

   This targeted .opt-tail .opt-group:first-child, which was true of the markup
   and false of the page: balanceOptions() moves every .opt-group into a row
   container to lay the panel out, so .opt-tail is emptied and the rule matched
   nothing. A selector that depends on a position the layout is free to change
   is a rule that works until the first time it matters. Named now. */
.options-panel.is-locked .opt-group.is-lock { opacity: 1; }
.options-panel.is-locked button:not([data-act="lock"]) { cursor: default; }

/* A fourth card size and a fourth reading step. 9 / 4 / 2 / 1 cells is a strict
   ramp where every glyph IS the density it produces — the rule these buttons
   already follow — so no glyph needs a symbol to explain it. */
.seg-sz.is-l2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; }
/* The scene background is the only option that is a picture rather than a
   colour. Grey, matching the card-size cells, so it does not read as a fourth
   colour beside the three swatches. */
.seg-scene { font-size: 28px; color: #8a8078; line-height: 1; }
.options-panel .seg-sz i { background: #8a8078; }
.options-panel .seg-sw { width: 24px; height: 24px; border-color: rgba(0,0,0,0.28); }
.options-panel.is-compact .seg-sw { width: 20px; height: 20px; }
.options-panel .seg button[aria-checked="true"] { box-shadow: 0 0 0 2px var(--accent); }

/* Measured OUT of the rows and OUT of the column: the partition asks how wide
   each group WANTS to be. With grow live it would read the width the previous
   layout gave it, and on the FIRST open — before any row exists — the panel is
   a column with align-items:stretch, so every group measures the full width and
   each one "needs" its own line. */
.options-panel.is-measuring { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
.options-panel.is-measuring .opt-row { display: block; width: auto; }
.options-panel.is-measuring .opt-group { flex: 0 0 auto !important;
                                         align-self: flex-start !important;
                                         display: inline-flex; }
.options-panel.is-measuring .seg,
.options-panel.is-measuring .presets { width: auto !important; }
.options-panel.is-measuring .seg button { flex: 0 0 auto !important;
                                          width: var(--opt-ctl) !important; }
.options-panel.is-measuring .preset { flex: 0 0 auto !important; }

/* あかちゃん's wordless grid. display:none, not visibility:hidden — the caption
   reserves a fixed height from --label-font, so hiding the text alone would
   leave ~54px of empty card under every illustration. */
.no-card-labels .ill-label { display: none; }

/* ── The header as a filter (#35) ─────────────────────────────────────────
   The title, the count and a filter glyph, wrapped in one button. It was inert
   — the most obvious thing on the page to click did nothing, while the real
   control hid in a hamburger.

   Unstyled as a button on purpose: no chrome at rest, so the grid still reads
   as headings with rules under them rather than a stack of five buttons. The
   affordance arrives on hover and focus, which is where someone is asking. */
.cls-section-toggle {
  display: flex; align-items: baseline; gap: 10px;
  width: 100%; padding: 0 6px 0 0;
  background: none; border: none; cursor: pointer;
  font-family: var(--font); text-align: left;
  color: inherit;
  border-radius: 8px;
  transition: opacity 0.12s;
}
.cls-section-toggle:hover .cls-section-title,
.cls-section-toggle:focus-visible .cls-section-title { color: var(--text-secondary); }

/* The glyph that says a header is clickable. Hidden until hover or focus, so
   five of them do not sit down the page competing with the titles. */
.cls-section-hint {
  margin-left: auto;
  font-size: 20px;
  color: var(--text-tertiary);
  opacity: 0;
  transition: opacity 0.12s;
}
.cls-section-toggle:hover .cls-section-hint,
.cls-section-toggle:focus-visible .cls-section-hint { opacity: 1; }

/* The chosen one stays lit… */
.cls-section.is-filtered .cls-section-title { color: var(--text-primary); }
.cls-section.is-filtered .cls-section-hint { opacity: 1; color: var(--text-primary); }

/* …and while a filter is on, the others recede. Only the headers dim — their
   grids are already empty, so dimming the section would say nothing. This is
   what makes one active filter read as a CHOICE rather than as "the rest
   happen to have no illustrations". */
/* DIMS THE WORDS, NOT THE HEADER (#78). This put opacity on the whole
   .cls-section-header, which was harmless while the header held only a title
   and a count — and became a bug the moment the controls moved into it: every
   other section's five buttons went to 45% and read as disabled, when they are
   perfectly usable and are how you get back.

   The title and count still recede, which is the whole point of the rule: the
   section you chose reads as a selection rather than as the only one with
   content. */
.ill-grid-wrap.is-filtering .cls-section:not(.is-filtered) .cls-section-title,
.ill-grid-wrap.is-filtering .cls-section:not(.is-filtered) .cls-section-count { opacity: 0.45; }
.ill-grid-wrap.is-filtering .cls-section:not(.is-filtered) .cls-section-header:hover
  .cls-section-title { opacity: 1; }

/* ── The relocated chrome (#35) ───────────────────────────────────────────
   One switcher and one palette button for the whole grid, in the head beside
   the display controls, instead of a copy inside every section header. */
.grid-filters {
  display: flex; align-items: center; gap: 4px;
  position: relative;
  margin-left: auto;
}
.grid-filters .section-hamburger-btn,
.grid-filters .color-picker-btn {
  width: 38px; height: 38px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  border-radius: 9px; color: var(--text-tertiary);
  transition: background 0.12s, color 0.12s;
}
.grid-filters .section-hamburger-btn .ms,
.grid-filters .color-picker-btn .ms { font-size: 22px; line-height: 1; }
.grid-filters .section-hamburger-btn:hover,
.grid-filters .color-picker-btn:hover { background: var(--hover); color: var(--text-primary); }
/* WEIGHT 400, NOT 300, AND THIS IS NOT A DESIGN CHANGE (2026-08-05).
   Both of these declared 300 and both have always PAINTED at 400: the Google
   Fonts request loads M PLUS Rounded 1c at 400/500/700/800 only, and CSS font
   matching for a target below 400 looks downward first, finds nothing, then
   takes the next weight up. So 300 described a weight the page could not use.
   Writing 400 changes no pixel while the webfont is present, and makes the two
   agree in the one case where they did not: if the webfont fails and a system
   fallback DOES carry a 300, the old declaration rendered genuinely lighter
   than every other title on the page. Do not restore 300 to "lighten" this —
   there is nothing lighter to reach, and opacity is the lever that works. */
.cls-section-title {
  font-size: 38px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--text-tertiary);
  margin: 0;
  line-height: 1;
}
/* Count beside the section title, in parentheses. Same size and weight as the
   title, set back by opacity rather than a lighter weight — see the note above
   for why a lighter weight is not available. Opacity also tracks
   --text-tertiary if that token ever moves. Scales with the title at both
   breakpoints below. */
.cls-section-count {
  font-size: 38px;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--text-tertiary);
  opacity: 0.5;
  flex-shrink: 0;
  white-space: nowrap;
}
.cls-section-count:empty { display: none; }
/* A section filtered to nothing keeps its header; fade its (0) so the eye still
   skips past it to the sections that do have results. */
.cls-section-count.is-zero { opacity: 0.3; }
.cls-section-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(180px * var(--card-scale)), 1fr));
  gap: 10px;
  margin-top: 18px;
  margin-bottom: 48px;
}

@media (max-width: 900px) {
  .cls-section-title { font-size: 28px; }
  .cls-section-count { font-size: 28px; }
}
@media (max-width: 1024px) {
  /* 3-column grid — placed after base styles so it wins the cascade */
  .ill-grid, .cls-section-grid { grid-template-columns: repeat(var(--card-cols-3), 1fr); gap: 6px; }
  .ill-label { padding: 4px 5px 5px; }
  .ill-label-text, .ill-label-base, .ill-label-qual { font-size: var(--label-font-sm, 11px); }
  /* Category header: shrink image on mobile */
  .domain-exp-cat-img { width: 72px; height: 72px; }
  .domain-exp-cat-title { font-size: 20px; }
  /* Compact breadcrumb on mobile: ancestors small, current stays bold */
  .page-nav-home  { font-size: 13px; }
  .page-nav-sep   { font-size: 13px; }
  .page-nav-ancestor { font-size: 13px; }
  /* Current title smaller on mobile (wrap handles row layout) */
  .page-nav-current { font-size: 22px; }
  /* Language flag moves to Menu dropdown on mobile */
  .lang-flag-btn { display: none; }
}
@media (max-width: 600px) {
  .cls-section-title { font-size: 22px; }
  .cls-section-count { font-size: 22px; }
}

/* ═══════════════════════════════════════════
   DOMAIN COLOUR SYSTEM
   Each domain defines --d100/80/60 (pastel tints)
   and --dtext (darkened hue for contrast text).
   Used by nav ancestor/active states and card tints.
   ═══════════════════════════════════════════ */

/* ── Per-domain CSS variables ── */
[data-domain="people"]             { --d100:hsl(345,62%,76%); --d80:hsl(345,62%,84%); --d60:hsl(345,62%,91%); --dtext:hsl(345,67%,27%); }
[data-domain="sports-recreation"]  { --d100:hsl(5,65%,77%);   --d80:hsl(5,65%,85%);   --d60:hsl(5,65%,92%);   --dtext:hsl(5,70%,27%);   }
[data-domain="food"]               { --d100:hsl(26,68%,78%);  --d80:hsl(26,68%,86%);  --d60:hsl(26,68%,92%);  --dtext:hsl(26,73%,27%);  }
[data-domain="home-daily-life"]    { --d100:hsl(14,60%,80%);  --d80:hsl(14,60%,87%);  --d60:hsl(14,60%,93%);  --dtext:hsl(14,65%,27%);  }
[data-domain="animals"]            { --d100:hsl(32,55%,78%);  --d80:hsl(32,55%,86%);  --d60:hsl(32,55%,92%);  --dtext:hsl(32,60%,27%);  }
[data-domain="history-traditions"] { --d100:hsl(44,58%,77%);  --d80:hsl(44,58%,85%);  --d60:hsl(44,58%,91%);  --dtext:hsl(44,63%,27%);  }
[data-domain="design-elements"]    { --d100:hsl(56,68%,76%);  --d80:hsl(56,68%,83%);  --d60:hsl(56,68%,90%);  --dtext:hsl(56,73%,24%);  }
[data-domain="health-science"]     { --d100:hsl(92,48%,77%);  --d80:hsl(92,48%,85%);  --d60:hsl(92,48%,92%);  --dtext:hsl(92,53%,26%);  }
[data-domain="plants-nature"]      { --d100:hsl(128,42%,76%); --d80:hsl(128,42%,84%); --d60:hsl(128,42%,91%); --dtext:hsl(128,47%,27%); }
[data-domain="science-technology"] { --d100:hsl(183,52%,75%); --d80:hsl(183,52%,83%); --d60:hsl(183,52%,91%); --dtext:hsl(183,57%,25%); }
[data-domain="school-learning"]    { --d100:hsl(205,62%,76%); --d80:hsl(205,62%,84%); --d60:hsl(205,62%,91%); --dtext:hsl(205,67%,27%); }
[data-domain="current-events"]     { --d100:hsl(213,30%,75%); --d80:hsl(213,30%,83%); --d60:hsl(213,30%,91%); --dtext:hsl(213,35%,27%); }
[data-domain="work-society"]       { --d100:hsl(228,46%,78%); --d80:hsl(228,46%,86%); --d60:hsl(228,46%,92%); --dtext:hsl(228,51%,27%); }
[data-domain="arts-culture"]       { --d100:hsl(268,52%,78%); --d80:hsl(268,52%,86%); --d60:hsl(268,52%,92%); --dtext:hsl(268,57%,27%); }
[data-domain="world-geography"]    { --d100:hsl(168,48%,73%); --d80:hsl(168,48%,83%); --d60:hsl(168,48%,90%); --dtext:hsl(168,56%,24%); }
[data-domain="pop-culture"]        { --d100:hsl(308,56%,78%); --d80:hsl(308,56%,86%); --d60:hsl(308,56%,92%); --dtext:hsl(308,61%,27%); }

/* ── Card image-side tints (light fill behind illustration thumbnail) ── */
.tint-people            { background: linear-gradient(145deg, hsl(345,62%,97%), hsl(345,62%,91%)); }
.tint-sports-recreation { background: linear-gradient(145deg, hsl(5,65%,97%),   hsl(5,65%,91%));   }
.tint-food              { background: linear-gradient(145deg, hsl(26,68%,97%),  hsl(26,68%,91%));  }
.tint-home-daily-life   { background: linear-gradient(145deg, hsl(14,60%,97%),  hsl(14,60%,91%));  }
.tint-animals           { background: linear-gradient(145deg, hsl(32,55%,97%),  hsl(32,55%,91%));  }
.tint-history-traditions{ background: linear-gradient(145deg, hsl(44,58%,97%),  hsl(44,58%,91%));  }
.tint-design-elements   { background: linear-gradient(145deg, hsl(56,68%,97%),  hsl(56,68%,90%));  }
.tint-plants-nature     { background: linear-gradient(145deg, hsl(128,42%,97%), hsl(128,42%,91%)); }
.tint-science-technology{ background: linear-gradient(145deg, hsl(183,52%,97%), hsl(183,52%,91%)); }
.tint-school-learning   { background: linear-gradient(145deg, hsl(205,62%,97%), hsl(205,62%,91%)); }
.tint-current-events    { background: linear-gradient(145deg, hsl(213,30%,97%), hsl(213,30%,91%)); }
.tint-work-society      { background: linear-gradient(145deg, hsl(228,46%,97%), hsl(228,46%,91%)); }
.tint-arts-culture      { background: linear-gradient(145deg, hsl(268,52%,97%), hsl(268,52%,91%)); }
.tint-world-geography   { background: linear-gradient(145deg, hsl(168,48%,97%), hsl(168,48%,91%)); }
.tint-pop-culture       { background: linear-gradient(145deg, hsl(308,56%,97%), hsl(308,56%,91%)); }

/* ═══════════════════════════════════════════
   MODAL — HORIZONTAL (v2.0, #76 — replaced the flip card 2026-08-05)
   ═══════════════════════════════════════════
   Part names are Guidelines/14 → Anatomy → "Modal, horizontal", and the class
   names ARE those terms. The flip is gone: there is no second face, no
   perspective, no backface-visibility. The back face became the Details panel,
   which is a SIBLING of the words rather than the reverse of everything. */
.modal-overlay {
  display: none;
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 900;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.modal-overlay.open { display: flex; }

/* THE MODAL ITSELF. 16:9 at size M — the shape the prototype settled on by
   measuring how much of the catalogue fits, not by taste — with the
   illustration and the words taking half the width each. Fixed rather than
   fluid: a modal that resizes to its content makes every illustration a
   different shape, and the set bar and the buttons move under the cursor
   between one illustration and the next. */
/* 0.5, not #76's 0.4 — Nepô's call of 2026-08-07, chosen in the playground
   against 40/60 and 45/55 with all three one click apart. It is ONE token and
   both columns read it (.head-ill and .ill-col), so the header cannot drift
   away from the artwork it sits over; that is why the split is a variable and
   not two widths.

   It also settles the artwork box: SQUARE cannot survive this split. At 50/50
   the left column is 559px wide and ~474px tall once the 156px set bar is in,
   so a strict 1/1 asks for 559px of a 474px space — measured in the playground,
   where clamping it with max-height:100% silently failed because a flex item's
   percentage max-height resolves against the flex CONTAINER (630px), not the
   room left by its siblings. The box stays Fills. */
.modal-scene {
  --mw: min(1120px, 94vw);
  --mh: min(630px, 86vh);
  --leftw: calc(var(--mw) * 0.5);
  /* The shared height floor of the Set bar and the Action row. Each is the last
     thing in a full-height column, so equal heights put their two top rules on
     ONE line across the modal — by construction, not by tuning padding on each
     side until it happens to line up today. */
  /* 156, not the prototype's 128, and the difference is the app's own carousel:
     its items are 76px wide and CAPTIONED — the prototype's were bare 52px
     thumbnails. Measured rather than guessed: the set bar's natural height with
     real members is 153px, so a 128px floor left it 25px taller than the action
     row and the "one line across the modal" was two lines 25px apart. The floor
     has to clear the TALLER of the two, or it is not a floor. */
  --bottom-row: 156px;
  width: var(--mw);
  height: var(--mh);
  max-width: none;
  position: relative;
  display: flex;
  overflow: hidden;
  background: var(--surface);
  border-radius: 24px;
  box-shadow: 0 30px 80px rgba(0,0,0,0.3);
  animation: slideUp 0.25s cubic-bezier(0.34,1.56,0.64,1);
}
@keyframes slideUp {
  from { opacity: 0; transform: translateY(20px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .modal-scene { animation: none; } }

/* ── Header: the controls that change how the WHOLE modal reads ── */
.modal-head {
  position: absolute; top: 0; left: 0; right: 0; z-index: 5;
  height: 52px; display: flex; align-items: center; pointer-events: none;
}
.modal-head > * { pointer-events: auto; }
/* The illustration's own two corners: close at the left, backgrounds at the
   right. In flow as the two ends of a space-between row — an absolute offset
   would need re-tuning every time --leftw changed. */
/* flex-start, NOT center: .modal-head is 52px tall and the background switch is
   now a COLUMN about 100px high, which centred would hang 24px above the
   modal's rounded top edge. Both children hang from the top instead — the head
   is absolutely positioned over the artwork, so the column simply continues
   down the illustration's right margin. */
/* align-SELF, not align-items, and that distinction is the whole fix. Setting
   align-items only governs this element's CHILDREN; .modal-head still centred
   .head-ill itself inside its 52px, so a 103px column of swatches started at
   -30px and hung above the modal's rounded top edge. Measured: the switch's top
   was outside .modal-scene's box. align-self hangs it from the top instead.
   .head-text keeps .modal-head's centring, which is right for a 30px row. */
.head-ill  { flex: 0 0 var(--leftw); display: flex; align-self: flex-start;
             align-items: flex-start;
             justify-content: space-between; padding: 10px 14px 0 12px; }
.head-text { flex: 1; display: flex; align-items: center; gap: 8px; padding-left: 30px; }

.close-btn {
  width: 32px; height: 32px; border-radius: 50%; flex: 0 0 auto;
  background: rgba(255,255,255,0.9); border: 1px solid var(--border);
  color: var(--text-primary); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font);
}
.close-btn:hover { border-color: var(--text-tertiary); }
.close-btn .ms { font-size: 18px; line-height: 1; }

/* Background switch — three swatches over the illustration. */
/* ── THE MODAL'S DISCLOSURE MENU ───────────────────────────────────────────
   One button showing the current value, opening the alternatives. Shared,
   because the header holds several of these and they have to look and behave
   as one family; the background chooser is the first to use it. Replaces the
   open row of swatches, which reached down the artwork and read as decoration
   ON the illustration rather than as a control OVER it. */
.modal-menu { position: relative; }
.modal-menu-btn {
  /* 30px, matching every other control in this header — Copy Text, Random and
     the playground's three menus. It had no height at all, so it took the one
     its 14px swatch plus padding happened to give it (24px) and sat visibly
     short of its neighbours. A control's height should come from the row it is
     in, not from whatever it contains. */
  display: inline-flex; align-items: center; gap: 1px; cursor: pointer;
  height: 30px; padding: 0 4px 0 7px; border-radius: 15px; line-height: 1;
  border: 1px solid var(--border); background: rgba(255,255,255,0.92);
  color: var(--text-secondary); font-family: var(--font);
}
.modal-menu-btn:hover { border-color: var(--text-tertiary); }
.modal-menu-caret { font-size: 16px; margin-left: -2px; color: var(--text-tertiary); }
.modal-menu-list {
  position: absolute; top: 38px; right: 0; z-index: 9; display: none;
  flex-direction: column; gap: 1px; min-width: 150px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 11px; padding: 5px;
  box-shadow: 0 10px 26px rgba(0,0,0,0.18);
}
.modal-menu-list.open { display: flex; }
.modal-menu-list button {
  display: flex; align-items: center; gap: 8px; width: 100%;
  font-family: var(--font); font-size: calc(12px * var(--text-scale));
  text-align: left; padding: 5px 8px; border-radius: 7px;
  border: 1px solid transparent; background: none; cursor: pointer;
  color: var(--text-secondary); white-space: nowrap;
}
.modal-menu-list button:hover { background: var(--surface2, #f5f2ed); }
.modal-menu-list button.on { background: var(--hub-light); color: var(--hub); font-weight: 700; }
.bg-swatch { width: 14px; height: 14px; border-radius: 4px;
             border: 1px solid rgba(0,0,0,.18); display: block; }
.bg-swatch.bg-default { background: #f5f4f2; }
.bg-swatch.bg-dark    { background: hsl(35,6%,18%); }
/* Scene is an ICON rather than a swatch (2026-08-07) — the same `landscape`
   glyph the grid's control uses. Sized to sit on the swatches' baseline so a
   menu row does not jump when the marks differ. */
.bg-scene-mark { font-size: 16px; line-height: 1; color: var(--text-secondary);
                 width: 16px; display: block; }
.bg-swatch.bg-checker { background:
  linear-gradient(45deg,#ddd 25%,transparent 25%,transparent 75%,#ddd 75%) 0 0/8px 8px,
  linear-gradient(45deg,#ddd 25%,transparent 25%,transparent 75%,#ddd 75%) 4px 4px/8px 8px, #fff; }

/* ── The four header controls (#81) ──────────────────────────────────────────
   Font size · language · こども · Random. Each of the first three is ONE button
   showing its current value, which drops down the alternatives — three flags, a
   こども button and a pair of chevrons side by side came to 333px of a 350px
   phone, measured 2026-08-07. The trigger showing the STATE rather than the
   name of the control is what makes one button enough.

   They reuse .modal-menu-btn wholesale, so all six controls in this header are
   the same 30px object. What follows is only what differs. */

/* The background chooser sits at the artwork's right, so its list opens to the
   right edge; these sit at the left of .head-text and would otherwise hang off
   toward the middle of the words. */
.head-text .modal-menu-list { left: 0; right: auto; }
/* On the phone the row moves to .ill-foot, the artwork's BOTTOM right — a list
   opening downward from there would be off the modal entirely. */
.ill-foot .modal-menu-list { top: auto; bottom: 38px; right: 0; left: auto; }

/* Disabled means こども on a card that is not in Japanese: readingActive() is
   `lang === 'jp' && readLevel !== 'off'`, so there is nothing for it to do. A
   fact about the card, not a failure — so it recedes rather than warns. */
.modal-menu-btn:disabled { opacity: .35; cursor: default; }
.modal-menu-btn:disabled:hover { border-color: var(--border); }

/* Irasutoya's own flag illustration, as the top bar uses. 20px inside a 30px
   control rather than the old 30px inside 36px: it is a menu trigger now, one
   of a row of four, not a lone button. */
.modal-flagimg { width: 20px; height: 20px; object-fit: contain;
                 display: block; pointer-events: none; }
.modal-menu-list .modal-flagimg { width: 22px; height: 22px; }

/* こども reads as a word because that is its shipped name (read.title) and the
   thing it changes is words. `.on` when a reading level is actually applied —
   the trigger has to say which state it is in, or the single button is just an
   icon again. */
.modal-kd { font-size: 11px; font-weight: 700; letter-spacing: .02em;
            line-height: 1; color: var(--text-tertiary); }
.modal-kd.on { color: var(--hub); }

/* The A that shows the size each row would give. Fixed width so the labels
   beside them line up whatever the letter's size. */
.modal-menu-a { font-weight: 700; width: 20px; display: inline-block;
                text-align: center; flex: 0 0 auto; }

/* Random is the one that carries a word — `shuffle` is a metaphor, where a
   chevron is a direction. Same 30px as its neighbours, a pill rather than a
   circle because it holds text. */
.modal-rand { gap: 5px; padding: 0 11px; font-size: 11.5px; font-weight: 700; }
.modal-rand .ms { font-size: 16px; }
.ill-foot { position: absolute; right: 12px; bottom: 12px; z-index: 6;
            display: none; align-items: center; gap: 8px; }

/* ── The illustration column ── */
.ill-col { flex: 0 0 var(--leftw); min-width: 0; display: flex; flex-direction: column;
           border-right: 1px solid var(--border); background: var(--surface2, #f5f2ed); }
/* --art-pad is a TOKEN because two things read it: this padding, and the
   skeleton's inset (#82), which has to occupy the space the illustration will
   rather than the margin around it. Written as a literal in both places it was
   already one edit away from a skeleton that did not line up with the picture
   it stands in for. */
.artwork { --art-pad: 26px;
           flex: 1; min-height: 0; position: relative;
           display: flex; align-items: center; justify-content: center;
           padding: var(--art-pad) var(--art-pad) 0; }
.artwork[data-bg="default"] { background: #f5f4f2; }
.artwork[data-bg="dark"]    { background: hsl(35,6%,18%); }
.artwork[data-bg="checker"] { background:
  linear-gradient(45deg,hsl(35,8%,90%) 25%,transparent 25%,transparent 75%,hsl(35,8%,90%) 75%) 0 0/16px 16px,
  linear-gradient(45deg,hsl(35,8%,90%) 25%,transparent 25%,transparent 75%,hsl(35,8%,90%) 75%) 8px 8px/16px 16px, #fff; }
/* SCENE — a real illustration behind the subject (#81). The image itself is set
   inline by renderBgSwitch(), because it is chosen at runtime; everything about
   HOW it sits belongs here. `cover` and not `contain`: this is a backdrop, so
   filling the box matters more than showing all of it. */
.artwork[data-bg="scene"] { background-color: #f5f4f2;
  background-size: cover; background-position: center; background-repeat: no-repeat; }

/* ── The text column ── */
.text-col { flex: 1; display: flex; flex-direction: column; min-width: 0; }
/* Everything the Details panel MAY cover. The action row is this element's
   SIBLING and so is unreachable by it, at any button-row height. */
.panel-bounds { flex: 1; position: relative; display: flex; flex-direction: column; min-height: 0; }
/* `safe` is load-bearing: plain `center` also centres content taller than the
   box, pushing the first line of a long description above the scroll origin
   where it can never be reached. The 56/8 padding puts a short block in the
   middle of the area BELOW the header rather than of the whole column. */
.reading { flex: 1; overflow-y: auto; padding: 56px 30px 8px;
           display: flex; flex-direction: column; justify-content: safe center; }
.reading > :last-child { margin-bottom: 0; }

/* max-height was 300px, sized for the flip card's fixed image band. In a
   448px column that capped the illustration at 300px however much room it had
   — the exact defect the high-resolution tier exists to remove, reintroduced
   one line lower. It fills the artwork now. */
.modal-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
}
/* NO THUMBNAIL STAND-IN (#82). Until 2026-08-07 the modal painted the 200px
   thumbnail from the grid's cache and swapped the master in behind it. Nepô's
   call: show only the full-resolution image. The swap was visible, and what it
   showed first was a 200px picture in a 559px column.

   Deleting the stand-in leaves the question of what fills the column for the
   ~29 KB it takes the master to arrive, and that is the SKELETON. His choice of
   2026-08-07, against the illustration's own dominant colour, a pixelated
   thumbnail, a mosaic and holding the modal shut: a neutral shimmer claims
   nothing about a drawing it has not seen yet. The colour placeholder was the
   close second and its flaw is exactly that — `enrich/palette.js` knows the
   dominant colour, but stating it before the picture arrives makes the modal
   assert something it may then contradict.

   The image FADES IN rather than cutting, so the skeleton and the drawing are
   one event. `opacity` and not `filter` here: there is nothing underneath to
   show through, which was the reason the old swap could not use it. */
.modal-img { opacity: 1; transition: opacity .2s ease-out; }
.modal-img.is-loading { opacity: 0; transition: none; }

/* The skeleton itself. A single sweep left to right over a flat base — it says
   "loading" by MOVING, so a still frame of it is not mistaken for a drawing
   with a pale band in it. It sits inside .artwork's padding box rather than
   filling the element, so it occupies the space the illustration will, not the
   26px margin around it. */
.artwork-skeleton {
  position: absolute; inset: var(--art-pad) var(--art-pad) 0; border-radius: 12px;
  background: var(--skel-base, hsl(35,8%,90%));
  overflow: hidden; pointer-events: none;
}
.artwork-skeleton::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 20%, var(--skel-sheen, hsla(0,0%,100%,0.72)) 50%, transparent 80%);
  transform: translateX(-100%);
  animation: skelSweep 1.15s ease-in-out infinite;
}
@keyframes skelSweep { to { transform: translateX(100%); } }
/* On the dark and checker backgrounds a near-white skeleton is a hole in the
   modal rather than a placeholder. Both tokens move together — the sheen has to
   stay lighter than the base or the sweep runs backwards as a dark band. */
.artwork[data-bg="dark"] { --skel-base: hsl(35,6%,26%); --skel-sheen: hsla(0,0%,100%,0.16); }
.artwork[data-bg="checker"],
.artwork[data-bg="scene"] { --skel-base: hsla(35,8%,88%,0.92); }
@media (prefers-reduced-motion: reduce) {
  .modal-img { transition: none; }
  /* The sweep goes; the base stays. Removing the skeleton outright would put
     an empty column back, which is the thing this exists to prevent. */
  .artwork-skeleton::after { animation: none; opacity: .5; transform: none; }
}
.modal-close {
  position: absolute;
  top: 12px; right: 12px;
  width: 32px; height: 32px;
  border-radius: 50%;
  background: rgba(0,0,0,0.26);
  border: none; cursor: pointer;
  color: white; font-size: 16px;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
  font-family: var(--font); z-index: 2;
}
.modal-close:hover { background: rgba(0,0,0,0.5); }

/* .modal-scroll-body and .modal-body are GONE — the Reading area replaced both
   (see the shell above). Left-aligned now rather than centred: a centred
   paragraph is readable as one line and tiring as four, and the 3/5 column
   gives the description real width for the first time. */
.modal-title {
  font-size: calc(18px * var(--text-scale)); font-weight: 700;
  letter-spacing: -0.3px; line-height: 1.35; margin-bottom: 3px;
}
.modal-desc {
  font-size: calc(13.5px * var(--text-scale)); line-height: 1.65;
  color: var(--text-secondary); margin-bottom: 14px;
}
/* In こども (#69) the description matches the TITLE's size — 18px, not 13.5px.
   The usual hierarchy exists because an adult skims the title and reads the
   description only if they want it. A child reading kana is doing the opposite:
   the description is the longest continuous text on the screen and the hardest
   thing there, so it is the last place to shrink the type. Line-height opens
   too, because 分かち書き spacing already widens the line and cramped leading
   would undo it. Only applied when kanaHTML actually rendered — openModal sets
   the class from the same condition that fills the element. */
.modal-desc.is-kana { font-size: calc(18px * var(--text-scale)); line-height: 1.75; }
/* THE DESCRIPTION WHILE THE DETAIL TIER IS STILL LOADING (2026-08-15). The
   modal can open before descriptions.js has landed — the grid no longer waits
   for it — and an empty paragraph reads as an illustration with nothing to
   say. Two pale lines with the artwork skeleton's own sweep say "loading"
   instead; openModalInner sets the class only when the description is empty
   AND the tier is not here, and _onPhase2bReady() re-renders the card. */
.modal-desc.is-pending { position: relative; min-height: 2.6em; }
.modal-desc.is-pending::before,
.modal-desc.is-pending::after {
  content: ''; position: absolute; left: 0; height: .9em; border-radius: 6px;
  background: linear-gradient(100deg,
    var(--skel-base, hsl(35,8%,90%)) 20%, var(--skel-sheen, hsla(0,0%,100%,0.72)) 50%,
    var(--skel-base, hsl(35,8%,90%)) 80%);
  background-size: 200% 100%;
  animation: skelSlide 1.15s ease-in-out infinite;
}
.modal-desc.is-pending::before { top: .2em; width: 92%; }
.modal-desc.is-pending::after  { top: 1.55em; width: 61%; }
@keyframes skelSlide { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) {
  .modal-desc.is-pending::before, .modal-desc.is-pending::after { animation: none; }
}

/* ── THE DRAWN TEXT AND ITS READING (#86) ──
   A reading aid for a visitor in English or Portuguese who is standing in front
   of the real sign: the Japanese as drawn, and how to say it.

   THE JAPANESE IS THE LARGER OF THE TWO, and that is the whole layout decision.
   The romaji tells you how it sounds, but the kanji is what the reader has to
   MATCH against the thing in front of them, so it cannot be the smaller line.

   Sized in the same calc(px * var(--text-scale)) as everything else in the
   modal, so it follows the modal's own text-size control — which is scoped to
   .modal-scene and persists separately from the platform's (Nepô, 2026-08-07:
   language and こども are about this card, size is about your eyes). */
.modal-reading {
  display: flex; flex-direction: column; gap: 2px;
  margin: -4px 0 14px;
  padding: 8px 10px;
  /* --hub and --hub-light, the pair the app already uses for a tinted panel
     (.modal-menu-list button.on, .carousel-item img). Written as real tokens
     rather than a literal with a fallback: --surface-2 does not exist in this
     stylesheet — the token is --surface2 — so the first version of this rule
     silently painted a hardcoded rgba() that no theme could ever reach. */
  border-left: 2px solid var(--hub);
  background: var(--hub-light);
  border-radius: 0 6px 6px 0;
}
.modal-reading[hidden] { display: none; }   /* or flex beats the attribute */
.modal-reading .reading-label {
  font-size: calc(10px * var(--text-scale));
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-tertiary);
}
.modal-reading .reading-ja {
  font-size: calc(16px * var(--text-scale)); line-height: 1.45;
  color: var(--text-primary);
}
.modal-reading .reading-romaji {
  font-size: calc(13px * var(--text-scale)); line-height: 1.4;
  color: var(--text-secondary); font-style: italic;
}

/* Metadata collapsible */
/* File details — gray bar below the action buttons */
.modal-meta { width: 100%; margin-top: 8px; }
.modal-details-flip-btn {
  display: flex; align-items: center; justify-content: center;
  color: var(--text-secondary); background: var(--surface2);
  border: 1px solid var(--border); font-family: var(--font);
  cursor: pointer; transition: background 0.15s;
}
.modal-details-flip-btn:hover { background: #e2e2e0; color: var(--text-primary); }
.modal-meta-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px 20px;
  margin-top: 10px;
  /* No left padding: the grid now sits flush with the "File Info" label above
     it, and the full width is available to a row that spans both columns. */
  padding: 12px 0 4px;
  text-align: left;
}
.modal-meta-col { display: flex; flex-direction: column; gap: 10px; }
.modal-meta-item { display: flex; flex-direction: column; gap: 2px; }
.modal-meta-label {
  font-size: calc(12px * var(--text-scale)); font-weight: 400;
  letter-spacing: 0.1px;
  color: var(--text-tertiary);
}
.modal-meta-value {
  font-size: calc(14px * var(--text-scale)); color: var(--text-secondary);
  word-break: break-all; line-height: 1.35;
}
/* A row that spans both columns. The derived link is a full illustration title
   and wraps awkwardly inside a half-width column; given the whole grid it fits
   on one line. break-word rather than break-all so a long title breaks between
   words instead of mid-word. */
.modal-meta-wide { grid-column: 1 / -1; }
.modal-meta-wide .modal-meta-value { word-break: break-word; }
/* Links in the metadata grid follow the encyclopedia's convention, the same one
   .info-panel-body uses: accent colour, no rule until hover. The browser default
   (blue + permanent underline) read as foreign next to everything around it. */
.modal-meta-value a {
  color: var(--accent);
  text-decoration: none;
  transition: color 0.15s;
}
.modal-meta-value a:hover { text-decoration: underline; }
/* THE TAG SHELF is fenced by two rules — one above it here, one on the action
   row below. Without the lower one the chips and the buttons read as a single
   field of rounded shapes, which is exactly what they are not: tags lead deeper
   INTO the encyclopedia, the buttons leave it. */
/* ONE LINE: the label sits beside the tags, not above them (Nepô, 2026-08-07).
   It was a centred heading over a centred row — two lines to say what one says,
   in a modal where the words already compete for height with the illustration.
   .modal-tags takes the remainder and min-width:0 lets it SHRINK, which is what
   keeps fitModalTags() honest: it measures that element's clientWidth to decide
   how many chips fit, so the label must take its width out of the calculation
   rather than pushing the row wider. */
.tag-shelf { padding: 12px 30px 13px; border-top: 1px solid var(--border);
             display: flex; align-items: center; gap: 10px; }
.tag-shelf .section-label { margin: 0; flex: 0 0 auto; justify-content: flex-start; }
/* Slightly larger, now that it leads the line rather than captioning it. */
.tag-shelf .section-label .ms { font-size: 17px; }
.tag-shelf .modal-tags { flex: 1; min-width: 0; justify-content: flex-start; }
.section-label { font-size: calc(11px * var(--text-scale)); font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text-tertiary); margin: 0 0 7px;
  display: flex; align-items: center; gap: 5px; justify-content: center; }
.section-label .ms { font-size: 14px; }
.panel-body .section-label { justify-content: flex-start; margin-top: 14px; }
.panel-body .section-label:first-child { margin-top: 0; }
.modal-tags { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
.modal-tag {
  font-size: calc(12px * var(--text-scale)); background: #f0f0f0; color: var(--text-secondary);
  border-radius: 8px; padding: 4px 10px; cursor: pointer;
  border: none; transition: background 0.15s; font-family: var(--font);
}
.modal-tag:hover { background: #e0e0e0; }

/* The action row: four buttons, visible in EVERY state — the one thing the flip
   could not do, because it hid everything to show anything. Shares
   --bottom-row with the set bar so their two rules meet across the modal. */
.action-row {
  padding: 13px 30px 15px; border-top: 1px solid var(--border);
  min-height: var(--bottom-row); box-sizing: border-box; align-content: center;
  background: var(--surface);
}
.modal-front-footer {
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  padding: 12px 14px 14px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--surface);
}
/* THREE COLUMNS, TWO ROWS (Nepô, 2026-08-07). Six actions, read across:
     Share · Copy · Link       — the illustration leaves, somewhere
     Save Card · Save · Info   — it is kept, or explained
   The buttons SHRINK to fit rather than the row growing: the action row shares
   a 156px floor with the set strip (--bottom-row) and the two must land on one
   line across the modal (#76). minmax(0,1fr) lets a long label ellipse inside
   its button instead of widening the track past the modal. */
.modal-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
                 gap: 7px; }
/* Two columns on the phone, where three of ~100px leave no room for a word. */
@media (max-width: 760px) {
  .modal-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.modal-action-btn {
  flex: unset;
  display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 8px 10px; min-width: 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  background: #f5f5f3; border: 1px solid rgba(0,0,0,0.09);
  border-radius: 10px; font-size: 12px; font-weight: 500;
  color: var(--text-primary); cursor: pointer; text-decoration: none;
  transition: background 0.18s, color 0.18s, border-color 0.18s;
  font-family: var(--font); white-space: nowrap;
}
/* ── Copy — Blue (school-learning h:205 s:62) ── */
.modal-action-copy         { background: hsl(205,62%,91%) !important; color: hsl(205,67%,27%) !important; border-color: hsl(205,50%,83%) !important; }
.modal-action-copy:hover   { background: hsl(205,62%,86%) !important; }
.modal-btn--success        { background: hsl(205,62%,76%) !important; color: hsl(205,67%,27%) !important; border-color: hsl(205,55%,68%) !important; }
.modal-btn--error          { background: hsl(5,65%,82%)   !important; color: hsl(5,70%,30%)    !important; border-color: hsl(5,58%,72%)   !important; }

/* ── Download — Green (plants-nature h:128 s:42) ── */
.modal-action-download       { background: hsl(128,42%,91%) !important; color: hsl(128,47%,27%) !important; border-color: hsl(128,35%,83%) !important; }
.modal-action-download:hover { background: hsl(128,42%,86%) !important; }
.modal-btn--downloading      { background: hsl(128,42%,76%) !important; color: hsl(128,47%,27%) !important; border-color: hsl(128,35%,68%) !important; }

/* ── Link (Irasutoya) — Red (sports h:5 s:65) ── */
/* The two new actions take the hue of the action they are a variant OF, one
   step lighter: Share is Copy for someone else, Save Card is Save with the
   words attached. Reading down a column then gives one idea in two strengths,
   which is what the left pair being new is meant to say. */
.modal-action-share       { background: hsl(205,45%,95%) !important; color: hsl(205,55%,32%) !important; border-color: hsl(205,38%,88%) !important; }
.modal-action-share:hover { background: hsl(205,45%,90%) !important; }
.modal-action-card        { background: hsl(128,32%,95%) !important; color: hsl(128,40%,31%) !important; border-color: hsl(128,28%,88%) !important; }
.modal-action-card:hover  { background: hsl(128,32%,90%) !important; }
.modal-action-link       { background: hsl(5,65%,92%) !important; color: hsl(5,70%,27%) !important; border-color: hsl(5,58%,83%) !important; }
.modal-action-link:hover { background: hsl(5,65%,86%) !important; }
.modal-btn--active       { background: hsl(5,65%,76%) !important; color: hsl(5,70%,27%) !important; border-color: hsl(5,58%,68%) !important; }

/* ── Info (flip) — Yellow (design-elements h:56 s:68) ── */
.modal-details-flip-btn { background: hsl(56,68%,91%) !important; color: hsl(56,73%,24%) !important; border-color: hsl(56,58%,82%) !important; }
.modal-details-flip-btn:hover { background: hsl(56,68%,86%) !important; }

/* Back-face "Back" button */
.modal-back-flip-btn {
  width: 100%; border-radius: 10px; height: 36px;
  font-size: 12px; font-weight: 500; gap: 6px;
  background: #efefed; border: 1px solid rgba(0,0,0,0.1);
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  color: var(--text-secondary); font-family: var(--font);
  transition: background 0.15s;
}
.modal-back-flip-btn:hover { background: #e2e2e0; color: var(--text-primary); }

/* ── Back face ── */
.modal-back-header {
  background: #f5f4f2;
  border-radius: 24px 24px 0 0;
  padding: 14px 52px 13px 16px; /* right padding gives space for the floating ✕ button */
  display: flex; align-items: center; gap: 11px;
  position: relative;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.modal-back-thumb {
  width: 69px; height: 69px; object-fit: contain;
  border-radius: 8px; background: white;
  flex-shrink: 0; padding: 3px;
}
.modal-back-header-text { flex: 1; min-width: 0; }
.modal-back-title {
  font-size: 15px; font-weight: 400; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.modal-back-subtitle-text { display: none; }
.modal-back-scroll { flex: 1; overflow-y: auto; }
.modal-back-body { padding: 16px 22px 14px; }
.modal-back-section { margin-bottom: 20px; }
.modal-back-section-label {
  font-size: 13px; font-weight: 400;
  letter-spacing: 0.1px;
  color: var(--text-tertiary); margin-bottom: 10px;
}

/* Taxonomy list on back */
.modal-taxonomy { display: flex; flex-direction: column; gap: 6px; }
.modal-taxon {
  font-size: 15px; font-weight: 400;
  border-radius: 10px; padding: 9px 13px;
  cursor: pointer; border: 1.5px solid transparent;
  transition: filter 0.15s; line-height: 1.35;
}
.modal-taxon:hover { filter: brightness(0.93); }
.modal-taxon-path { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.modal-taxon-domain { font-weight: 400; }
.modal-taxon-sep { opacity: 0.45; font-size: 11px; }
.modal-back-tags { display: flex; flex-wrap: wrap; gap: 5px; }

/* Back: pinned footer — flip back button */
.modal-back-footer {
  display: flex; align-items: stretch;
  padding: 10px 14px 14px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--surface);
}

/* ═══════════════════════════════════════════
   COMMAND PALETTE (⌘K)
   ═══════════════════════════════════════════ */
/* Opened from ⌘K: the modal has to sit above the palette (z-index 1000), which
   stays open underneath so the search is still there when the modal closes. */
.modal-overlay.over-cmdk { z-index: 1100; }

.cmdk-overlay {
  display: none;
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.4);
  backdrop-filter: blur(6px);
  z-index: 1000;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12vh;
}
.cmdk-overlay.open { display: flex; }

.cmdk {
  background: var(--surface);
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.28);
  width: 580px;
  max-width: calc(100vw - 32px);
  overflow: hidden;
  animation: slideDown 0.2s cubic-bezier(0.34,1.56,0.64,1);
}

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-12px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.cmdk-search {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--border);
}
.cmdk-icon { font-size: 20px; color: var(--text-tertiary); line-height: 1; }
.cmdk-input {
  flex: 1; border: none; outline: none;
  font-size: 16px; font-family: var(--font);
  background: transparent; color: var(--text-primary);
  font-weight: 500;
}
.cmdk-input::placeholder { color: var(--text-tertiary); }

.cmdk-results { max-height: 400px; overflow-y: auto; padding: 8px; }

.cmdk-result {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 9px; border-radius: 10px;
  cursor: pointer; transition: background 0.1s;
}
.cmdk-result:hover,
.cmdk-result.selected { background: var(--surface2); }
.cmdk-result.selected { background: #eef4ff; }

.cmdk-thumb {
  width: 46px; height: 46px;
  object-fit: contain; border-radius: 8px;
  background: #f5f4f2; flex-shrink: 0;
}
.cmdk-info { flex: 1; min-width: 0; }
.cmdk-name {
  font-size: 14px; font-weight: 500; color: var(--text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cmdk-name-jp {
  font-size: 12px; color: var(--text-tertiary); margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cmdk-empty { padding: 30px; text-align: center; color: var(--text-tertiary); font-size: 14px; }

/* ── The facet bar (#35 half 2) ───────────────────────────────────────────
   Variant B: one row, the rest disclosed. Sits where the colour bar sits, and
   borrows its language — a label on the left, values on the right — because a
   second filter surface should not need a second visual grammar. */

.facet-bar { margin: 6px 0 2px; }
.facet-bar[hidden] { display: none; }

.facet-row { display: flex; align-items: flex-start; gap: 12px; padding: 3px 0; }
.facet-label {
  flex: 0 0 128px; padding-top: 5px;
  font-size: 13px; color: var(--text-tertiary);
}
.facet-chips { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; min-width: 0; }

.facet-chip {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 4px 11px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface2);
  font-family: var(--font); font-size: 13px; color: var(--text-primary);
  cursor: pointer; white-space: nowrap;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.facet-chip:hover { border-color: var(--text-tertiary); }
.facet-chip.on { background: var(--text-primary); border-color: var(--text-primary); color: #fff; }
.facet-n { font-size: 11px; opacity: 0.55; font-variant-numeric: tabular-nums; }
.facet-chip.on .facet-n { opacity: 0.8; }

/* #45: the general value leads its row and is set in italic. NEVER labelled
   "(any)" — P(動物|犬) is 6.2%, so "any" would promise the whole facet and
   deliver a sixth of it. The rule border is the divider that decision asks for,
   drawn on the chip rather than as a separate element so it survives wrapping. */
.facet-chip.is-general {
  font-style: italic;
  margin-right: 7px;
  border-right-width: 1px;
  box-shadow: 7px 0 0 -6px var(--border);
}

.facet-more {
  display: inline-flex; align-items: center; gap: 4px;
  margin: 4px 0 0 140px; padding: 4px 2px;
  background: none; border: none; cursor: pointer;
  font-family: var(--font); font-size: 13px; color: var(--text-secondary);
}
.facet-more:hover { color: var(--text-primary); }
.facet-more .ms { font-size: 18px; }

/* THE DASHED RULE IS GONE (#78). It separated the first facet from "the rest",
   a divide that stopped existing when every facet gained the same hairline
   beneath it — and it drew a line under the first row that no other row had,
   which read as the bar being split into two kinds of thing.

   The .facet-rest wrapper itself is no longer emitted: renderFacetBar puts
   every row into .facet-rows now. The class is kept unstyled rather than
   deleted so a page served from an older cache does not inherit a border from
   a rule that outlived its markup. */
.facet-rest { margin: 0; padding: 0; border-top: 0; }

.facet-clear {
  display: inline-flex; align-items: center; gap: 3px;
  margin: 6px 0 0 140px; padding: 3px 9px 3px 5px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); cursor: pointer;
  font-family: var(--font); font-size: 12px; color: var(--text-secondary);
}
.facet-clear:hover { color: var(--text-primary); border-color: var(--text-tertiary); }
.facet-clear .ms { font-size: 15px; }

@media (max-width: 700px) {
  /* The label goes above its chips — 128px of gutter is a third of the screen. */
  .facet-row { flex-direction: column; gap: 4px; }
  .facet-label { flex: none; padding-top: 0; }
  .facet-more, .facet-clear { margin-left: 0; }
}

/* ── Query chips (#63 item 3) ─────────────────────────────────────────────
   One chip per query token on the search results page. COLOUR ENCODES WHAT A
   CHIP DOES, not an arbitrary palette: a diagnostic filter behaves nothing like
   a search term — it answers a curation question and fails closed — and an OR
   group is the only chip meaning "any of these" rather than "this".

   Colour is never the ONLY signal. Excluded chips also carry a leading − and a
   line through the text, because the difference between "with" and "without" is
   the whole point of the control and must survive a colour-blind reader. */

.query-chips {
  display: flex; flex-wrap: wrap; gap: 7px;
  margin: 10px 0 2px;
}

.qchip {
  display: inline-flex; align-items: stretch;
  border-radius: 999px; overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface2);
  font-size: 13px; line-height: 1;
}
.qchip-body {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 6px 4px 6px 11px;
  border: none; background: transparent; cursor: pointer;
  font-family: var(--font); font-size: 13px; color: var(--text-primary);
}
.qchip-body:hover { background: rgba(0,0,0,0.04); }
.qchip-minus { font-weight: 700; opacity: 0.8; }
.qchip-x {
  border: none; background: transparent; cursor: pointer;
  padding: 0 9px 0 5px; font-size: 15px; line-height: 1;
  color: var(--text-tertiary); font-family: var(--font);
}
.qchip-x:hover { background: rgba(0,0,0,0.07); color: var(--text-primary); }

/* A plain word. Deliberately the quietest of the three — it is the common case
   and the row should read as a query, not as decoration. */
.qchip--term { background: var(--surface2); }

/* "any of these" */
.qchip--or {
  background: #eef4ff;
  border-color: #c9dcff;
}

/* A curation instrument, not a word someone searched for. */
.qchip--filter {
  background: #fff4e2;
  border-color: #f0d9b0;
  font-variant-numeric: tabular-nums;
}
.qchip--filter .qchip-text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }

/* Excluded: struck, muted, and prefixed with −. Three signals, one meaning. */
.qchip.is-excluded { background: var(--surface); border-style: dashed; }
.qchip.is-excluded .qchip-text { text-decoration: line-through; opacity: 0.62; }

/* ── Command palette: groups, meta, footer, egg (#63 item 2) ──────────────
   The palette went from one flat list of illustrations to five kinds of row
   under headings, so it needs a heading, a right-hand column and a footer that
   states the keyboard model where it is used. */

.cmdk-close {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex-shrink: 0;
  border: none; border-radius: 8px; cursor: pointer;
  background: transparent; color: var(--text-tertiary);
  transition: background 0.1s, color 0.1s;
}
.cmdk-close:hover { background: var(--surface2); color: var(--text-primary); }
.cmdk-close .ms { font-size: 18px; }

.cmdk-group + .cmdk-group { margin-top: 6px; }
.cmdk-group-label {
  padding: 8px 9px 4px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
  color: var(--text-tertiary); text-transform: uppercase;
}

/* Stands in for .cmdk-thumb on a row that is not an illustration, at the same
   46px, so icon rows and thumbnail rows share one baseline grid. */
.cmdk-glyph {
  width: 46px; height: 46px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 8px; background: var(--surface2);
  color: var(--text-secondary); font-size: 20px;
}
.cmdk-result.selected .cmdk-glyph { background: rgba(255,255,255,0.7); }

.cmdk-meta {
  display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  font-size: 12px; color: var(--text-tertiary);
}
.cmdk-parent { white-space: nowrap; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
.cmdk-count {
  padding: 1px 7px; border-radius: 999px;
  background: var(--surface2); font-variant-numeric: tabular-nums;
}
.cmdk-result.selected .cmdk-count { background: rgba(255,255,255,0.75); }

.cmdk-foot {
  display: flex; align-items: center; gap: 16px;
  padding: 9px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface2);
  font-size: 12px; color: var(--text-tertiary);
}
.cmdk-hint { display: flex; align-items: center; gap: 5px; }
.cmdk-foot kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 5px;
  border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: 5px; background: var(--surface);
  font-family: var(--font); font-size: 11px; line-height: 1;
  color: var(--text-secondary);
}
/* The arrows are literal ↑ ↓ characters, deliberately — see the comment in
   index.html. An icon-font <kbd class="ms"> loses the font-family contest to
   the rule above and renders the ligature NAME as text. */

/* The ↑↑↓↓←→←→BA panel. Replaces the result list rather than opening a dialog:
   the palette is already a dialog, and a second one over it would need its own
   focus and escape handling for a joke. */
.cmdk-egg { padding: 26px 20px 30px; text-align: center; }
.cmdk-egg-img { width: 96px; height: auto; margin-bottom: 10px; }
.cmdk-egg-title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
.cmdk-egg-body { font-size: 13px; color: var(--text-tertiary); margin-top: 5px; }

@media (max-width: 560px) {
  /* The footer is a keyboard legend, and this viewport has no keyboard. */
  .cmdk-foot { display: none; }
  .cmdk-parent { display: none; }
}

/* ═══════════════════════════════════════════
   EMPTY STATE
   ═══════════════════════════════════════════ */
.empty-state {
  text-align: center; padding: 80px 20px; color: var(--text-tertiary);
}
.empty-state-icon { font-size: 48px; margin-bottom: 14px; }
.empty-state-text { font-size: 17px; font-weight: 500; color: var(--text-secondary); }
.empty-state-sub  { font-size: 14px; margin-top: 8px; line-height: 1.6; }
/* Irasutoya's own illustration instead of an emoji — same 200px WebP thumbnail
   the grid uses, so it costs nothing extra to serve. */
.empty-state-img {
  width: 120px; height: 120px;
  object-fit: contain;
  margin: 0 auto 14px;
  display: block;
}
.empty-state[hidden] { display: none; }

::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.13); border-radius: 3px; }

/* ═══════════════════════════════════════════
   TOP BAR NAV LINKS
   ═══════════════════════════════════════════ */
.top-bar-links {
  display: none; /* shown via min-width query below */
  align-items: center;
  gap: 2px;
  flex: 1;
  min-width: 0;
  overflow: visible;
}
@media (min-width: 1025px) {
  .top-bar-links { display: flex; }
}
.top-bar-link {
  background: none;
  border: none;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-tertiary);
  cursor: pointer;
  padding: 5px 8px;
  border-radius: 6px;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}
.top-bar-link:hover {
  background: rgba(0,0,0,0.06);
  color: var(--text-primary);
}
.top-bar-link--web {
  color: hsl(205,62%,40%) !important;
  font-weight: 500 !important;
}
.top-bar-link--web:hover {
  background: hsl(205,62%,93%) !important;
  color: hsl(205,67%,27%) !important;
}
.top-bar-link--nature {
  color: hsl(128,42%,32%) !important;
  font-weight: 500 !important;
}
.top-bar-link--nature:hover {
  background: hsl(128,42%,91%) !important;
  color: hsl(128,42%,22%) !important;
}
.top-bar-link--sports {
  color: hsl(5,65%,38%) !important;
  font-weight: 500 !important;
}
.top-bar-link--sports:hover {
  background: hsl(5,65%,93%) !important;
  color: hsl(5,65%,27%) !important;
}
/* iPad nav dropdown — base hidden, shown at 1025-1199px */
.ipad-nav-wrap {
  display: none;
  position: relative;
  flex-shrink: 0;
}
@media (max-width: 1199px) and (min-width: 1025px) {
  .ipad-nav-wrap { display: flex; align-items: center; }
}
.ipad-nav-btn {
  background: none;
  border: none;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-tertiary);
  cursor: pointer;
  padding: 5px 8px;
  border-radius: 6px;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
  display: flex;
  align-items: center;
  gap: 4px;
}
.ipad-nav-btn:hover { background: rgba(0,0,0,0.06); color: var(--text-primary); }
.ipad-nav-arrow {
  display: inline-block;
  transition: transform 0.15s;
}
.ipad-nav-wrap.open .ipad-nav-arrow { transform: rotate(180deg); }
.ipad-nav-dropdown {
  display: none;
  position: fixed;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.12);
  padding: 6px;
  z-index: 500;
}
.ipad-nav-dropdown.open { display: block; }

.top-bar-sep {
  color: var(--border);
  font-size: 14px;
  line-height: 1;
  user-select: none;
  flex-shrink: 0;
}
/* How-to-search illustrated examples */
.hts-example {
  background: var(--hover);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px 16px;
  margin: 14px 0;
}
.hts-path {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-tertiary);
  margin-bottom: 12px;
}
.hts-path strong { color: var(--text-primary); font-weight: 700; }
.hts-path-sep { color: var(--border); font-size: 14px; }
.hts-thumbs {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.hts-thumb-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.hts-thumb {
  width: 76px;
  height: 76px;
  object-fit: contain;
  border-radius: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 5px;
}
.hts-thumb-label {
  font-size: 11px;
  color: var(--text-tertiary);
  text-align: center;
  max-width: 76px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hts-search-demo {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1.5px solid var(--accent);
  border-radius: 10px;
  padding: 8px 13px;
  margin: 10px 0 12px;
  font-size: 13px;
  color: var(--text-secondary);
}
.hts-search-demo span {
  color: var(--text-primary);
  font-weight: 500;
}
.hts-search-results {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 4px;
}
.hts-search-result {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  background: var(--hover);
  border: 1px solid var(--border);
  border-radius: 9px;
  font-size: 12px;
}
.hts-search-result img {
  width: 36px;
  height: 36px;
  object-fit: contain;
  border-radius: 6px;
  background: var(--surface);
  flex-shrink: 0;
}
.hts-search-result-title { color: var(--text-primary); font-weight: 500; }
.hts-search-result-path { color: var(--text-tertiary); font-size: 11px; margin-top: 1px; }

/* ── Three-levels table inside how-to-search ── */
/* Search-command reference at the foot of How to Search. Two columns so the
   command and what it finds read as a pair; collapses to stacked on narrow
   screens, where a two-column table would force a horizontal scroll. */
.hts-cmd {
  width: 100%;
  border-collapse: collapse;
  margin: 14px 0 18px;
}
.hts-cmd td {
  padding: 9px 12px;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.hts-cmd tr:last-child td { border-bottom: none; }
.hts-cmd td:first-child { width: 1%; white-space: nowrap; }
.hts-cmd code {
  background: var(--bg-secondary);
  border-radius: 6px;
  padding: 2px 7px;
  font-size: 0.92em;
  white-space: nowrap;
}
@media (max-width: 560px) {
  .hts-cmd, .hts-cmd tr, .hts-cmd td { display: block; width: auto; }
  .hts-cmd td { border-bottom: none; padding: 2px 0; }
  .hts-cmd td:first-child { width: auto; padding-top: 10px; }
  .hts-cmd tr { border-bottom: 1px solid var(--border); padding-bottom: 8px; }
}
.hts-levels {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 14px 0 18px;
}
.hts-level {
  display: flex;
  align-items: baseline;
  gap: 10px;
  background: var(--bg-secondary);
  border-radius: 8px;
  padding: 10px 14px;
}
.hts-level-name {
  font-weight: 700;
  font-size: 13px;
  color: var(--text-primary);
  min-width: 90px;
  flex-shrink: 0;
}
.hts-level-desc {
  font-size: 13px;
  color: var(--text-secondary);
  line-height: 1.4;
}

/* ── Web links grid inside info panel ── */
.web-links-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin: 16px 0 20px;
}
.web-link-card {
  position: relative;
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px;
  background: var(--hover);
  border-radius: 14px;
  border: 1px solid var(--border);
  color: inherit;
  transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
}
.web-link-card:hover {
  background: var(--surface);
  box-shadow: 0 4px 16px rgba(0,0,0,0.10);
  transform: translateY(-2px);
}
/* Stretched invisible anchor — covers the whole card, no text = no underline */
.web-link-stretch {
  position: absolute; inset: 0;
  border-radius: 14px;
}
.web-link-card-icon {
  font-size: 28px; flex-shrink: 0;
  width: 48px; height: 48px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px;
  background: var(--surface);
}
.web-link-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.web-link-card-title { font-size: 13px; font-weight: 700; color: var(--text-primary); }
.web-link-card-desc { font-size: 11px; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.web-cta {
  margin-top: 16px;
  font-size: 13px; line-height: 1.7;
  color: var(--text-tertiary);
}
.web-creator-row {
  display: flex; align-items: flex-start; gap: 16px;
  margin-bottom: 18px;
}
.web-creator-img {
  width: 144px; height: auto; flex-shrink: 0;
  border-radius: 10px;
}
@media (max-width: 600px) {
  /* いらすとやへ！ — image stacks above text, bigger since it has full width */
  .web-creator-row { flex-direction: column; align-items: center; gap: 12px; }
  .web-creator-img { width: 140px; }
  /* いらすとやへ！ — link cards in a single column */
  .web-links-grid { grid-template-columns: 1fr; }
  /* つかいかた — level rows stack title above description */
  .hts-level { flex-direction: column; gap: 4px; }
  .hts-level-name { min-width: unset; }
  /* このプロジェクトについて — avatar stays horizontal, image shrinks */
  .info-avatar-wrap { flex-wrap: wrap; gap: 12px; padding: 12px; }
  .info-avatar { width: 72px; border-radius: 8px; }
  .info-avatar-name-jp { font-size: 28px; }
  .info-avatar-name { font-size: 13px; }
  .info-avatar-email { font-size: 12px; margin-top: 4px; }
  .info-avatar-divider { width: 100%; height: 1px; align-self: unset; margin: 4px 0; }
  .info-avatar-links { width: 100%; }
  /* General — no horizontal scroll in info panels */
  .info-panel-body { overflow-x: hidden; }
}
.web-creator-text {
  font-size: 13.5px; line-height: 1.7;
  color: var(--text-secondary);
  padding-top: 2px;
}

/* ═══════════════════════════════════════════
   INFO OVERLAY PANEL
   ═══════════════════════════════════════════ */
.info-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.45);
  z-index: 300;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.info-overlay.open { display: flex; }
.info-panel {
  background: var(--surface);
  border-radius: 16px;
  width: 100%;
  max-width: 680px;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 60px rgba(0,0,0,0.22);
  overflow: hidden;
}
.info-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.info-panel-title {
  font-size: 17px;
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: -0.2px;
}
.info-close {
  width: 30px; height: 30px;
  border-radius: 50%;
  border: none;
  background: #e8e8e6;
  color: var(--text-secondary);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s;
  flex-shrink: 0;
}
.info-close .ms { font-size: 18px; line-height: 1; }
.info-close:hover { background: #d8d8d6; }
.info-panel-body {
  overflow-y: auto;
  padding: 24px 48px 48px;
  font-size: 14px;
  line-height: 1.75;
  color: var(--text-secondary);
}
@media (max-width: 600px) {
  .info-panel-body { padding: 20px 20px 36px; }
}
.info-panel-body h2 {
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
  margin: 20px 0 6px;
}
.info-panel-body h2:first-child { margin-top: 0; }
.info-panel-body p { margin: 0 0 12px; }
.info-panel-body ul {
  margin: 0 0 12px;
  padding-left: 20px;
}
.info-panel-body li { margin-bottom: 6px; }
.info-panel-body a {
  color: var(--accent);
  text-decoration: none;
}
.info-panel-body a:hover { text-decoration: underline; }
.info-panel-body hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 20px 0;
}

/* About / Contact avatar */
.info-avatar-wrap {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 20px;
  padding: 16px;
  background: var(--bg);
  border-radius: 12px;
}
.info-avatar {
  width: 140px;
  height: auto;
  border-radius: 12px;
  flex-shrink: 0;
}
.info-avatar-text {
  flex: 1;
  min-width: 0;
}
.info-avatar-name-jp {
  font-size: 42px;
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: -1px;
  line-height: 1.1;
}
.info-avatar-name {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-secondary);
  margin-top: 4px;
  letter-spacing: 0;
}
.info-avatar-email {
  font-size: 13px;
  color: var(--accent);
  margin-top: 6px;
}
.info-avatar-divider {
  width: 1px;
  align-self: stretch;
  background: var(--border);
  flex-shrink: 0;
  margin: 0 4px;
}
.info-avatar-links {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
}
.info-social-btn {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 13px;
  border-radius: 10px;
  background: var(--hover);
  border: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
}
.info-social-btn:hover {
  background: var(--surface);
  box-shadow: 0 3px 10px rgba(0,0,0,0.09);
  transform: translateY(-1px);
}
.info-social-icon {
  font-size: 18px;
  flex-shrink: 0;
  width: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-style: normal;
}
.info-social-name {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-primary);
}
.info-social-dash {
  color: var(--text-tertiary);
  font-size: 12px;
}
.info-social-url {
  font-size: 12px;
  color: var(--text-tertiary);
}
.info-tag {
  display: inline-block;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 2px 8px;
  font-size: 12px;
  color: var(--text-tertiary);
  margin-right: 4px;
  margin-bottom: 4px;
}

/* ═══════════════════════════════════════════
   HUB BADGE (illustration grid cards)
   ═══════════════════════════════════════════ */
.hub-badge {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  background: var(--hub); color: white;
  font-size: 9px; font-weight: 700;
  padding: 2px 7px; border-radius: 10px;
  letter-spacing: 0.03em;
  border: 1.5px solid white;
  /* no box-shadow — shadow is a painted property; badge border is sufficient */
  pointer-events: none;
}

/* ═══════════════════════════════════════════
   HUB CAROUSEL (modal)
   ═══════════════════════════════════════════ */
/* THE SET BAR. Was `.carousel-strip` under the illustration in a one-column
   card; now the bottom half of the illustration column, sharing --bottom-row
   with the action row so the rule above each lands on one line. */
.set-bar {
  border-top: 1px solid var(--border);
  background: var(--hub-light);
  flex-shrink: 0;
  min-height: var(--bottom-row); box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center;
  /* top was 12px; the other 4px of the larger tab (#81) comes from here. */
  padding: 8px 14px 14px 22px;
}
/* ALIGNED WITH THE FIRST ILLUSTRATION (Nepô, 2026-08-07), not with the bar's
   edge. .set-bar pads 22px on the left and .set-viewport adds 30px of arrow
   gutter on top of it, so the first item begins 52px in while the pill began at
   22px — the heading sat a full arrow's width to the left of everything it
   labels. The 30px here is the SAME gutter, so the two move together: the pill
   now starts on the first item's left edge and the count ends on the last
   item's right edge. */
/* margin-bottom was 10px, and the 4px is where the larger tab is paid from
   (#81) — see the note on .set-tab .ms. Changed here at its source rather than
   overridden lower down: a rule that out-shouts another leaves two numbers in
   the file and no statement of which one is the design. */
.set-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
            padding: 0 30px; }
/* Back sits before the pill, in the arrow gutter the strip already reserves —
   so it costs no width and lines up with the chevrons below it. */
.set-back {
  flex: 0 0 auto; width: 24px; height: 24px; margin-left: -28px; padding: 0;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface); color: var(--hub); font-family: var(--font);
}
/* DISABLED, NOT HIDDEN (Nepô, 2026-08-07). A control that appears only once it
   works cannot be learned before you need it — and its arrival shifts the row.
   Present from the start, greyed until there is somewhere to go back to. */
.set-back:disabled { opacity: .3; cursor: default; border-color: var(--border); }
.set-back:hover:not(:disabled) { border-color: var(--hub); }
.set-back .ms { font-size: 17px; }
/* TWO TABS WHERE THE PILL WAS (#81). The pill was a LABEL — it said "this is a
   set" about the only thing the strip could show. These are CONTROLS, and the
   difference has to be visible before it is clicked: the active one keeps the
   pill's filled look so the row still reads at a glance, the inactive one is an
   outline, and both are large enough to be a target.

   NOT uppercase, and not 9px. The pill could be both because `Set` is a word
   nobody needs to read; セット and Relacionadas are, and uppercase does nothing
   to Japanese while 9px makes it unreadable. */
.set-tabs { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.set-tab {
  display: inline-flex; align-items: center; gap: 5px; line-height: 1;
  padding: 5px 12px; border-radius: 13px;
  font-family: var(--font); font-size: 12px; font-weight: 700;
  letter-spacing: 0.02em; white-space: nowrap; cursor: pointer;
  border: 1px solid var(--hub); background: transparent; color: var(--hub);
}
/* THE TAB'S HEIGHT IS SPENT FROM A FIXED BUDGET, which is why the size Nepô
   asked for on 2026-08-07 had to be PAID FOR rather than simply added. The icon
   is the tallest thing in the tab, so it sets the tab's height, which sets
   .set-head's height, which decides whether the whole bar stays within
   --bottom-row (156px) — and that floor is what puts the set bar's top rule and
   the action row's on ONE line across the modal (#76's structural claim).
   The bar was sitting EXACTLY on the floor, so the next two rules are where the
   larger tab is paid from. Measured, not reasoned: the probe reads the bar's
   height and its bottom against .modal-scene's. */
.set-tab .ms { font-size: 15px; line-height: 1; }
.set-tab:hover:not(:disabled):not(.on) { border-color: var(--hub); color: var(--hub); }
.set-tab.on { background: var(--hub); border-color: var(--hub); color: #fff; }
/* Disabled means "there is no set here", which is a fact about the
   illustration, not a failure — so it recedes rather than warning. It is never
   also `.on`: that shipped in the playground as filled purple, saying "you are
   here", unclickable, above an empty strip. */
.set-tab:disabled { opacity: .35; cursor: default; }
/* The empty Related result. Sized to the strip it replaces so the bar does not
   change height when an illustration has no relatives. */
.set-empty { display: flex; align-items: center; height: 100%;
             font-size: 12px; color: var(--text-tertiary); padding: 0 2px; }
.set-name { font-size: 12.5px; color: var(--hub); font-weight: 700; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* POSITION, not size — the bar already shows how many by showing them; what a
   scrolling strip hides is which one you are on. */
.set-count { margin-left: auto; font-size: 11.5px; color: var(--text-tertiary); }
.set-desc {
  font-size: 11.5px; color: var(--hub); line-height: 1.45;
  padding: 0 0 6px;
}

/* The gutter is the arrows' room, reserved whether or not they show. */
.set-viewport { position: relative; padding: 0 30px; }
.set-selector-wrap { overflow: hidden; }
.set-selector { display: flex; gap: 8px; transition: transform 0.25s ease; justify-content: flex-start; }

.carousel-item {
  flex-shrink: 0; width: 76px; cursor: pointer;
  border-radius: 10px; overflow: hidden;
  border: 2px solid transparent;
  transition: border-color 0.15s, transform 0.1s;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
.carousel-item:hover { transform: translateY(-1px); box-shadow: var(--shadow-card-hover); }
.carousel-item.active { border-color: var(--hub); }
.carousel-item img { width: 100%; aspect-ratio: 1; object-fit: contain; background: var(--hub-light); padding: 6px; display: block; }
/* HIDDEN — Nepô, 2026-08-07. At 76px wide the caption was 9px type ellipsed
   after a few characters, so on a set of near-identical members it said the
   same truncated thing under every one. The pictures differ; the captions did
   not. The rule is kept rather than deleted because the NAME is not lost: it
   stays in aria-label and in the title attribute app.js sets, so a screen
   reader still announces it and a hover still shows it.

   --bottom-row (156px) is a FLOOR and is unchanged, so the set bar keeps its
   height and the one line it shares with the action row across the modal
   (#76) survives this. Asserted, not assumed. */
.carousel-item-label { display: none; }
/* SELECTION IS A BORDER, NEVER AN OUTLINE — an outline paints outside the
   border box and an overflow ancestor clips it. .carousel-item has always
   used a border; keep it that way. */

.set-arrow {
  position: absolute; top: 28px; transform: translateY(-50%);
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: 0 2px 6px rgba(0,0,0,0.1); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; color: var(--text-secondary); transition: opacity 0.15s;
  font-family: var(--font);
}
.set-arrow:hover { background: #f0f0f0; }
/* Faded, never removed — the gutter must stay reserved or the strip jumps
   sideways the moment it reaches an end. */
.set-arrow.hidden { opacity: 0; pointer-events: none; }
.set-arrow.left { left: 0; }
.set-arrow.right { right: 0; }

/* ── Reading level (#69) ───────────────────────────────────────────────────
   Kasane (襲の色目) palette, from Nepô's reference sheet, deepened only as far
   as legibility as TEXT requires. Five rules, and each earns its place on
   frequency measured over every title and description:
     indigo  hiragana              34.4% of tokens · 藍色, added — Kasane has no blue
     purple  katakana              18.2% of tokens
     green   の                    46.4% of particles
     gold    every other particle  53.6% of particles
     red     kanji the reader KNOWS — the grade selector
   Punctuation stays ink. Latin has no colour: Roman letters in a kana line are
   already unmistakable.
   Red is the interesting one — the other four mark what a thing IS, red marks
   what the reader CAN DO. Moving up a grade turns more of the page red: a
   progress bar made of the text itself.
   (This paragraph said PURPLE until 2026-08-05, describing the arrangement the
   swap below replaced on 2026-08-02. It read as current for three days and a
   session nearly documented "purple is the progress bar" on the strength of it,
   catching the error only because the rendered ladder came out red. If the
   colours are ever swapped again, this paragraph is the one that goes stale.) */
:root{
  --kx-hira:#4a5da5; --kx-kata:#a868b8; --kx-no:#7fb84f;
  --kx-pt:#d9a038;   --kx-grade:#d93b2b;
  /* Swapped 2026-08-02: red went to the grade selector and purple to katakana.
     Red is the warmest thing on the line, and the kanji a reader HAS LEARNED is
     the one mark meant to be noticed — it is a reward, not a label. Katakana is
     18.2% of tokens and only needs to read as "different script", which purple
     does without competing. */
  --kx-gap:0.35em;
}
.kx-gap{margin-inline-end:var(--kx-gap)}
.kx-h{color:var(--kx-hira)}
.kx-k{color:var(--kx-kata)}
.kx-no{color:var(--kx-no)}
.kx-pt{color:var(--kx-pt)}
.kx-g{color:var(--kx-grade);font-weight:500}
/* ── The こども menu, in two surfaces ──────────────────────────────────────
   Settled 2026-08-02 in playground/reading-menu.html, which produced three
   surfaces rather than one winner. Two of them live here — illustrated cards
   at ≥601px, the compact strip below it — and the third (the ladder) is on the
   つかいかた page, where explaining is the job.
   ONE ORDER on all three: ひらがな → 1–6年生 → 漢字 LAST, because 漢字 is the
   level containing every kanji and belongs above 6年生 rather than beside
   ひらがな. */
.read-level{position:relative;display:inline-block}
/* The button reads こども — the name of the feature, not the current state, and
   not a difficulty setting: it is a way of reading the encyclopedia.
   It no longer needs a FIXED width. It did while the label changed with the
   state (かな / 1年 / 漢字), because a Latin digit is narrower than a full-width
   kanji and picking a year resized the button and dragged the top bar; the
   width was pinned to stop that. A constant label cannot jump, so the pin is
   gone with the reason for it. If the label is ever made variable again, put
   the width back. */
.read-level-btn{font:inherit;font-size:13px;font-weight:700;text-align:center;
  padding:5px 10px;border-radius:9px;border:1px solid var(--border,#e5e0d8);
  background:var(--card,#fff);color:var(--text,#3a3632);cursor:pointer;line-height:1.2}
.read-level-btn.is-on{background:var(--kx-grade);border-color:var(--kx-grade);color:#fff}
.read-level-menu{display:none;position:absolute;top:calc(100% + 6px);right:0;z-index:60;
  background:var(--card,#fff);border:1px solid var(--border,#e5e0d8);border-radius:14px;
  padding:10px;box-shadow:0 12px 32px rgba(0,0,0,.14)}
.read-level-menu.open{display:block}

/* ── cards — desktop and tablet ─────────────────────────────────────────── */
.read-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;width:304px}
/* Selection LIFTS the card, it does not repaint it — this file's own idiom
   (.big-card, .subcat-card). A checked rule that set a background would beat
   .rc-kana's tint on specificity and flatten the one card meant to stand out
   at the moment it is chosen; height is a channel no background rule can win. */
.rc{border:2px solid transparent;border-radius:12px;background:var(--hover,#f7f4ef);
  padding:8px 6px;cursor:pointer;text-align:center;font:inherit;color:inherit;
  box-shadow:var(--shadow-card);
  transition:border-color .12s,background .15s,box-shadow .15s,transform .15s}
.rc:hover{transform:translateY(-1px);box-shadow:var(--shadow-card-hover)}
.rc:focus-visible{outline:2px solid var(--kx-grade);outline-offset:2px}
.rc[aria-checked="true"]{border-color:var(--kx-grade);transform:translateY(-2px);
  box-shadow:var(--shadow-card-hover)}
/* LAST on purpose: :hover and [aria-checked] are both class-plus-pseudo/attr,
   so among equals the later rule wins. Put the press above either and a
   selected card stops responding to being pressed. */
.rc:active{transform:scale(.99)}
/* MAX-WIDTH IS LOAD-BEARING. gakunen1–6 are 200×86 BANNERS, not squares —
   capped by height alone one renders far wider than its column and bursts the
   panel. Height caps the tall art, width caps the wide art. */
.rc-art{display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:38px}
.rc-art img{max-width:100%;max-height:56px;object-fit:contain}
.rc-mode{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:1px;padding:11px 10px}
.rc-name{font-size:15px;font-weight:800;line-height:1.35}
.rc-sub{font-size:11.5px;font-weight:600;color:var(--muted,#8a8078);line-height:1.4}
/* ひらがな is where a child who cannot read kanji starts, so it is not one of
   eight equal boxes. The tint is its OWN level colour at low alpha — standing
   out costs no new colour. */
.rc-kana{background:rgba(255,204,128,.30)}
.rc-kana:hover{background:rgba(255,204,128,.46)}
.rc-kana .rc-sub{color:#a8763a}

/* ── strip — phones ─────────────────────────────────────────────────────── */
.read-strip{display:flex;flex-direction:column;gap:2px}
.read-strip button{border:0;background:none;cursor:pointer;font:inherit;font-size:13px;
  font-weight:700;padding:8px 14px;border-radius:9px;color:var(--text,#3a3632);
  min-width:8.5em;text-align:center;white-space:nowrap}
.read-strip button:hover{background:var(--hover,#f4f1ec)}
/* selected background/colour are set inline from LEVEL_COLOR — no colour here,
   or this rule would fight the inline one on every deselect */
.read-strip button[aria-checked="true"]{font-weight:800}
.read-strip .read-sep{height:1px;background:var(--border,#e8e2da);margin:3px 5px}

/* Which surface shows. 600px is this file's existing phone boundary. */
.read-level-menu .read-strip{display:none}
@media (max-width:600px){
  .read-level-menu .read-cards{display:none}
  .read-level-menu .read-strip{display:flex}
}

/* ── ladder — the つかいかた page ────────────────────────────────────────────
   Six years as a climb, which is how Japanese learning books lay the years out.
   Rungs at or below the chosen level fill in their own colour; those above stay
   grey — the years still to come. */
.read-ladder{display:flex;align-items:flex-end;gap:5px;overflow-x:auto;
  padding:4px 0 6px;margin:6px 0 18px}
/* THE RUNGS MUST SHRINK, and min-width was stopping them. The panel is
   max-width:680px with 48px of padding each side — 584px of content — while
   eight rungs at min-width:70px plus seven 6px gaps wanted 602px. It overflowed
   by 18px, which cropped 漢字: the one rung the whole climb builds towards.
   flex:1 has basis 0 already, so removing the floor is the entire fix — 584px
   then gives about 68px a rung, and ひらがな, the longest label, measures roughly
   44px at 11px. overflow-x:auto stays for phones, where the media query below
   restores a floor and scrolling is the honest answer. */
.read-ladder .rung{flex:1;min-width:0;border:0;background:none;cursor:pointer;
  padding:0;text-align:center;transition:transform .15s}
.read-ladder .rung img{width:100%;height:44px;object-fit:contain;margin-bottom:5px}
/* ひらがな's rung gets the column's full width. study_nihongo is 191×200 where
   gakunen1–6 are 200×86, so inside a 44px box the near-square drawing renders
   about 42px wide against their 100+, and the first rung of the climb looked
   like an afterthought. */
.read-ladder .rung-kana img{height:auto;max-height:none}
.read-ladder .rung-bar{display:block;border-radius:10px 10px 0 0;
  transition:background .15s,height .15s,opacity .15s}
.read-ladder .rung small{display:block;font-size:11px;color:var(--muted,#8a8078);
  margin-top:5px;white-space:nowrap}
/* Phones cannot fit eight rungs — the body padding drops to 20px but the
   viewport drops much further, so here the floor comes back and the ladder
   scrolls on purpose rather than crushing ひらがな into two lines. */
@media (max-width:600px){ .read-ladder{gap:4px} .read-ladder .rung{min-width:52px} }


/* ═══════════════════════════════════════════
   MODAL — THE DETAILS PANEL (#76)
   ═══════════════════════════════════════════
   The back face's successor, and a SIBLING rather than a reverse: it slides
   over the words and stops at the column rule, so the illustration, the set bar
   and the four buttons stay visible while you read the metadata. */
.details-panel {
  position: absolute; inset: 0;
  /* ABOVE the header (z 5): at a lower z-index the language flag floats on top
     of the panel's own heading — the one control the panel is not showing. */
  z-index: 7;
  background: var(--surface);
  border-left: 1px solid var(--border);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.34s cubic-bezier(0.22,0.61,0.36,1);
  will-change: transform;
  box-shadow: -16px 0 34px rgba(0,0,0,0.09);
}
.modal-overlay.details-open .details-panel { transform: none; }
@media (prefers-reduced-motion: reduce) { .details-panel { transition: none; } }
.panel-head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 30px; border-bottom: 1px solid var(--border);
}
/* SCALES WITH THE READER (Nepô, 2026-08-07). #79 scaled the surfaces a person
   READS and deliberately left the chrome alone, and this heading was filed as
   chrome — but it stopped being a label on a container the moment it became a
   sentence: "About This Illustration" is read, not glanced at. It is also the
   first line inside the panel, so at the largest setting it was the one piece
   of text there that had not grown. */
.panel-head b { font-size: calc(13px * var(--text-scale)); flex: 1; }
/* A WORD, NOT A GLYPH — the modal's own close is already visible in the other
   corner, and two identical marks would mean two different things. Black
   because every other button carries a domain hue that means something, and
   leaving a panel is not a domain. */
.modal-action-btn.act-close { background: var(--text-primary) !important; color: #fff !important;
  border-color: var(--text-primary) !important; flex: 0 0 auto; }
.panel-body { flex: 1; overflow-y: auto; padding: 16px 30px 20px; }
/* THREE COLUMNS, with the filename spanning all of them on the line above
   (Nepô, 2026-08-07). Dimensions, file size and published date are each a short
   value that reads at a glance; the filename is long, and giving it a row of
   its own is what lets it be shown WHOLE rather than ellipsed into uselessness.
   One column below 760px, where three would leave ~90px each. */
.info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
             gap: 10px 14px; }
.info-row--file { grid-column: 1 / -1; }
@media (max-width: 760px) { .info-grid { grid-template-columns: 1fr; } }

/* The filename and its copy button share a line: the value takes the room and
   the button holds the right edge, so it lands in the same place whatever the
   name's length. */
/* THE BUTTON SITS AGAINST THE NAME, not at the far edge (Nepô, 2026-08-07).
   The value used to be flex:1, which made it eat the whole row and pushed Copy
   to the right margin — a button an inch of empty space away from the thing it
   acts on. flex:0 1 auto sizes the name to its content, so the pair reads as
   one unit; the 10px gap is the only thing between them. It still SHRINKS
   rather than shoving the button off on a narrow panel. */
.info-file { display: flex; align-items: center; gap: 10px; min-width: 0; }
.info-file .info-value { flex: 0 1 auto; min-width: 0; }
/* NAMED, NOT JUST DRAWN (Nepô, 2026-08-07). It was a 26px icon square, which
   asks the reader to recognise a glyph in a panel where nothing else is a
   control; with the word beside it there is no guessing. Sized like a real
   button rather than an affordance squeezed beside the value. */
.info-copy {
  flex: 0 0 auto; height: 32px; border-radius: 8px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px; padding: 0 11px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-secondary); font-family: var(--font);
  font-size: calc(12px * var(--text-scale));
  transition: border-color .15s, color .15s, background .15s;
}
.info-copy:hover { border-color: var(--text-tertiary); background: var(--surface2, #f5f2ed); }
.info-copy .ms { font-size: 17px; }
.info-copy-label { white-space: nowrap; }
.info-copy.is-done   { color: #4c8a5a; border-color: #4c8a5a; }
.info-copy.is-failed { color: #b4693f; border-color: #b4693f; }

/* ═══════════════════════════════════════════
   MODAL — PHONE (#76)
   ═══════════════════════════════════════════
   Below ~760px there is not room for two columns of real width; forcing it
   gives a 150px illustration beside 150px of text. Stacked instead. */
@media (max-width: 760px) {
  .modal-scene {
    --mw: 100%; --mh: auto;
    flex-direction: column; max-height: 86vh;
    /* ONE SCROLLER, AND IT IS THE MODAL. Two columns scrolling independently is
       right side by side; stacked it reads as the illustration being stuck
       while a small window of text moves under it. overflow-x stays hidden so
       the set selector can scroll sideways without the sheet following it. */
    overflow-y: auto; overflow-x: hidden;
  }
  .ill-col  { flex: none; border-right: none; border-bottom: 1px solid var(--border); }
  .text-col { overflow: visible; }
  .artwork  { min-height: 240px; }
  .panel-bounds { min-height: auto; }
  /* Not a scroll box here — the 56px of top padding clears a header that, on
     the phone, sits over the ILLUSTRATION rather than over the words, and it
     was pushing the title out of an 85px window from the inside. */
  .reading { flex: none; overflow: visible; justify-content: flex-start; padding: 20px 24px 14px; }
  /* The picture's three corners: close ↖, backgrounds ↗, language ↘. */
  .head-ill  { flex: 1; }
  .head-text { display: none; }
  .ill-foot  { display: flex; }
  .details-panel { width: 100%; }
}


/* ═══════════════════════════════════════════
   MODAL — FILE INFO ON THE FRONT, FACETS IN THE PANEL (#76)
   ═══════════════════════════════════════════ */
/* One line, directly above the tags. Quiet on purpose: these four facts are a
   footnote to the illustration, and anything louder would compete with the
   description above it. Wraps rather than truncates — a long filename is the
   one fact here that can be long, and a clipped filename is useless. */
/* ── THE PANEL'S SECTIONS ─────────────────────────────────────────────────
   File Info is static and always readable; Taxonomy and Type are disclosure
   widgets, which is what opens room for more sections later without the panel
   simply getting longer. */
/* Every section is a <details> now, File Info included — it just opens by
   default. One shape means one set of rules, and a reader learns the control
   once. .panel-sec-body is the single child each section wraps its content in,
   so the open-state border below applies to ONE element per section rather
   than to however many divs happen to be inside it. */
.panel-sec { margin-bottom: 10px; }
.panel-sec:last-child { margin-bottom: 0; }
/* A SUMMARY HAS TO LOOK LIKE A CONTROL. It was a bare section heading — the
   same small grey uppercase label as the static one above it — so the only
   thing saying it could be clicked was a chevron, and Nepô read the whole row
   as text (2026-08-07). It now has what every other control in this modal has:
   a surface, a border, room around the words, and a hover. `justify-content:
   space-between` pushes the chevron to the far edge so the row reads as a
   whole strip rather than as a word with a mark after it. */
/* `summary.section-label`, and the second class is not decoration: the summary
   carries .section-label for its typography, and `.panel-body .section-label`
   (0,2,0) sets justify-content:flex-start — which out-ranks a plain
   `.panel-sec > summary` (0,1,1), so space-between silently did nothing and the
   chevron sat against the word instead of at the far edge. Found by asking the
   PAGE which rules it matched rather than by reading this file, which is the
   only way that third rule was ever going to turn up. Matching the element's
   real specificity is the fix; adding a louder rule elsewhere would not be. */
.panel-sec > summary.section-label {
  cursor: pointer; list-style: none; display: flex; align-items: center;
  justify-content: space-between; gap: 5px; user-select: none;
  padding: 8px 10px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface2, #f5f2ed);
  transition: background .15s, border-color .15s;
}
.panel-sec > summary:hover { background: #ece7e0; border-color: var(--text-tertiary); }
.panel-sec > summary:focus-visible { outline: 2px solid var(--hub); outline-offset: 1px; }
/* Open, the heading joins the content below it rather than floating above it. */
.panel-sec[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.panel-sec[open] > :not(summary) {
  border: 1px solid var(--border); border-top: 0;
  border-radius: 0 0 9px 9px; padding: 10px;
}
.panel-sec > summary::-webkit-details-marker { display: none; }
/* The chevron is drawn here rather than left to the browser's default triangle,
   which cannot be positioned or coloured consistently across engines. It turns
   with the section, so the control states which way it is going. */
.panel-sec > summary::after {
  content: 'expand_more'; font-family: 'Material Symbols Rounded';
  font-size: 16px; line-height: 1; color: var(--text-tertiary);
  transition: transform .18s ease; margin-left: 1px;
}
.panel-sec[open] > summary::after { transform: rotate(180deg); }
/* THE CLOSED SECTION MUST ACTUALLY CLOSE. A <details> hides its non-summary
   children through the UA stylesheet, and an AUTHOR display wins over that:
   .modal-taxonomy is display:flex and .modal-facets is display:grid, so both
   bodies stayed fully visible while the element reported open === false.
   Measured 2026-08-07 — the taxonomy list was 179px tall open and 179px tall
   closed. The same shape as #78's facet-bar bug: every piece of state was
   correct and only the pixels disagreed, which is why the check has to read
   computed visibility and never the DOM flag. */
.panel-sec:not([open]) > :not(summary) { display: none; }
.panel-sec > summary:hover::after { color: var(--text-secondary); }
@media (prefers-reduced-motion: reduce) {
  .panel-sec > summary::after { transition: none; }
}

/* The 2x2 File Info grid, back in the panel where #76 found it — see the note
   in index.html for why it returned. */
.info-row { min-width: 0; }
.info-label {
  display: flex; align-items: center; gap: 5px;
  font-size: calc(11px * var(--text-scale)); color: var(--text-tertiary);
  margin-bottom: 3px;
}
.info-label .ms { font-size: 18px; }
.info-value {
  font-size: calc(12.5px * var(--text-scale)); color: var(--text-primary);
  word-break: break-all;
}
/* Mifune's wording is prose, not an identifier — break-all would chop Japanese
   mid-phrase, which the filename above it actually needs and a sentence never
   does. */
.info-original { word-break: normal; line-height: 1.55; }
.original-note {
  font-size: calc(11px * var(--text-scale)); color: var(--text-tertiary);
  margin-top: 2px;
}

/* Copy Text sits above the title, right-aligned so it holds the same edge as
   the header's controls and does not indent the words it acts on. Quiet until
   wanted: the card is for reading, and a button competing with the title would
   be the loudest thing on a surface whose whole job is the illustration. */
/* ── CLASSIFIERS ──────────────────────────────────────────────────────────
   The card's middle part. Deliberately NOT chips: a tag is a route into the
   encyclopedia and looks clickable because it is; a classifier points at this
   illustration and does nothing when pressed. Giving them the same shape would
   teach a reader that some chips search and some are decoration.

   So they read as a DEK — the line under a headline that qualifies it. Middle
   dots rather than commas, because that is Mifune's own separator inside the
   parenthetical these came from. Scales with the reader like every other line
   here (#79). */
.modal-classifiers {
  font-size: calc(12.5px * var(--text-scale));
  color: var(--text-tertiary);
  letter-spacing: .01em;
  text-transform: uppercase;
  font-weight: 700;
  font-size: calc(10.5px * var(--text-scale));
  /* ABOVE the title now, left-aligned (Nepô, 2026-08-07) — a KICKER rather than
     a dek. Set small, uppercase and bold, which is how a kicker is told apart
     from the headline under it; centred body text above a left-aligned title
     would have read as a stray line. */
  margin: 0 0 4px;
  text-align: left;
}
.modal-classifiers:empty { display: none; }

/* The header's far right, after .head-text — which is flex:1 and pushes it
   there. TWO buttons since 2026-08-07: Copy Text and Random, the two things in
   the header that ACT ON the card rather than adjust how it reads. The three on
   the left change this card; these two take you away from it or take it with
   you, and the split is the whole reason Random moved. */
.head-actions { display: flex; align-items: center; gap: 8px;
                align-self: center; margin-right: 14px; flex: 0 0 auto; }
#modal-copy-text { flex: 0 0 auto; }
/* THE SAME BUTTON AS THE OTHERS IN THIS ROW (Nepô, 2026-08-07). It was
   borderless and grey while everything beside it was a bordered pill on white,
   so it read as a caption rather than a control — the one thing in the header
   that did not look pressable. Matched to .modal-menu-btn and .pg81-rand:
   same height, same radius, same surface. */
.copy-text {
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  height: 30px; padding: 0 10px; border-radius: 15px; line-height: 1;
  border: 1px solid var(--border); background: rgba(255,255,255,0.92);
  color: var(--text-secondary); font-family: var(--font);
  font-size: 12px; font-weight: 700; white-space: nowrap;
  transition: border-color .15s, color .15s, background .15s;
}
.copy-text:hover { border-color: var(--text-tertiary); }
.copy-text .ms { font-size: 17px; }
.copy-text .ms { font-size: 16px; }
.copy-text.is-done   { color: #4c8a5a; border-color: #4c8a5a; }
.copy-text.is-failed { color: #b4693f; border-color: #b4693f; }

.file-strip {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  justify-content: center;
  padding: 10px 30px 0;
  font-size: calc(11.5px * var(--text-scale)); color: var(--text-tertiary);
}
.file-fact { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.file-fact .ms { font-size: 14px; }
/* The filename may be long; it alone may wrap inside itself. */
.file-fact:first-child { white-space: normal; word-break: break-all; }

/* The "+N" chip that stands in for the tags beyond the seventh. Outlined
   rather than filled, so it reads as a control and not as one more tag. */
.modal-tag--more {
  background: transparent !important;
  border: 1px dashed var(--border) !important;
  color: var(--text-tertiary) !important;
}
.modal-tag--more:hover { border-color: var(--text-secondary) !important; }

/* Facets in the panel — a facet name, then the tags that put this illustration
   in it. The name is a LABEL, not a chip: only the tags are clickable, because
   only the tags are searchable. */
/* TWO COLUMNS (Nepô, 2026-08-07). A facet row is a short name and one or two
   tags, so a single column left most of the panel's width empty and made a
   7-facet illustration scroll for no reason. `start` rather than stretch: rows
   have different heights and stretching them would space the tags apart to no
   purpose. The panel is full width below 760px, where two columns of ~170px
   would wrap every name, so the second breakpoint drops back to one. */
/* ── FACETS AS A TABLE (Nepô, 2026-08-07) ──────────────────────────────────
   Name in a fixed left column, values in a scroller on the right, a rule
   between rows. It replaces a two-column card grid tried earlier the same day:
   with the names inline the eye had to find where each row began, and two
   independent columns meant two different left edges to track.

   SCOPED TO .modal-facets, and that scoping is load-bearing: `.facet-row` and
   `.facet-name` are ALSO the grid's Filter-by-Type bar (#78), which is a
   wrapping row of chips and must stay one. Making the rules global would
   silently turn that bar into a table too. */
/* THE GRID IS THE TABLE, AND THE ROWS ARE `display: contents` — Nepô asked for
   the name on one line with the column sized to fit it (2026-08-07), and that
   is only possible this way. When each ROW was its own grid, `max-content`
   would size every row independently and the names would no longer line up:
   one table would become five little ones. Making .modal-facets the grid gives
   all rows ONE pair of tracks, so max-content measures the longest name across
   the whole table and every value starts on the same edge.

   The cost of display:contents is that the row element paints nothing, so the
   rule between rows moves onto the two CELLS. That is why the border is
   declared on .facet-name and .facet-scroll rather than on .facet-row. */
/* column-gap: 0, AND THE SPACE MOVES INTO THE NAME CELL (Nepô, 2026-08-07).
   The rows are display:contents, so the rule between rows is drawn by the two
   CELLS — and a gap between them is a stretch of row with nothing to draw it,
   which showed as a 12px break in every line. Padding inside the name cell
   keeps the same distance between the name and the chips while letting its
   border run all the way to the value column, so the lines are continuous. */
.modal-facets { display: grid; grid-template-columns: max-content minmax(0, 1fr);
                column-gap: 0; }
.modal-facets .facet-row { display: contents; }
/* align-self: STRETCH (the default), not center — and this is what actually
   makes the line continuous. The two cells have different heights (a chip is
   ~26px, a name ~15px), so centring them put their TOP EDGES 6.2px apart, and
   the border-top of each drew a separate line at a separate height: closing the
   column gap alone left a rule that stepped. Stretched, both boxes are the full
   row height, so both borders sit on the same y. The content is centred INSIDE
   each cell instead, which is what centring was for in the first place. */
.modal-facets .facet-name,
.modal-facets .facet-scroll {
  border-top: 1px solid var(--border); padding: 5px 0;
  display: flex; align-items: center;
}
.modal-facets .facet-row:first-child > * { border-top: 0; padding-top: 0; }
/* minmax(0,1fr) on the value track: a grid track's default min-width is auto,
   so the chips would push the column wider than the panel instead of scrolling
   inside it, and nothing would ever overflow — the arrows could never appear. */
.facet-row { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.facet-name {
  font-size: calc(10.5px * var(--text-scale)); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-tertiary);
  margin-right: 2px;
}
/* ONE LINE, ALWAYS. Without nowrap a two-word name wraps and the row grows a
   line taller than the chips beside it, so the rules stop being evenly spaced
   and the table reads as ragged. nowrap is also what makes max-content mean
   anything: a wrapping element's max-content width is its longest WORD. */
.modal-facets .facet-name { margin: 0; line-height: 1.3; white-space: nowrap;
                            padding-right: 12px; }

/* The scroller and its arrows. The arrows are always in the markup and are
   revealed by syncFacetArrows() only on rows that actually overflow, so a row
   with one tag costs no width for controls it does not need. */
.facet-scroll { position: relative; display: flex; align-items: center;
                gap: 2px; min-width: 0; }
.facet-vals { display: flex; gap: 5px; overflow-x: auto; min-width: 0;
              scrollbar-width: none; -ms-overflow-style: none; }
.facet-vals::-webkit-scrollbar { display: none; }
.facet-vals > * { flex: 0 0 auto; }
.facet-arrow {
  display: none; flex: 0 0 auto; width: 18px; height: 22px; padding: 0;
  align-items: center; justify-content: center; cursor: pointer;
  border: 0; background: none; color: var(--text-tertiary);
  font-family: var(--font); border-radius: 5px;
}
.facet-scroll.is-scrollable .facet-arrow { display: flex; }
.facet-arrow:hover:not(:disabled) { background: var(--surface2, #f5f2ed); color: var(--text-primary); }
.facet-arrow:disabled { opacity: .25; cursor: default; }
.facet-arrow .ms { font-size: 16px; line-height: 1; }
