/*!
 * Sonic — Sound · Audio · Acoustics
 * © 2026 Durand R. Begault & Michael Cohen. All rights reserved.
 * Companion site to the 1996 Sonic CD-ROM (Academic Press Professional).
 * https://mcohen.info/spatial-media/Sonic/
 */

/* ============================================
   SONIC — Modern Audio Education Platform
   Design System & Core Styles
   ============================================ */

/* --- Custom Properties --- */
:root {
  /* Colors — Light Mode */
  --color-bg: #f8f9fa;
  --color-surface: #ffffff;
  --color-surface-alt: #f0f2f5;
  --color-text: #1a1a2e;
  --color-text-secondary: #4a4a6a;
  /* WCAG AA needs 4.5:1 for normal text. The former #7a7a9a gave only
     4.14:1 on white and 3.92:1 on the alt surface — a fail. #646488
     keeps the same hue and gives 5.64:1 / 5.36:1, with margin. */
  --color-text-muted: #646488;
  --color-primary: #0d7377;
  --color-primary-light: #11999e;
  --color-primary-dark: #094f52;
  --color-accent: #e84545;
  --color-accent-light: #ff6b6b;
  --color-border: #e0e2e8;
  --color-border-light: #eef0f4;
  --color-code-bg: #f4f5f7;
  --color-highlight: #fff3cd;
  --color-warning-bg: #fff3cd;
  --color-warning-text: #856404;
  --color-success: #28a745;
  --color-link: #0d7377;
  --color-link-hover: #11999e;
  --color-nav-bg: #1a1a2e;
  --color-nav-text: #ccc;
  --color-nav-active: #11999e;

  /* Waveform / Viz Colors */
  --color-wave-primary: #11999e;
  --color-wave-secondary: #e84545;
  --color-wave-grid: #e0e2e8;
  --color-wave-bg: #1a1a2e;

  /* Demo-container pattern tokens (2026-07 refinement).
     Kept as derived aliases so the pattern references intent, not raw hex.
     --color-panel-rule is the teal-tinted hairline separating demo header
     from body; --color-focus-ring is used by every interactive control's
     :focus-visible state site-wide. */
  --color-panel-rule:   rgba(13, 115, 119, 0.35);
  --color-focus-ring:   var(--color-primary-light);

  /* Shadows */
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.1);
  --shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
  --shadow-glow: 0 0 20px rgba(17, 153, 158, 0.15);

  /* Typography */
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-heading: 'Space Grotesk', var(--font-body);
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-md: 1.125rem;
  --font-size-lg: 1.25rem;
  --font-size-xl: 1.5rem;
  --font-size-2xl: 2rem;
  --font-size-3xl: 2.5rem;
  --font-size-4xl: 3.5rem;
  --line-height: 1.7;

  /* Spacing */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: 4rem;

  /* Layout */
  --content-width: 780px;
  --nav-width: 280px;
  --border-radius: 8px;
  --border-radius-lg: 12px;
  --border-radius-xl: 16px;

  /* Transitions */
  --transition-fast: 150ms ease;
  --transition-base: 250ms ease;
  --transition-slow: 400ms ease;
}

/* --- Dark Mode --- */
[data-theme="dark"] {
  --color-bg: #0f0f1a;
  --color-surface: #1a1a2e;
  --color-surface-alt: #16213e;
  --color-text: #e8e8f0;
  --color-text-secondary: #b0b0cc;
  /* Dark surfaces need the muted tone lighter, not darker: #a0a0bd gives
     6.71:1 on #1a1a2e and 7.48:1 on the page background. */
  --color-text-muted: #a0a0bd;
  --color-border: #2a2a45;
  --color-border-light: #222240;
  --color-code-bg: #16213e;
  --color-highlight: #3d3200;
  --color-warning-bg: #3d3200;
  --color-warning-text: #ffc107;
  --color-link: #11999e;
  --color-link-hover: #40c4c9;
  --color-nav-bg: #0a0a15;
  --color-wave-grid: #2a2a45;
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
  --shadow-lg: 0 8px 30px rgba(0,0,0,0.5);
  --shadow-glow: 0 0 20px rgba(17, 153, 158, 0.25);
  --color-panel-rule: rgba(17, 153, 158, 0.4);
}

/* --- Reset & Base --- */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-body);
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* --- Typography --- */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  line-height: 1.3;
  color: var(--color-text);
  margin-bottom: var(--space-md);
}

h1 { font-size: var(--font-size-3xl); font-weight: 700; }
h2 { font-size: var(--font-size-2xl); font-weight: 700; }
h3 { font-size: var(--font-size-xl); font-weight: 600; }
h4 { font-size: var(--font-size-lg); font-weight: 600; }

p {
  margin-bottom: var(--space-lg);
  color: var(--color-text-secondary);
}

a {
  color: var(--color-link);
  text-decoration: none;
  transition: color var(--transition-fast);
}
a:hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

em { font-style: italic; }
strong { font-weight: 600; }

/* Superscripts/subscripts (e.g., ordinals like 1st, 2nd, "8ve") should always
   render in their source case, even inside parents that uppercase their text
   (such as .control-label, .nav-section-label, .chapter-number, etc.).
   A small negative margin-left pulls the smaller superscript flush against the
   preceding character (otherwise font-metric whitespace leaves a visible gap). */
sup, sub {
  text-transform: none;
  margin-left: -0.12em;
  padding-left: 0;
  line-height: 0;
}

.term {
  color: var(--color-accent);
  font-weight: 600;
  cursor: help;
  border-bottom: 1px dashed var(--color-accent);
  text-decoration: none;
}
a.term:hover {
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  border-bottom-style: solid;
  text-decoration: none;
}

/* --- Glossary page --- */
.glossary-index a {
  color: var(--color-accent);
  text-decoration: none;
  padding: 0 0.15em;
}
.glossary-index a:hover {
  text-decoration: underline;
}
.glossary-group {
  margin-bottom: var(--space-2xl);
}
.glossary-letter {
  color: var(--color-accent);
  border-bottom: 1px solid var(--color-border);
  padding-bottom: var(--space-xs);
  margin-bottom: var(--space-md);
}
.glossary-list {
  margin: 0;
}
.glossary-list dt {
  font-weight: 600;
  margin-top: var(--space-md);
  scroll-margin-top: var(--space-xl);
}
.glossary-list dd {
  margin: 0 0 var(--space-xs) 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
[id^="term-anchor-"] {
  scroll-margin-top: var(--space-xl);
}

/* Deep-links from the chapter-0 demo ToC and from What's New target the
   demo's outer container; without scroll-margin-top the demo's title
   lands flush against the viewport edge, so users read it as "I'm
   slightly above the demo" and scroll down looking for it. Reserve a
   generous offset so the demo title sits clearly inside the viewport
   with a strip of whitespace above it. */
.demo-container[id],
.harmonic-figure[id],
.harmonics-decomp-figure[id] {
  scroll-margin-top: var(--space-2xl);
}

/* Figure anchors sit just above the figure body so a link to #fig-X-Y
   scrolls the body itself (not the caption underneath) into view. A
   modest top margin keeps the figure from butting against the viewport
   edge. */
/* A heading that carries an id is now a jump target in its own right — the
   section ids moved from their <section> wrappers onto their headings so the
   site search can deep-link to them, and each demo title gained one too. Give
   them the same breathing room the empty figure anchors have, or a jump lands
   with the heading flush against the top edge. */
.section-title[id],
.demo-title[id] { scroll-margin-top: var(--space-xl); }

.figure-anchor {
  scroll-margin-top: var(--space-xl);
}

/* --- Layout: Site Shell --- */
.site-wrapper {
  min-height: 100vh;
}

/* --- Sidebar Navigation --- */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--nav-width);
  height: 100vh;
  /* dvh is the dynamic viewport height — on iOS Safari, 100vh includes the
     URL bar's screen real estate so a 100vh sidebar overflows the visible
     viewport (pushing the Extras / footer section off-screen with no
     scroll hint). dvh tracks the actual visible viewport, so the sidebar
     stays fully on-screen as the URL bar collapses/expands. */
  height: 100dvh;
  background: var(--color-nav-bg);
  color: var(--color-nav-text);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;   /* momentum scrolling on iOS */
  z-index: 100;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--color-border);
  transition: transform var(--transition-base);
}

.sidebar-header {
  padding: var(--space-xl) var(--space-lg);
  border-bottom: 1px solid rgba(255,255,255,0.08);
}

.sidebar-logo {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  text-decoration: none;
  color: #fff;
}

.sidebar-logo svg {
  width: 36px;
  height: 36px;
}

.sidebar-logo-text {
  font-family: var(--font-heading);
  font-size: var(--font-size-xl);
  font-weight: 700;
  letter-spacing: -0.02em;
}

.sidebar-logo-sub {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  display: block;
  margin-top: 2px;
}

.sidebar-nav {
  flex: 1;
  padding: var(--space-lg) 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
@media (max-width: 900px) {
  /* Tighten vertical spacing inside the sidebar on mobile so the full nav
     (including the Extras section with Shepard / glossary / etc) fits in
     a typical phone viewport without scrolling. iOS Safari users were
     missing the Extras section because the visible viewport ended at the
     end of "Advanced" and the scroll-hint wasn't discoverable. */
  .sidebar-nav { padding: var(--space-sm) 0; }
  .nav-section-label { padding: var(--space-sm) var(--space-lg) calc(var(--space-xs) / 2); }
  .nav-item { padding: var(--space-xs) var(--space-lg); }
}

.nav-section-label {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-text-muted);
  padding: var(--space-md) var(--space-lg) var(--space-sm);
  font-weight: 600;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  color: var(--color-nav-text);
  text-decoration: none;
  font-size: var(--font-size-sm);
  transition: all var(--transition-fast);
  border-left: 3px solid transparent;
  position: relative;
}

.nav-item:hover {
  background: rgba(255,255,255,0.05);
  color: #fff;
  text-decoration: none;
}

.nav-item.active {
  color: var(--color-nav-active);
  border-left-color: var(--color-nav-active);
  background: rgba(17, 153, 158, 0.08);
}

.nav-item .nav-number {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  opacity: 0.5;
  min-width: 1.5em;
}

.nav-progress {
  position: absolute;
  right: var(--space-lg);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-primary);
  opacity: 0;
  transition: opacity var(--transition-fast);
}

.nav-item.completed .nav-progress {
  opacity: 1;
  background: var(--color-success);
}

/* Sidebar footer */
.sidebar-footer {
  padding: var(--space-lg);
  border-top: 1px solid rgba(255,255,255,0.08);
}

.theme-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  background: none;
  border: 1px solid rgba(255,255,255,0.15);
  color: var(--color-nav-text);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: var(--font-size-sm);
  width: 100%;
  transition: all var(--transition-fast);
}

.theme-toggle:hover {
  background: rgba(255,255,255,0.08);
  border-color: rgba(255,255,255,0.25);
}

/* Sidebar toggle — always visible at all widths so the ToC can be hidden
   on demand (e.g. to reclaim horizontal space on tablets when a wide
   demo doesn't fit alongside the ToC). */
.mobile-nav-toggle {
  display: flex;
  position: fixed;
  top: var(--space-md);
  left: var(--space-md);
  z-index: 200;
  background: var(--color-nav-bg);
  border: 1px solid var(--color-border);
  color: #fff;
  width: 44px;
  height: 44px;
  border-radius: var(--border-radius);
  cursor: pointer;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  transition: transform var(--transition-base);
}

/* Floating theme toggle — always visible at top-right of the viewport so
   the user can switch dark/light even when the ToC sidebar is collapsed.
   Uses the brand teal so it stands out in both light AND dark themes
   (the nav-bg color is dark-on-dark in dark mode and disappears). */
.theme-toggle-fab {
  display: flex;
  position: fixed;
  top: var(--space-md);
  right: var(--space-md);
  z-index: 250;
  background: var(--color-primary-light);
  border: 2px solid var(--color-primary-dark);
  color: #fff;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
  transition: transform var(--transition-fast), background var(--transition-fast);
}
.theme-toggle-fab:hover {
  background: var(--color-primary-dark);
  transform: scale(1.08);
}
.theme-toggle-fab:active { transform: scale(0.95); }

/* Sidebar collapse: visible by default at desktop, hidden at mobile.
   The `body.sidebar-collapsed` class force-hides at any width; `body.sidebar-open`
   force-shows. JS reconciles the two with a single source of truth in localStorage. */
.sidebar { transition: transform var(--transition-base); }

body.sidebar-collapsed .sidebar {
  transform: translateX(-100%);
}
body.sidebar-collapsed .main-content {
  margin-left: 0;
}

@media (max-width: 900px) {
  .sidebar {
    transform: translateX(-100%);
  }
  body.sidebar-open .sidebar {
    transform: translateX(0);
    box-shadow: var(--shadow-lg);
  }
}

/* --- Main Content Area --- */
.main-content {
  margin-left: var(--nav-width);
  min-height: 100vh;
  transition: margin-left var(--transition-base);
}

@media (max-width: 900px) {
  .main-content {
    margin-left: 0;
  }
}

/* Chapter content */
.chapter-content {
  max-width: var(--content-width);
  margin: 0 auto;
  padding: var(--space-3xl) var(--space-xl);
}

@media (max-width: 900px) {
  .chapter-content {
    padding: calc(var(--space-3xl) + 44px) var(--space-lg) var(--space-3xl);
  }
}

/* Wider screens: gradually relax the content-column cap so interactive
   demos and figures have more room to breathe, while keeping prose
   line-lengths comfortable (typographic sweet-spot is 60–90 characters).
   Tiered breakpoints rather than a single linear scale, so each tier is
   a well-tested layout. Bumps the CSS var --content-width via :root
   overrides at min-width media queries. */
@media (min-width: 1200px) { :root { --content-width:  920px; } }
@media (min-width: 1500px) { :root { --content-width: 1040px; } }
@media (min-width: 1800px) { :root { --content-width: 1160px; } }
@media (min-width: 2200px) { :root { --content-width: 1280px; } }

/* When the user collapses the ToC sidebar, the main-content area also
   gains the 280px the sidebar was using — bump the column up another
   tier so that space turns into usable width too. */
body.sidebar-collapsed { --content-width: 1080px; }
@media (min-width: 1500px) { body.sidebar-collapsed { --content-width: 1200px; } }
@media (min-width: 1800px) { body.sidebar-collapsed { --content-width: 1360px; } }

.chapter-header {
  margin-bottom: var(--space-3xl);
  padding-bottom: var(--space-xl);
  border-bottom: 1px solid var(--color-border);
}

.chapter-number {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-primary);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: var(--space-sm);
  display: block;
}

.chapter-title {
  font-size: var(--font-size-3xl);
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: var(--space-md);
}

.chapter-subtitle {
  font-size: var(--font-size-md);
  color: var(--color-text-muted);
  max-width: 720px;
}

/* Section headings */
.section-title {
  font-size: var(--font-size-xl);
  margin-top: var(--space-3xl);
  margin-bottom: var(--space-lg);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-light);
}

.subsection-title {
  font-size: var(--font-size-lg);
  margin-top: var(--space-2xl);
  margin-bottom: var(--space-md);
}

/* --- Figures --- */
.figure {
  margin: var(--space-2xl) 0;
  text-align: center;
}

.figure img {
  max-width: 100%;
  height: auto;
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-sm);
}

[data-theme="dark"] .figure img {
  filter: invert(0.88) hue-rotate(180deg);
}

.figure-caption {
  margin-top: var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.figure-caption strong {
  color: var(--color-text-secondary);
}

/* --- Tables --- */
.data-table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--space-xl) 0;
  font-size: var(--font-size-sm);
  border-radius: var(--border-radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.data-table thead th {
  background: var(--color-primary-dark);
  color: #fff;
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  font-weight: 600;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.data-table tbody td {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-border-light);
  background: var(--color-surface);
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

.data-table tbody tr:hover td {
  background: var(--color-surface-alt);
}

/* --- German gloss reveal (site-wide opt-in, parallel to Japanese) --------
   Same mechanism as .jp-gloss / data-jp but for German. The two toggles
   are independent: a reader can turn on JP, DE, both, or neither.
   Markup: <span class="de-gloss" lang="de"> (Frequenz)</span> */
.de-gloss { display: none; }
:root[data-de="on"] .de-gloss { display: inline; }
.de-gloss { font-style: italic; }
dt .de-gloss {
  font-weight: normal;
  font-style: italic;
  color: var(--color-text-muted);
  font-size: 0.92em;
  margin-left: 0.35em;
}
.de-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  margin-top: 6px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
  width: 100%;
  justify-content: center;
}
.de-toggle:hover { background: var(--color-surface-alt); }
.de-toggle[aria-pressed="true"] {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}

/* --- Japanese gloss reveal (site-wide opt-in) ----------------------------
   Authors can annotate any English term with a Japanese equivalent (often
   with furigana via <ruby>kanji<rt>reading</rt></ruby>). The gloss is
   hidden by default — the .jp-toggle in the sidebar (or the floating FAB)
   sets data-jp="on" on <html> via app.js, which reveals every .jp-gloss
   inline. Pattern:
     ... directivity<span class="jp-gloss"> (<ruby>指向性<rt>しこうせい</rt></ruby>)</span> ...
   When off the surrounding prose reads natively; when on the parenthesised
   gloss appears in flow after the English term.                              */
.jp-gloss { display: none; }
:root[data-jp="on"] .jp-gloss { display: inline; }
.jp-gloss ruby { font-size: inherit; }
.jp-gloss rt {
  font-size: 0.55em;
  color: var(--color-text-muted);
  letter-spacing: 0.05em;
  /* small extra breathing room above the kanji so the furigana
     doesn't collide with the descender of the line above */
  line-height: 1.1;
}
/* Glossary hover-popup: the JP / DE glosses inside it follow the
   site-wide toggles (no special override). The global rules
   :root[data-jp="on"] .jp-gloss { display: inline } and
   :root[data-de="on"] .de-gloss { display: inline } already match —
   the popup is appended to document.body, so it sits inside <html> and
   inherits the data-jp / data-de state. The popup's <strong> term row
   still needs the taller line-height so any furigana <rt> (when JP is
   ON) doesn't collide with the popup border. */
.glossary-popup strong { line-height: 2.0; display: inline-block; }
.glossary-popup ruby rt {
  font-size: 0.55em;
  color: var(--color-text-muted);
  letter-spacing: 0.05em;
}
/* In glossary <dt> terms, give the gloss a touch of separation */
dt .jp-gloss {
  font-weight: normal;
  color: var(--color-text-muted);
  font-size: 0.92em;
  margin-left: 0.35em;
}
/* Sidebar toggle button — mirrors .theme-toggle visual language */
.jp-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  margin-top: 6px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
  width: 100%;
  justify-content: center;
}
.jp-toggle:hover { background: var(--color-surface-alt); }
.jp-toggle[aria-pressed="true"] {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}

/* --- Source-picker popup (alternative to .sonic-src-btn row) -------------
   Used by demos that opt in via { usePopup: true } in renderSourcePicker.
   Native <select>, styled to match the rest of the demo chrome. */
.sonic-src-select {
  font: 14px Inter, system-ui, sans-serif;
  padding: 5px 10px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  min-width: 200px;
  max-width: 100%;
}
.sonic-src-select:hover,
.sonic-src-select:focus { border-color: var(--color-primary); outline: none; }

/* --- Slider end-labels --------------------------------------------------
   Authors can give any <input type="range"> two tiny end-labels by adding
   data-min-label="…" and data-max-label="…" attributes. A site-wide JS
   pass (initSliderEndLabels in app.js) injects the markup below the
   slider so the polarity is immediately obvious without relying on the
   reading order of the main label. */
.slider-end-labels {
  display: flex;
  justify-content: space-between;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-top: 1px;
  /* Pad in by half the slider-thumb width (18 px → 9 px) so the labels
     sit under the track endpoints, not the thumb's outer bounding box.
     Without this the labels visually drift past the visible track. */
  padding: 0 9px;
  letter-spacing: 0.03em;
  pointer-events: none;
}
.slider-end-labels .slider-end-min { text-align: left; }
.slider-end-labels .slider-end-max { text-align: right; }

/* --- Keyboard-shortcut hint floating above a button --------------------
   The <kbd> sits visually above its corresponding button so the row of
   buttons stays uncluttered and the shortcut is still legible. The hint
   is absolutely positioned — the wrapper claims only the button's own
   height in flow, so a single shortcut-tagged button mid-row no longer
   pushes the whole transport row taller than its un-tagged neighbours
   (the previous column-flex layout did push it down). Wrap the button
   + kbd in a .btn-with-kbd span. */
.btn-with-kbd {
  display: inline-block;
  position: relative;
  vertical-align: middle;
}
.btn-kbd-hint {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 2px;
  font-size: 0.65em;
  padding: 1px 5px;
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-family: var(--font-mono, ui-monospace, monospace);
  white-space: nowrap;
  line-height: 1.2;
  pointer-events: none;        /* never intercept clicks meant for the button below */
}

/* Glossary back-link to the chapter where the term first appears. */
.dd-source {
  display: block;
  font-size: 0.85em;
  color: var(--color-text-muted);
  margin-top: 0.3em;
}

/* --- Bibliography entries: cover thumbnail + text in flex row.
   Cover images come from Open Library's ISBN-keyed cover API; the
   inline `onerror` hides the <img> when no cover exists, so entries
   without an Open Library record fall back gracefully to text-only.
   Local cover files placed at images/bib/<bib-id>.jpg would be
   preferred by future enhancement; for now we rely on the network
   source.                                                          */
.bib-entry {
  display: flex;
  gap: var(--space-md);
  align-items: flex-start;
  margin-bottom: var(--space-md);
}
.bib-cover {
  width: 100px;
  max-width: 100px;
  flex-shrink: 0;
  height: auto;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.20);
  background: var(--color-surface-2, #f4f4f6);
}
.bib-text {
  flex: 1;
  min-width: 0;
}
.bib-text > p {
  margin: 0;
}
@media (max-width: 540px) {
  .bib-cover { width: 70px; max-width: 70px; }
}

/* --- iOS Apps page: each <li> shows a small square app icon next to the
   linked name. Icons are downloaded at iTunes Lookup API resolution
   (100×100) and saved to images/apps/<app-store-id>.jpg.
   The rounded-square mask matches Apple's iOS app-icon convention.   */
.app-icon {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  margin-right: 10px;
  vertical-align: middle;
  box-shadow: 0 1px 3px rgba(0,0,0,0.15);
  background: var(--color-surface-2, #f4f4f6);
  flex-shrink: 0;
}

/* Inline favicon next to external resource links on resources.html.
   Sized to read at body-font height; rendered with a soft background so
   transparent-on-white site icons survive dark-mode without bleeding. */
.site-icon {
  width: 18px;
  height: 18px;
  vertical-align: -4px;
  margin-right: 4px;
  border-radius: 3px;
  background: var(--color-surface-2, #f4f4f6);
  object-fit: contain;
  flex-shrink: 0;
}

/* --- Generic audio-sample matrix --- */
/* Used wherever a set of audio-demo buttons can be naturally arranged as
   a 2D matrix exposing orthogonal axes (e.g. chapter 7 HPF/LPF × cutoff,
   chapter 9 position × IR-component). Number of columns is controlled
   via inline style on .snd-matrix-grid: grid-template-columns: auto repeat(N, 1fr). */
.snd-matrix {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--space-md);
  margin: var(--space-md) 0;
}
.snd-matrix-title {
  font-weight: 700;
  font-size: var(--font-size-lg);
  color: var(--color-primary);
  margin-bottom: var(--space-xs);
}
.snd-matrix-desc {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-sm);
}
.snd-matrix-grid {
  display: grid;
  gap: 4px 8px;
  align-items: center;
}
.snd-matrix-corner {}
.snd-matrix-colhead {
  text-align: center;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--color-border);
  padding-bottom: 4px;
}
.snd-matrix-rowhead {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text);
  padding-right: var(--space-sm);
  border-right: 1px solid var(--color-border);
}
.snd-matrix-rowhead-sub {
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-text-muted);
}
.snd-matrix-cell {
  display: flex;
  justify-content: center;
}
.snd-matrix-cell a, .snd-matrix-cell button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 36px;
  padding: 4px 10px;
  border: none;
  border-radius: 6px;
  background: var(--color-surface-alt);
  color: var(--color-primary);
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s, transform 0.08s;
}
.snd-matrix-cell a:hover,
.snd-matrix-cell a:focus,
.snd-matrix-cell button:hover,
.snd-matrix-cell button:focus {
  background: var(--color-primary);
  color: #fff;
}
.snd-matrix-cell a:active,
.snd-matrix-cell button:active { transform: scale(0.96); }
/* Span both columns / center the "shared baseline" sample under a header */
.snd-matrix-baseline {
  grid-column: 1 / -1;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* --- Icon-only play button (used inside data-tables) -------------------
   When a data-sound link's visible content is just the ▶ glyph (no text
   label), the inherited .audio-link underline + plain-text styling reads
   as clutter. Use this class to opt out of the underline and present the
   link as a small outlined pill, so the play affordance is immediate.
   Also supports a combined cell — e.g. duration text plus inline icon —
   via the .play-pill variant that styles a button-shaped wrapper.       */
.play-icon-btn,
a.audio-link.play-icon-btn {                 /* override .audio-link */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 30px;
  padding: 3px 10px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-primary);
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s, transform 0.08s;
}
.play-icon-btn:hover,
.play-icon-btn:focus {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
  text-decoration: none;
}
.play-icon-btn:active { transform: scale(0.96); }

/* .play-pill = button that contains both an icon AND a text label
   (e.g. "▶ 0.3 sec"). Reads as data + affordance in one cell. */
.play-pill,
a.audio-link.play-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s, transform 0.08s;
}
.play-pill .play-glyph {
  color: var(--color-primary);
  font-size: 14px;
  line-height: 1;
}
.play-pill:hover, .play-pill:focus {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
  text-decoration: none;
}
.play-pill:hover .play-glyph,
.play-pill:focus .play-glyph { color: #fff; }
.play-pill:active { transform: scale(0.97); }

/* --- Chapter 3: same-pitch-spectra (6 samples + per-sample spectrogram) --- */
/* 6 vertical cells: button on top, spectrogram canvas below. On narrow
   viewports collapses to 3 columns then 2. Each canvas freezes at the end
   of its sample so all 6 spectrograms remain visible for comparison. */
.same-pitch-spectra {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-sm);
  margin: var(--space-sm) 0;
}
@media (max-width: 720px) {
  .same-pitch-spectra { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 380px) {
  .same-pitch-spectra { grid-template-columns: repeat(2, 1fr); }
}
.sps-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.sps-cell .sps-btn {
  width: 100%;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 4px;
}
.sps-cell .sps-btn[aria-pressed="true"] {
  background: var(--color-primary);
  color: #fff;
}
.sps-cell .sps-sub {
  font-size: 0.7em;
  font-weight: 400;
  opacity: 0.75;
  font-style: italic;
  letter-spacing: 0;
  text-transform: none;
}
.sps-spec {
  width: 100%;
  height: 80px;
  background: #0e1a26;
  border-radius: 4px;
  display: block;
}

/* --- Fig 7.6: piano / cymbal play buttons integrated into the figure --- */
/* Two-column row sitting tight under the figure image (which has piano on
   the left half and cymbal on the right half). Visually each button sits
   directly below the half of the image that depicts its sound. */
.fig76-play-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  margin: var(--space-xs) 0 var(--space-sm);
}
.fig76-play-row .fig76-play {
  width: 100%;
}

/* --- Fig 7.11: Side-by-side delay-example matrices --- */
/* Two 2D grids (Piano | Speech), each with rows = delay time
   and columns = mono / stereo, so a reader can compare across rows
   (same delay, two source materials) or down columns (delay-time
   perceptual category) at a glance. */
.fig711-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-lg);
  margin: var(--space-md) 0;
}
@media (max-width: 640px) {
  .fig711-pair { grid-template-columns: 1fr; }
}
.fig711-grid {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--space-md);
}
.fig711-grid-title {
  font-weight: 700;
  font-size: var(--font-size-lg);
  color: var(--color-primary);
  margin-bottom: var(--space-xs);
}
.fig711-original {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
}
.fig711-original a { font-weight: 600; }
.fig711-matrix {
  display: grid;
  grid-template-columns: auto 1fr 1fr;
  gap: 4px 8px;
  align-items: center;
}
.fig711-corner { /* top-left of header rail */ }
.fig711-colhead {
  text-align: center;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: lowercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--color-border);
  padding-bottom: 4px;
}
.fig711-rowhead {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text);
  padding-right: var(--space-sm);
  border-right: 1px solid var(--color-border);
}
.fig711-amp {
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-text-muted);
}
.fig711-cell {
  display: flex;
  justify-content: center;
}
.fig711-cell a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 36px;
  padding: 4px 10px;
  border-radius: 6px;
  background: var(--color-surface-alt);
  color: var(--color-primary);
  font-size: 17px;
  line-height: 1;
  text-decoration: none;
  transition: background 0.12s, transform 0.08s;
}
.fig711-cell a:hover,
.fig711-cell a:focus {
  background: var(--color-primary);
  color: #fff;
}
.fig711-cell a:active { transform: scale(0.96); }

/* --- Inline Audio Player --- */
.audio-player {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-lg);
  padding: var(--space-sm) var(--space-md);
  margin: var(--space-md) 0;
  transition: all var(--transition-base);
  box-shadow: var(--shadow-sm);
}

.audio-player:hover {
  box-shadow: var(--shadow-md);
  border-color: var(--color-primary-light);
}

.audio-player.playing {
  border-color: var(--color-primary);
  box-shadow: var(--shadow-glow);
}

.audio-play-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--color-primary);
  border: none;
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: all var(--transition-fast);
}

.audio-play-btn:hover {
  background: var(--color-primary-light);
  transform: scale(1.05);
}

.audio-play-btn svg {
  width: 18px;
  height: 18px;
}

.audio-info {
  flex: 1;
  min-width: 0;
}

.audio-label {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.audio-sublabel {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.audio-waveform-mini {
  width: 120px;
  height: 32px;
  flex-shrink: 0;
}

.audio-progress-bar {
  flex: 1;
  height: 4px;
  background: var(--color-border);
  border-radius: 2px;
  overflow: hidden;
  cursor: pointer;
}

.audio-progress-fill {
  height: 100%;
  background: var(--color-primary);
  border-radius: 2px;
  width: 0%;
  transition: width 100ms linear;
}

.audio-time {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  flex-shrink: 0;
}

/* Audio player group — for side-by-side comparisons */
.audio-group {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-sm);
  margin: var(--space-lg) 0;
}

.audio-group .audio-player {
  margin: 0;
}

/* Reset native <button> chrome for audio-trigger buttons so a
   <button class="audio-link"> renders as inline text identical to the
   previous <a href="#" class="audio-link">. Class-specific rules below
   then re-introduce padding / borders / backgrounds where the variants
   (.play-icon-btn, .play-pill) want a pill-shaped chip. */
button.audio-link,
button.play-icon-btn,
button.play-pill {
  font: inherit;
  background: transparent;
  border: none;
  margin: 0;
  padding: 0;
  line-height: inherit;
  text-align: inherit;
  color: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}

/* Small region-code chip that appears next to a US App Store link for
   visitors whose browser locale isn't US — e.g. "· jp" for a Japanese
   visitor. Injected by app.js. Reads as a quiet hint, not a CTA. */
.app-store-region-chip {
  display: inline-block;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.7em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  text-decoration: none;
  padding: 1px 5px;
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-surface);
  vertical-align: 0.1em;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.app-store-region-chip:hover,
.app-store-region-chip:focus-visible {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
  text-decoration: none;
}

/* Pop-up source selector — used by demos that accept more than a few
   audio inputs (Fig 9.3 spatializer, etc.) instead of a row of always-
   visible buttons. Native <select> with <optgroup> gives a real OS pop-up
   on every platform with zero JS for the disclosure, and accessibility
   for free. Keeps the layout compact regardless of how many sources are
   added later. */
.source-selector {
  font: inherit;
  font-size: var(--font-size-sm);
  padding: 6px 10px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  min-width: 240px;
  max-width: 100%;
  appearance: menulist;
  -webkit-appearance: menulist;
}
.source-selector:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.source-selector optgroup {
  font-weight: 600;
  font-style: normal;
}

/* Numeric value displays in slider labels (e.g. "0°", "-180°", "120 bpm").
   Without these the layout reflows on every slider tick because the digit
   string changes width — especially noticeable when head-tracking drives
   the azimuth at ~20 fps. tabular-nums fixes digit-glyph width; the
   min-width reserves space for the widest expected value (sign + 3
   digits + unit). Right-align so the numerals stack cleanly. */
.control-label [data-value],
[data-value-host] {
  display: inline-block;
  font-variant-numeric: tabular-nums;
  min-width: 3.2em;
  text-align: right;
}

/* Inline "click here" audio links */
.audio-link {
  color: var(--color-primary);
  font-weight: 600;
  cursor: pointer;
  border-bottom: 2px solid var(--color-primary-light);
  transition: all var(--transition-fast);
  padding: 0 2px;
}

.audio-link:hover {
  background: rgba(17, 153, 158, 0.1);
  text-decoration: none;
}

.audio-link.playing {
  background: rgba(17, 153, 158, 0.15);
  color: var(--color-primary-dark);
}

/* --- Interactive Demo Containers --- */
/* --- Demo-container pattern (2026-07 refinement) ---
   Each demo is a numbered lab station. Header carries a mono figure-number
   with a hairline connector to the sandbox name; body wraps controls in a
   12-column grid that collapses to a single column on phones. See CLAUDE.md
   §"Demo container pattern" for the vocabulary and when to reach for each
   modifier class. */
.demo-container {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-lg);
  padding: 0;                         /* padding moved inside .demo-header / .demo-body */
  margin: var(--space-2xl) 0;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.demo-header {
  display: grid;
  /* Third column for the share control. It collapses to nothing when the
     header has only the figure number and title, so existing headers are
     unaffected. */
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  column-gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-panel-rule);
  background: linear-gradient(180deg, transparent, rgba(13, 115, 119, 0.03));
}
/* A header still on the older single-title markup puts that title in the first
   column — the narrow `auto` one sized for a figure number — so a long title
   was squeezed into a tall, thin stack beside the share control. Until such a
   header is migrated to figno + title, let its title take both columns. */
.demo-header > .demo-title:first-child { grid-column: 1 / 3; }

/* Twenty-five headers across seven chapters carry the demo's description inside
   the header rather than in the body. As a grid item that paragraph claimed a
   column of its own, and because an `auto` column is sized from content before
   `1fr` gets the remainder, the prose took ~390 px while the title it belongs to
   was left with ~110 px — a long name then wrapped into a narrow, tall stack
   (Figure 1.8b was the reported case). Give the description its own full-width
   row instead. `order` moves it after the share control for placement only, so
   the share still lands in the header row where it belongs; reading order and
   the DOM are untouched. */
.demo-header > .demo-description {
  order: 1;
  grid-column: 1 / -1;
  margin-block: var(--space-xs) 0;
}

.demo-title {
  font-family: var(--font-heading);
  font-size: var(--font-size-lg);
  font-weight: 600;
  color: var(--color-text);
  letter-spacing: -0.01em;
  margin: 0;
}

/* Mono "part-number" label sitting to the left of the sandbox name, joined
   by a hairline connector. Below 480 px the connector hides and the number
   moves above the name so a long title has room to breathe. */
.demo-figno {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--color-primary);
  white-space: nowrap;
}
.demo-figno::after {
  content: "";
  display: none;                     /* hidden on phone portrait by default */
  width: 2em;
  border-top: 1px solid var(--color-panel-rule);
  vertical-align: middle;
  margin: 0 0.5em;
}
@media (min-width: 640px) {
  .demo-figno::after { display: inline-block; }
  .demo-header       { padding: var(--space-md) var(--space-xl); }
}
@media (max-width: 480px) {
  .demo-header {
    grid-template-columns: 1fr;
    row-gap: var(--space-xs);
  }
}

/* Demo body wraps the visualization + controls with padding that tightens on
   phones (32 px eats too much of a 375 px viewport). */
.demo-body {
  padding: var(--space-md);
}
@media (min-width: 640px) {
  .demo-body { padding: var(--space-lg) var(--space-xl) var(--space-xl); }
}

/* Legacy: existing demos still have the .demo-badge span. Left as no-op
   so the class name doesn't 404, but visually removed — its content
   duplicated the play-button label. Safe to strip from HTML in a later pass. */
.demo-badge {
  display: none;
}

/* Canvas for visualizations */
.viz-canvas {
  width: 100%;
  height: 200px;
  background: var(--color-wave-bg);
  border-radius: var(--border-radius);
  display: block;
}

/* Controls: mobile-first grid. Phones stack every group vertically; tablet
   and up switch to a 12-column layout so pairs of sliders sit side-by-side.
   Individual demos declare intent via modifier classes rather than reaching
   for inline `style="grid-column: 1 / -1"` — see .control-group--full etc. */
.demo-controls {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
  margin-top: var(--space-lg);
  align-items: start;
}
@media (min-width: 640px) {
  .demo-controls {
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--space-md);
  }
}

.control-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;                       /* let children shrink inside the grid cell */
}
@media (min-width: 640px) {
  .control-group          { grid-column: span 6; }
  .control-group--full    { grid-column: 1 / -1; }
  .control-group--third   { grid-column: span 4; }
  .control-group--quarter { grid-column: span 3; }
}

/* Label + value form a legend / tape pair. Label text on the left in Inter 500
   sentence-case (was uppercase-tracked, which shouted and forced every value
   span to opt out with text-transform:none). Value is pushed to the right
   edge via margin-left:auto — works with existing markup where the [data-value]
   span is the last child of the label. */
.control-label {
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  font-weight: 500;
  letter-spacing: normal;
  color: var(--color-text-secondary);
  text-transform: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-md);
  margin: 0;
}
/* Ordinal number + superscript (1st, 2nd, 3rd, …). Wrapped so the digit
   and its suffix stay one flex item inside .control-label (otherwise the
   parent's `display: flex; gap: …` splits them and the sup loses its
   baseline offset). */
.control-label .ord { display: inline; white-space: nowrap; }
.control-label .ord sup { font-size: 0.72em; vertical-align: super; line-height: 0; }
.control-label [data-value],
.control-label .sonic-active-label,
[data-value] {
  font-family: var(--font-mono);
  font-weight: 500;
  color: var(--color-primary);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  text-transform: none;
  letter-spacing: normal;
  margin-left: auto;                  /* push readout to the right of the row */
}

.control-value {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-primary);
}

/* Range sliders */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  /* Fill the parent's width by default. Slider rows that need a fixed
     compact width (e.g. inline next to a label) can opt back to a
     specific width via a wrapping container; we no longer hard-code
     180 px here because doing so misaligned the end-labels and
     explanation row, which fill the full control-group. */
  width: 100%;
  height: 6px;
  background: var(--color-border);
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-primary);
  cursor: pointer;
  border: 2px solid #fff;
  box-shadow: var(--shadow-sm);
  transition: transform var(--transition-fast);
}

input[type="range"]::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-primary);
  cursor: pointer;
  border: 2px solid #fff;
  box-shadow: var(--shadow-sm);
}

/* Visible focus ring for keyboard users (no effect on touch or mouse click). */
input[type="range"]:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 4px;
  border-radius: 3px;
}
input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px rgba(17, 153, 158, 0.20), var(--shadow-sm);
}

/* Bigger touch targets on coarse pointers (phones, tablets). Apple HIG says
   44 pt minimum; Android says 48; the previous 18 px thumb was undersized
   for either. Doesn't affect desktop mouse use. */
@media (pointer: coarse) {
  input[type="range"] { height: 8px; }
  input[type="range"]::-webkit-slider-thumb { width: 24px; height: 24px; }
  input[type="range"]::-moz-range-thumb    { width: 24px; height: 24px; }
}

/* Respect users who've asked their OS to minimise motion. */
@media (prefers-reduced-motion: reduce) {
  input[type="range"]::-webkit-slider-thumb { transition: none; }
  input[type="range"]::-webkit-slider-thumb:hover { transform: none; }
}

/* Waveform selector buttons */
.waveform-selector {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

/* Selection idiom shared across all interactive demos: button rows where
   one (or none) is .active. Visual style cascades onto every variant —
   .waveform-btn, .spec3d-src-btn, .vu-src-btn, .sonic-src-btn, etc. */
.waveform-btn,
.spat-source-btn,
.spat-reverb-btn,
.spec3d-src-btn,
.spec3d-axis-btn,
.spec3d-song-btn,
.vu-src-btn,
.vu-mode-btn,
.sonic-src-btn,
/* These classes were previously missing a base style — their .active rule
   set a teal background but left padding/border-radius/border to browser
   defaults, so they looked like a different SHAPE when active (notably
   Fig 2.1's rmsp-wave-btn buttons). Adding them here gives every demo's
   preset/mode/option button the SAME idle rounded-rectangle, and the
   .active state then just paints the same shape teal. */
.sp-wave-btn,
.sp-polarity-btn,
.rmsp-wave-btn,
.flt-type-btn,
.pc-curve-btn,
.cents-mode-btn,
.sm-tech-btn,
.shepard-dir-btn,
.cc-wave-a-btn,
.cc-wave-b-btn,
.spec-scale-btn {
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: var(--font-size-sm);
  font-family: var(--font-body);
  transition: all var(--transition-fast);
}

.waveform-btn,
.sp-wave-btn,
.rmsp-wave-btn,
.mod-wave-btn,
.pc-curve-btn,
.cc-wave-a-btn,
.cc-wave-b-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Narrowcasting source/sink gender glyphs (♂ U+2642 / ♀ U+2640) render
   optically small in most system fonts; bump size and nudge the baseline so
   they read at parity with adjacent text. */
.nc-mf {
  font-size: 1.22em;
  line-height: 1;
  vertical-align: -0.05em;
}

.wave-icon {
  width: 24px;
  height: 12px;
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
}

.waveform-btn:hover,
.spat-source-btn:hover,
.spat-reverb-btn:hover,
.spec3d-src-btn:hover,
.spec3d-axis-btn:hover,
.spec3d-song-btn:hover,
.vu-src-btn:hover,
.vu-mode-btn:hover,
.sonic-src-btn:hover,
.cc-wave-a-btn:hover,
.cc-wave-b-btn:hover,
.spec-scale-btn:hover {
  border-color: var(--color-primary-light);
  color: var(--color-primary);
}

.waveform-btn.active,
.spat-source-btn.active,
.spat-reverb-btn.active,
.spec3d-src-btn.active,
.spec3d-axis-btn.active,
.spec3d-song-btn.active,
.vu-src-btn.active,
.vu-mode-btn.active,
.sonic-src-btn.active,
/* GUI-consistency pass: every demo's preset/mode/option button now uses
   the same active treatment. Previously several of these classes had
   .active toggled in JS but no corresponding CSS, so the selection wasn't
   visible (notably stereo-polarity, rms-pan, filter-type, pitch-curve,
   cents-mode, small-mic-tech, and the new radial-style + EQ-preset rows). */
.sp-wave-btn.active,
.sp-polarity-btn.active,
.rmsp-wave-btn.active,
.flt-type-btn.active,
.pc-curve-btn.active,
.cents-mode-btn.active,
.sm-tech-btn.active,
.shep-style-btn.active,
.shep-style-preset.active,
.shepard-dir-btn.active,
.geq-preset.active,
.mod-matrix-preset.active,
.cc-wave-a-btn.active,
.cc-wave-b-btn.active,
.spec-scale-btn.active,
/* The siren page's carrier and modulator waveform buttons. These toggled
   .active in JS with no rule to match, so the selected waveform was invisible
   — the same omission this block was written to fix for the others. */
.mod-wave-btn.active {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}
/* Defensive: when the active class is combined with .btn .btn-outline (as
   on the radial-style and Shepard mode-toggle rows), .btn-outline's
   default `color: var(--color-primary)` was beating the white text. The
   compound selector below forces white-on-teal regardless of class order. */
.btn.btn-outline.active.shep-style-btn,
.btn.btn-outline.active.shep-style-preset,
.btn.btn-outline.active.shep-spokes-btn,
.btn.btn-outline.active.shep-mode-btn,
.btn.btn-outline.geq-preset.active,
.btn.btn-outline.mod-matrix-preset.active,
.btn.btn-outline.mod-wave-btn.active {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}
/* Layout-continuum slider + preset row */
.shep-style-slider-wrap .shep-slider-row .shep-style-slider {
  height: 28px;
  cursor: pointer;
}
.shep-style-presets .shep-style-preset {
  padding: 4px 10px;
  font-size: var(--font-size-sm);
}

/* Narrow screens (portrait phones): the inline shep.html style sets the
   preset row to 7 equal-flex cells with display:flex + justify-content:
   space-between. At ~360 px viewports each cell becomes too narrow to
   render its label (the longest, "Black Pent" / "Pure Diatonic" / "Simple
   Piano", needs ≈ 80 px), and the label text gets clipped off the right
   edge. Allow the row to WRAP onto multiple lines below the breakpoint
   and floor each cell at a width that fits its longest label so the
   labels stay legible. The 4-px gap from the inline style is preserved.
   The slider above keeps full width regardless. */
@media (max-width: 600px) {
  .shep-style-presets {
    flex-wrap: wrap;
    justify-content: flex-start !important;
    row-gap: 6px;
  }
  .shep-style-presets .shep-style-preset-cell {
    flex: 1 1 calc(33.333% - 4px) !important;
    min-width: 80px;
  }
}
@media (max-width: 380px) {
  .shep-style-presets .shep-style-preset-cell {
    flex: 1 1 calc(50% - 4px) !important;
  }
}

/* Visual separator between preset cluster and song cluster within a
   single source-picker row. A thin vertical line — same height as the
   buttons, faintly tinted, doesn't compete visually. */
.sonic-src-divider {
  display: inline-block;
  align-self: stretch;
  width: 1px;
  margin: 0 var(--space-xs);
  background: var(--color-border);
  opacity: 0.55;
}

/* Per-harmonic mute / solo toggles in the additive-synthesis demo.
   `m` = forces this harmonic silent (red when active).
   `s` = silences every *other* harmonic that isn't also soloed (teal). */
.harm-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
}
.harm-toggles {
  display: inline-flex;
  gap: 2px;
}
.harm-toggle {
  width: 30px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  border-radius: 4px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
  /* iOS Safari long-press on a button OR its surrounding text triggers
     the native text-selection callout / magnifier — which steals the
     long-press intent away from attachLongPressClick. Suppress the
     callout on the buttons themselves; the JS handler also page-locks
     user-select while a press is in-flight (see attachLongPressClick).
     touch-action: manipulation also kills the 300 ms tap delay and
     the pinch-zoom hijack on the button. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}
.harm-toggle .harm-icon {
  /* Emoji glyph — let the button's flex centering own positioning.
     Fixed width/height (legacy SVG sizing) was pushing the glyph
     down-right because the emoji sat on the text baseline inside a
     too-small box. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;       /* clicks always hit the button, not the SVG */
  font-size: 18px;            /* ~20% bigger emoji glyphs */
  line-height: 1;
  text-align: center;
}
.harm-toggle:hover {
  border-color: var(--color-primary-light);
  color: var(--color-text);
}
/* Visual confirmation that a long-press has registered (touch-only path to
   the additive-shift behaviour). The button pulses briefly so the user
   knows the gesture was recognised before they lift their finger. */
.harm-toggle.long-pressing {
  background: rgba(255,200,87,0.22);
  border-color: #ffc857;
  transform: scale(1.08);
  transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.harm-toggle.harm-mute.active {
  background: #e84545;
  border-color: #e84545;
  color: #fff;
}
.harm-toggle.harm-af.active {
  background: rgba(255,200,87,0.25);
  border-color: #ffc857;
  color: #ffc857;
}
.harm-toggle.harm-solo.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.harm-control.is-muted > input[type="range"] {
  opacity: 0.35;
}
.harm-control.is-soloed > input[type="range"] {
  accent-color: var(--color-primary);
}
/* Fourier Sandbox narrowcast grid: 17 dense columns, smaller M/S buttons. */
.fs-narrowcast-grid .harm-toggle.fs-narrowcast-btn {
  width: 100%;
  min-width: 0;
  height: 20px;
  font-size: 11px;
  padding: 0;
  line-height: 1;
}
.fs-narrowcast-grid .harm-toggle.fs-narrowcast-btn .harm-icon {
  width: auto;
  height: auto;
  font-size: 11px;
}
.harm-control.is-soloed .control-label {
  color: var(--color-primary);
}

kbd {
  display: inline-block;
  padding: 1px 5px;
  font: 600 11px/1.4 var(--font-mono);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-bottom-width: 2px;
  border-radius: 3px;
  color: var(--color-text);
}

/* "Closest preset" highlight in the FM/AM Try-Live demos: lights up
   whichever preset the current slider settings sit nearest to. */
.mod-live-preset.is-closest {
  border-color: var(--color-primary);
  background: rgba(17, 153, 158, 0.10);
  color: var(--color-primary);
  font-weight: 600;
}

/* Spatializer: azimuth + distance side-by-side, with the flip controls
   sitting directly under the azimuth slider. */
.spat-az-dist-row {
  display: flex !important;
  flex-direction: row !important;   /* override .control-group's default column */
  gap: var(--space-md);
  flex-wrap: wrap;
  align-items: flex-start;
}
.spat-az-col, .spat-dist-col {
  display: flex;
  flex-direction: column;
  flex: 1 1 240px;
  gap: var(--space-xs);
  min-width: 0;
}
.spat-flip-row {
  display: flex;
  gap: var(--space-xs);
  align-items: center;
  margin-top: var(--space-xs);
  flex-wrap: wrap;
}
.spat-flip-label {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.spat-flip-mode-grp {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius);
  overflow: hidden;
}
.spat-flip-mode-btn {
  padding: 4px 10px;
  background: transparent;
  border: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  font: 500 var(--font-size-xs) var(--font-body);
  transition: background 0.15s, color 0.15s;
}
.spat-flip-mode-btn:not(:last-child) {
  border-right: 1px solid var(--color-border);
}
.spat-flip-mode-btn:hover { background: rgba(17,153,158,0.08); }
.spat-flip-mode-btn.active {
  background: var(--color-primary);
  color: #fff;
}

/* Active-preset highlight on the Shepard tone generator's preset buttons. */
.btn[data-shepard-preset].active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* ===== Shepard Scale Walker (shepard.html) =====
   12-step staircase + chromatic/diatonic mode toggle + linear keyboard
   + radial keyboard. Three different input modalities, one shared model. */
.shep-stair-wrap {
  display: flex;
  justify-content: center;
  margin: var(--space-sm) 0;
}
.shep-staircase-svg {
  width: 100%;
  max-width: 440px;
  height: auto;
  display: block;
}
.shep-staircase-svg .step { cursor: pointer; }
.shep-staircase-svg .step-marker {
  fill: rgba(231, 76, 60, 0.55);
  stroke: #fff;
  stroke-width: 1.8;
  transition: fill 0.15s, stroke-width 0.15s;
}
.shep-staircase-svg .step:hover .step-marker {
  fill: rgba(231, 76, 60, 0.9);
  stroke-width: 2.5;
}
.shep-staircase-svg .step.active .step-marker {
  fill: #e74c3c;
  stroke-width: 3;
}
.shep-staircase-svg .step-num {
  font-family: Inter, system-ui, sans-serif;
  fill: #fff;
  pointer-events: none;
}
.shep-staircase-svg .step.disabled { opacity: 0.28; cursor: default; }
.shep-staircase-svg .step.disabled .step-marker { fill: #777; }
.shep-staircase-svg .ball {
  fill: #ffd166;
  stroke: #7a1a14;
  stroke-width: 2;
  /* Position is driven by RAF in JS (parabolic-arc animation), not by
     CSS transitions, so multiple balls can move independently. */
  transition: opacity 0.25s ease, fill 0.25s ease;
}
/* Resting voice: still visible, but dimmed and desaturated so the active
   voices read as the foreground musical line. */
.shep-staircase-svg .ball.resting {
  opacity: 0.40;
  fill: #d9a44a;
}
/* Diatonic-mode merger polygons: each polygon covers the kerf between a
   black-key tread and its merger-partner white-key tread, painted in the
   trace's light-green tread color. Hand-tuned vertices fit each pair's
   iso geometry so the merger reads as a single fused tread rather than a
   painted stroke. Shown only in diatonic mode. */
.shep-staircase-svg .shep-merge-poly {
  fill: #9BFACC;
  stroke: none;
  pointer-events: none;
}

.shep-mode-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-sm) 0;
  font-size: var(--font-size-sm);
}
.shep-mode-btn {
  padding: 4px 12px;
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  border-radius: var(--border-radius);
  cursor: pointer;
  font: 500 var(--font-size-sm) var(--font-body);
  transition: all 0.15s;
}
.shep-mode-btn:hover { border-color: var(--color-primary-light); color: var(--color-text); }
.shep-mode-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.shep-mode-hint { color: var(--color-text-muted); font-size: var(--font-size-xs); }

/* Linear piano keyboard — implemented as an inline SVG with explicit
   integer x-coordinates so the 7 white keys are guaranteed uniformly
   spaced and the 5 black keys sit exactly where a real piano has them
   (between C-D, D-E, F-G, G-A, A-B). White keys carry per-pitch-class
   HSL tints (hue table from docs/linear-piano-keyboard-design.md); black
   keys are plain dark grey. Press / active states light the key up. */
.shep-linear-kbd-wrap {
  max-width: 480px;
  margin: var(--space-xs) auto;
  padding: 2px;
  background: #222;
  border-radius: 8px;
  box-shadow: 0 3px 6px rgba(0,0,0,0.25);
}
.shep-linear-kbd {
  width: 100%;
  height: auto;
  display: block;
  background: #ececec;
  border-radius: 6px;
  touch-action: none; /* prevent scroll while glissando-sliding on touch */
}
.shep-key { cursor: pointer; }
.shep-key rect {
  stroke: #555;
  stroke-width: 1;
  transition: fill 0.10s;
}
.shep-key--white rect {
  fill: #fff;
  stroke: #888;
}
/* Per-pitch-class white-key tints (idle = 92 % lightness, active = 80 %)  */
/* Hues matched to the radial keyboard's per-PC fills (see shepard.html
   shep-radial-wedge fill="#…"): C=0, D=30, E=60, F=113, G=180, A=240, B=279.
   Saturation bumped from 30% → 55% and lightness pulled from 95% → 88% so
   the linear keys read with the same richness as the radial wedges, not
   washed-out pastels. */
.shep-key--white[data-pc="0"]  rect { fill: hsl(  0, 55%, 88%); }
.shep-key--white[data-pc="2"]  rect { fill: hsl( 30, 55%, 88%); }
.shep-key--white[data-pc="4"]  rect { fill: hsl( 60, 55%, 88%); }
.shep-key--white[data-pc="5"]  rect { fill: hsl(113, 55%, 88%); }
.shep-key--white[data-pc="7"]  rect { fill: hsl(180, 55%, 88%); }
.shep-key--white[data-pc="9"]  rect { fill: hsl(240, 55%, 88%); }
.shep-key--white[data-pc="11"] rect { fill: hsl(279, 55%, 88%); }
.shep-key--white:hover rect { filter: brightness(0.95); }
.shep-key--white[data-pc="0"].active  rect, .shep-key--white[data-pc="0"].pressed  rect { fill: hsl(  0, 70%, 75%); }
.shep-key--white[data-pc="2"].active  rect, .shep-key--white[data-pc="2"].pressed  rect { fill: hsl( 30, 70%, 75%); }
.shep-key--white[data-pc="4"].active  rect, .shep-key--white[data-pc="4"].pressed  rect { fill: hsl( 60, 70%, 75%); }
.shep-key--white[data-pc="5"].active  rect, .shep-key--white[data-pc="5"].pressed  rect { fill: hsl(113, 70%, 75%); }
.shep-key--white[data-pc="7"].active  rect, .shep-key--white[data-pc="7"].pressed  rect { fill: hsl(180, 70%, 75%); }
.shep-key--white[data-pc="9"].active  rect, .shep-key--white[data-pc="9"].pressed  rect { fill: hsl(240, 70%, 75%); }
.shep-key--white[data-pc="11"].active rect, .shep-key--white[data-pc="11"].pressed rect { fill: hsl(279, 70%, 75%); }
.shep-key--black rect {
  fill: #222;
  stroke: #000;
}
.shep-key--black:hover rect { fill: #444; }
.shep-key--black.active rect,
.shep-key--black.pressed rect {
  fill: #4466aa;
}
.shep-key-label {
  font: 600 12px Inter, system-ui, sans-serif;
  fill: #555;
  pointer-events: none;
  user-select: none;
}
.shep-key-label--black {
  font-size: 10px;
  fill: #ddd;
}
.shep-key.disabled { opacity: 0.32; cursor: default; pointer-events: none; }

/* Radial keyboard. 12-wedge compass; diatonic spacing of yaws (360/7),
   black keys narrower and shorter (per docs/radial-keyboard-design.md). */
.shep-radial-wrap { display: flex; justify-content: center; margin: var(--space-xs) 0; }

/* ---- Shepard: a wide screen shows both keyboards at once ----
   The linear and the radial keyboard are the same twelve pitch classes seen
   two ways, and comparing them is the point of the page — but stacked they
   sit some 700 px apart with the style controls in between, so on a
   workstation only one is ever on screen while a quarter to a half of the
   display is empty. Above 1200 px they sit side by side instead.

   The staircase deliberately stays above the pair at full width: pulling it
   into a column would have shrunk it from ~1200 px to ~600 px, trading one
   cramped figure for another.

   Both keyboards are also allowed to grow. Their caps — 480 px for the linear
   one, 280 px for the radial — were chosen for a phone, and neither had
   changed at any viewport width up to 3440 px.

   Below the breakpoint not one of these rules applies, so the phone layout is
   exactly what it was. */
   The breakpoint is 1500 px rather than 1200, and the side column 440 px
   rather than 560, because of arithmetic that has to come out right: the
   content column is 920 px until 1500, and 920 less its padding, less the
   gap, less a 560 px side column leaves 262 px for the keyboard — barely half
   what it had stacked. tools/layout-test.js caught exactly that and named it
   shrinks-when-wider. At 1500 the content column is 1040, which leaves 504 px,
   and every step above that is wider again. */
@media (min-width: 1500px) {
  .shep-stage-grid { display: flex; align-items: flex-start; gap: var(--space-xl); }
  .shep-col-main   { flex: 1 1 0; min-width: 0; }
  .shep-col-side   { flex: 0 0 440px; }
  /* Its own cap was a phone measure; in a column it should fill the column. */
  .shep-col-main .shep-linear-kbd-wrap { max-width: 100%; }
  .shep-col-side .shep-radial-kbd      { max-width: 400px; }
}
.shep-radial-kbd  { width: 100%; max-width: 280px; display: block; touch-action: none; }
.shep-radial-wedge {
  cursor: pointer;
  /* Opacity transition powers the cross-fade morph between display styles
     (Piano notches ⇄ Extended blacks ⇄ Equal partners); fill-opacity &
     stroke-width transitions are the existing hover/press feedback. */
  transition: opacity 0.18s ease, fill-opacity 0.10s, stroke-width 0.10s;
  /* Kill the blue tap-highlight rectangle iPadOS Safari draws on SVG
     <path role="button"> elements when keys are tapped. The element
     should still be keyboard-focusable; we only suppress the touch
     highlight overlay. */
  -webkit-tap-highlight-color: transparent;
  outline: none;
}
.shep-radial-wedge:hover  { fill-opacity: 0.85; }
.shep-radial-wedge.active { fill-opacity: 0.95; stroke-width: 2; }
.shep-radial-wedge.pressed { fill-opacity: 1; stroke-width: 3; stroke: #fff; }
.shep-radial-wedge.disabled { opacity: 0.25; cursor: default; pointer-events: none; }
.shep-radial-wedge.morphing,
.shep-radial-label.morphing { opacity: 0; }
.shep-radial-label.disabled { opacity: 0.35; }
.shep-radial-label { transition: opacity 0.18s ease; }
/* Per-key keyboard-shortcut badges. Smaller font, monospace, dimmed so
   they don't compete with the musical-note label. Linear badges sit at the
   top of each key; radial badges sit just outside each wedge's outer
   perimeter along its centre angle. Italic + reduced opacity differentiates
   them visually from the note labels (which are upright sans-serif). */
.shep-key-kbd, .shep-key-kbd-radial {
  font: italic 700 13px ui-monospace, Menlo, monospace;
  fill: #ffe082;
  fill-opacity: 0.95;
  paint-order: stroke;
  stroke: rgba(0,0,0,0.7);
  stroke-width: 2.5;
  stroke-linejoin: round;
  pointer-events: none;
  dominant-baseline: middle;
}

/* Triangle-morph vertex labels (5 Major Pent / 7 Pure Diatonic / 12
   Piano). Paint-order: stroke with a dark outline behind a bright fill
   so the labels stay crisp on either light or dark page background
   regardless of the faint teal triangle interior behind them — the same
   technique the per-key kbd badges use. */
.shep-tri-vlabel {
  font: 700 14px Inter, system-ui, sans-serif;
  fill: #ffe082;
  fill-opacity: 1;
  paint-order: stroke;
  stroke: rgba(0,0,0,0.85);
  stroke-width: 3;
  stroke-linejoin: round;
  pointer-events: none;
}
/* Secondary tspan line listing additional presets that snap to the
   same vertex (e.g. "+ Yo · + Black" under "5  Major Pent"). Smaller,
   slightly dimmer so it reads as auxiliary metadata. */
.shep-tri-vlabel-aux {
  font-size: 11px;
  font-weight: 500;
  fill-opacity: 0.78;
}
.shep-key-kbd-radial { font-size: 11px; }
/* iPadOS Safari tap-highlight kill for linear-keyboard rectangles too */
.shep-key { -webkit-tap-highlight-color: transparent; }
.shep-radial-pointer { display: none !important; }   /* radial arm removed per user request */

/* Transport-control glyphs inside Play/Stop buttons */
.btn-icon {
  font-size: 0.95em;
  line-height: 1;
  display: inline-block;
  /* The Unicode play/stop glyphs render slightly above the type baseline;
     a small nudge centers them with the adjacent text. */
  transform: translateY(-1px);
}

/* Generic button */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  border: none;
  border-radius: var(--border-radius);
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
  transition: all var(--transition-fast);
}

.btn-primary {
  background: var(--color-primary);
  color: #fff;
}

.btn-primary:hover {
  background: var(--color-primary-light);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.btn-outline {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
}

.btn-outline:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

/* --- Warning Callout --- */
.callout {
  padding: var(--space-md) var(--space-lg);
  border-radius: var(--border-radius);
  margin: var(--space-lg) 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}

.callout-warning {
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-text);
  color: var(--color-warning-text);
}

.callout-info {
  background: rgba(17, 153, 158, 0.08);
  border: 1px solid var(--color-primary-light);
  color: var(--color-text-secondary);
}

.callout-icon {
  font-size: var(--font-size-xl);
  flex-shrink: 0;
  line-height: 1;
}

.callout-text {
  font-size: var(--font-size-sm);
  margin: 0;
  color: inherit;
}

/* --- Landing / Home Page --- */
.hero {
  min-height: 80vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-3xl) var(--space-xl);
  position: relative;
  overflow: hidden;
}

.hero-bg-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0.15;
  pointer-events: none;
}

.hero-content {
  position: relative;
  z-index: 1;
  max-width: 700px;
}

.hero h1 {
  font-size: var(--font-size-4xl);
  font-weight: 800;
  line-height: 1.1;
  margin-bottom: var(--space-lg);
  background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary-light));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero-tagline {
  font-size: var(--font-size-lg);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-2xl);
  max-width: 550px;
  margin-left: auto;
  margin-right: auto;
}

.hero-actions {
  display: flex;
  gap: var(--space-md);
  justify-content: center;
  flex-wrap: wrap;
}

.hero-actions .btn {
  padding: var(--space-md) var(--space-2xl);
  font-size: var(--font-size-base);
}

/* Interactive hero waveform */
.hero-viz {
  width: 100%;
  max-width: 600px;
  height: 120px;
  margin: var(--space-2xl) auto 0;
  border-radius: var(--border-radius-lg);
  overflow: hidden;
}

/* Chapter cards grid */
.chapters-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--space-lg);
  padding: var(--space-3xl) var(--space-xl);
  max-width: 1100px;
  margin: 0 auto;
}

.chapter-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-lg);
  padding: var(--space-xl);
  text-decoration: none;
  color: inherit;
  transition: all var(--transition-base);
  display: flex;
  flex-direction: column;
}

.chapter-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
  border-color: var(--color-primary-light);
  text-decoration: none;
  color: inherit;
}

.chapter-card-number {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-primary);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: var(--space-sm);
}

.chapter-card h3 {
  font-size: var(--font-size-md);
  margin-bottom: var(--space-sm);
}

.chapter-card p {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  flex: 1;
}

/* --- Chapter Navigation (prev/next) --- */
.chapter-nav {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-3xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border);
  gap: var(--space-md);
}

.chapter-nav a {
  display: flex;
  flex-direction: column;
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius);
  text-decoration: none;
  transition: all var(--transition-base);
  max-width: 45%;
}

.chapter-nav a:hover {
  border-color: var(--color-primary-light);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
}

.chapter-nav-label {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-xs);
}

.chapter-nav-title {
  font-weight: 600;
  color: var(--color-text);
  font-size: var(--font-size-sm);
}

.chapter-nav .next {
  margin-left: auto;
  text-align: right;
}

/* --- Responsive Audio Demo Grid --- */
.audio-demo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  gap: var(--space-sm);
  margin: var(--space-lg) 0;
}

/* Compact variant — for very short labels (e.g. the 7 dB-step buttons in
   chapter 2). Fixes the column count to a known value so a "loudest …
   quietest" label spanning the row actually aligns with the first and
   last buttons. Override the count per-grid with inline style="--cols: N".
   (auto-fit would create extra phantom columns to honour the minmax
   minimum, pushing the "quietest" label past the last button.) */
.audio-demo-grid.compact {
  grid-template-columns: repeat(var(--cols, 7), minmax(0, 1fr));
}

.audio-demo-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius);
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
  text-align: center;
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  transition: all var(--transition-fast);
}

.audio-demo-btn:hover {
  border-color: var(--color-primary-light);
  color: var(--color-primary);
  background: rgba(17, 153, 158, 0.05);
}

.audio-demo-btn.playing {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}

/* Triangular play / square stop icon for audio-demo-btn — see Fig 3.15 / 3.16
   matrix cells. CSS-only swap based on the .playing class that the audio
   engine already toggles on the trigger element. */
.audio-demo-btn .play-stop-icon::before { content: "\25B6"; font-size: 0.9em; }
.audio-demo-btn.playing .play-stop-icon::before { content: "\25A0"; }
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Fig 3.2 — overlay a column of play buttons on the right side of the
   guitar-string-harmonics SVG, aligned with the six harmonic rows so
   each ▶ sits next to its corresponding mode. The SVG (viewBox 0 0
   600 400) has the harmonics at y=160, 195, 230, 265, 300, 335 — i.e.
   40% through 83.75% of the height — and reserves the rightmost ~20 %
   of the width past the pitch labels for the buttons. */
.harmonic-figure {
  position: relative;
  display: block;
}
.harmonic-figure > img,
.harmonic-figure > svg {
  /* Both <img> and inline <svg> need to fill the container, otherwise the
     SVG renders at the browser's default intrinsic size and the percentage-
     based button container ends up misaligned with the SVG's viewBox
     y-coordinates. Using `aspect-ratio` (rather than `height:auto`) is more
     reliable across browsers for inline SVG — Safari in particular doesn't
     always derive height from the viewBox when height:auto is used. */
  width: 100%;
  /* viewBox is now -85 0 685 400 to include the vertical piano keyboard
     on the left edge. Aspect ratio adjusted accordingly. */
  aspect-ratio: 685 / 475;
  height: auto;
  display: block;
}
.harmonic-figure-buttons {
  position: absolute;
  /* Strings sit at SVG y = 160, 195, 230, 265, 300, 335 in viewBox 0..400.
     With 6 equal-height grid tracks, expanding the container by half a
     track at top and bottom lands each track centre on its string:
     top = (160 − 17.5)/400 = 35.625%; bottom = (400 − 352.5)/400 = 11.875%. */
  top: 35.625%;
  bottom: 11.875%;
  right: 1%;
  width: 13%;
  display: grid;
  grid-template-rows: repeat(9, 1fr);
}
.harmonic-figure-buttons > .harmonic-row {
  align-self: center;
  justify-self: center;
  display: flex;
  gap: 4px;
  align-items: center;
}

.harm-master-controls {
  display: flex;
  gap: var(--space-md);
  align-items: center;
  flex-wrap: wrap;
  margin: var(--space-md) 0 var(--space-sm);
}
.harm-master-controls .harm-master-play { white-space: nowrap; }
.harm-freq-control {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1 1 220px;
}
.harm-freq-control input[type="range"] { width: 100%; max-width: 320px; }
.harm-master-hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-style: italic;
}

/* Plucked vs bowed visual cue — change the SVG envelope color so the
   string animation reads differently. Plucked = original teal, bowed =
   warm rosin-orange. */
#fig3-2-svg .envelope { transition: stroke 0.25s; }
.harmonic-figure.is-bowed #fig3-2-svg .envelope { stroke: #b16a2c; }

/* Universal narrowcasting visual idiom (applied to .harmonic-row,
   .harm-control, .harm-row controls, and SVG row groups via .harm-row).
   Three states stack: is-muted (gray), is-shadow (desaturated),
   is-soloed (emboldened). A single element with both is-muted and
   is-soloed reads as muted overall. */
.is-muted { opacity: 0.28; }
.is-shadow { opacity: 0.55; filter: saturate(0.4); }

.harm-source-toggle {
  display: flex;
  gap: var(--space-xs);
  align-items: center;
  flex-wrap: wrap;
  margin: var(--space-sm) 0;
  font-size: var(--font-size-sm);
}
.harm-source-label {
  color: var(--color-text-muted);
}
.harm-source-btn {
  padding: 4px 10px;
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  border-radius: var(--border-radius);
  cursor: pointer;
  font: 500 var(--font-size-sm) var(--font-body);
  transition: all var(--transition-fast);
}
.harm-source-btn:hover {
  border-color: var(--color-primary-light);
  color: var(--color-text);
}
.harm-source-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* Fig 3.5 — sine-wave decomposition figure with a narrowcasting button
   column flanking the SVG. Each row in the column lines up with the
   corresponding sine wave drawn inside the SVG. */
.harmonics-decomp-figure {
  position: relative;
  display: block;
}
.harmonics-decomp-figure > img {
  width: 100%;
  display: block;
}
.harmonics-decomp-playbar {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  margin: var(--space-xs) 0 var(--space-sm);
}

/* Preset rows: label on its own line above; preset buttons stay on one
   line where they fit, and wrap onto additional lines when they don't.
   This is the "dynamic layout" mode: single-row on wide viewports
   (visually clean), graceful multi-row on narrow viewports (e.g. iPad with
   the ToC sidebar showing) so individual button labels never get clipped. */
.preset-row {
  display: flex;
  gap: var(--space-xs);
  align-items: center;
  flex-wrap: wrap;
  row-gap: var(--space-xs);
}
.preset-row > .btn,
.preset-row > button {
  flex-shrink: 0;
  white-space: nowrap;
}
.preset-row-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
  flex-wrap: wrap;
}
.preset-row-header .preset-label {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* Polar-pattern presets table: each row is a preset (button + ratio + DI).
   Numeric columns use tabular-nums so the dB values column-align cleanly. */
.polar-presets-table {
  border-collapse: collapse;
  margin: var(--space-xs) 0;
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}
.polar-presets-table th,
.polar-presets-table td {
  padding: 2px var(--space-sm);
  text-align: left;
  border: none;
}
.polar-presets-table thead th {
  color: var(--color-text-muted);
  font-weight: 600;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--color-border, rgba(255,255,255,0.1));
}
.polar-presets-table td:first-child { padding-left: 0; }
.polar-presets-table tbody td { vertical-align: middle; }
/* Keep preset button labels on one line — "Figure-8" was wrapping under default
   button shrink. The α-coefficient column gets a narrower max-width so the row
   space frees up for the wider Preset and DI columns. */
.polar-presets-table tbody td .btn { white-space: nowrap; }
.polar-presets-table th:nth-child(2),
.polar-presets-table td:nth-child(2) { max-width: 18em; padding-right: var(--space-xs); }
.polar-presets-table th:nth-child(3),
.polar-presets-table td:nth-child(3) { padding-left: var(--space-xs); }
.harmonics-decomp-hint {
  font-size: var(--font-size-xs);
  color: var(--text-secondary);
}
.harmonics-decomp-figure > .harmonics-decomp-buttons {
  position: absolute;
  /* y=41..351 of viewBox 420 → 9.8 %..83.6 %  ⇒  top 9.8 %, bottom 16.4 % */
  top: 9.8%;
  bottom: 16.4%;
  right: 0.5%;
  width: 80px;
  display: grid;
  grid-template-rows: repeat(6, 1fr);
}
.harmonics-decomp-figure > .harmonics-decomp-buttons > .harmonic-row {
  align-self: center;
  justify-self: end;
  display: flex;
  gap: 4px;
  align-items: center;
}
/* Dim the play button when its harmonic is muted or shadowed by a solo. */
.harmonic-row.is-muted .harmonic-play {
  opacity: 0.35;
  cursor: not-allowed;
}
.harmonic-row.is-soloed .harmonic-play {
  border-color: var(--color-primary);
}

/* Generic "audio-into-the-figure" overlay: each clickable region is a
   transparent button positioned with inline style="top:.. left:.. width:..
   height:.." over the SVG, so the figure's own labels act as the visible
   text. Used for Fig 3.6 (sawtooth partial sums) and Fig 3.20 (phase
   destructive interference). */
.fig-clickable {
  position: relative;
  display: block;
}
.fig-clickable > img {
  width: 100%;
  display: block;
}
.fig-clickable > .fig-clickable-region {
  position: absolute;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--border-radius);
  padding: 0;
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
/* A small ▶ glyph in the corner of each clickable region — gives a visible
   cue that the otherwise-transparent overlay is interactive. The button's
   aria-label still carries the full text for screen readers. */
.fig-clickable > .fig-clickable-region::after {
  content: '\25B6';   /* ▶ */
  position: absolute;
  bottom: 4px;
  right: 6px;
  font-size: 13px;
  line-height: 1;
  color: rgba(17, 153, 158, 0.55);
  transition: color var(--transition-fast), transform var(--transition-fast);
  pointer-events: none;
}
.fig-clickable > .fig-clickable-region:hover {
  background: rgba(17, 153, 158, 0.06);
  border-color: rgba(17, 153, 158, 0.5);
}
.fig-clickable > .fig-clickable-region:hover::after {
  color: var(--color-primary);
  transform: scale(1.25);
}
.fig-clickable > .fig-clickable-region.playing {
  background: rgba(17, 153, 158, 0.14);
  border-color: var(--color-primary);
}
.fig-clickable > .fig-clickable-region.playing::after {
  color: var(--color-primary);
}

/* --- Modulation preset table (Fig 3.15 / 3.16) --- */
.modulation-presets {
  border-collapse: collapse;
  margin: var(--space-lg) 0;
  width: 100%;
  max-width: 580px;
}
.modulation-presets th,
.modulation-presets td {
  padding: var(--space-xs) var(--space-sm);
  vertical-align: middle;
}
.modulation-presets thead th {
  font-size: var(--font-size-sm);
  font-weight: 600;
  text-align: center;
  color: var(--color-text-secondary);
  border-bottom: 1px solid var(--color-border);
  padding-bottom: var(--space-sm);
}
.modulation-presets thead th.corner {
  text-align: left;
  font-style: italic;
  color: var(--color-text-muted);
  font-weight: 500;
}
.modulation-presets tbody th {
  text-align: right;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.modulation-presets td {
  text-align: center;
}
.modulation-presets td .audio-demo-btn {
  width: 100%;
  min-width: 64px;
  padding: var(--space-sm) var(--space-sm);
}

/* --- Utility Classes --- */
.text-center { text-align: center; }
.text-muted { color: var(--color-text-muted); }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- Loading states --- */
@keyframes pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

.loading {
  animation: pulse 1.5s ease-in-out infinite;
}

/* --- Authors page: the rotating original-CD hero. ----------------
   The label side is a circular PNG with a transparent hub-hole and
   translucent outer-plastic / inner-clear rings so the page shows
   through, like a real CD. Clicking flips to the data side, which
   spins the opposite direction. Two images are stacked in the same
   slot; .flipped on the wrapper crossfades them. ---------------- */
@keyframes cd-spin-cw {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@keyframes cd-spin-ccw {
  from { transform: rotate(0deg); }
  to   { transform: rotate(-360deg); }
}
.cd-rom-hero {
  margin: 0 auto var(--space-xl);
  text-align: center;
}
.cd-flip-btn {
  display: inline-block;
  background: transparent;
  border: none;
  padding: 0;
  margin: 0 auto var(--space-md);
  cursor: pointer;
  position: relative;
  width: min(420px, 70vw);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  /* Soft drop-shadow gives the disc a sense of physicality */
  filter: drop-shadow(0 8px 24px rgba(0,0,0,0.25));
}
.cd-flip-btn:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 6px;
}
.cd-rom-hero img.cd-spin {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* 30s/rev reads as deliberate motion, not decorative noise. */
  animation: cd-spin-cw 30s linear infinite;
  will-change: transform, opacity;
  transition: opacity 0.6s ease;
}
.cd-rom-hero img.cd-spin--data {
  /* Data side spins the opposite direction so the flip feels
     mechanically distinct, not a still re-paint. */
  animation: cd-spin-ccw 30s linear infinite;
  opacity: 0;
}
.cd-flip-btn.flipped img.cd-spin--label { opacity: 0; }
.cd-flip-btn.flipped img.cd-spin--data  { opacity: 1; }

/* "DAD" disc-duplication photo — shown as a third click-state after label
   and data sides. Click it again to return to the label side. */
.cd-rom-hero .cd-dad-photo {
  display: none;
  margin: 0 auto var(--space-md);
  max-width: min(560px, 90vw);
  cursor: pointer;
}
.cd-rom-hero .cd-dad-photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  filter: drop-shadow(0 8px 24px rgba(0,0,0,0.25));
}
.cd-rom-hero .cd-dad-photo figcaption {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
  font-style: italic;
}
.cd-rom-hero.show-photo .cd-flip-btn,
.cd-rom-hero.show-photo .cd-flip-hint { display: none; }
.cd-rom-hero.show-photo .cd-dad-photo { display: block; }
.cd-rom-hero figcaption {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.cd-flip-hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-md);
  font-style: italic;
}
/* Respect prefers-reduced-motion: hold both discs still, fade only. */
@media (prefers-reduced-motion: reduce) {
  .cd-rom-hero img.cd-spin {
    animation: none;
  }
}

/* Authors-page group portrait. Same drop-shadow + caption treatment
   as the CD-ROM hero, but sized smaller so it doesn't dominate. */
.authors-group-photo {
  margin: var(--space-2xl) auto;
  max-width: 640px;
  text-align: center;
}
.authors-group-photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  margin: 0 auto var(--space-sm);
  filter: drop-shadow(0 6px 20px rgba(0,0,0,0.22));
}
.authors-group-photo figcaption {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* --- Print --- */
@media print {
  .sidebar, .mobile-nav-toggle, .demo-container, .audio-player, .theme-toggle {
    display: none;
  }
  .main-content {
    margin-left: 0;
  }
  body {
    color: #000;
    background: #fff;
  }
}

/* ===== Site-wide search (Pagefind) ===== */
.site-search-trigger {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-sm);
  padding: 6px 10px;
  background: var(--color-surface-alt);
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  font: 500 13px/1 Inter, system-ui, sans-serif;
  cursor: pointer;
  width: 100%;
  text-align: left;
  transition: background 120ms, color 120ms, border-color 120ms;
}
.site-search-trigger:hover {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-primary);
}
.site-search-trigger .search-trigger-label { flex: 1; }
.site-search-trigger .search-trigger-kbd {
  font: 600 11px JetBrains Mono, monospace;
  padding: 2px 5px;
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-bg);
  color: var(--color-text-muted);
}
.site-search-modal {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  display: none;
  align-items: flex-start;
  justify-content: center;
  z-index: 9000;
  padding: 80px 16px 16px;
  backdrop-filter: blur(3px);
}
.site-search-modal.open { display: flex; }
.site-search-modal-inner {
  width: min(720px, 100%);
  max-height: 80vh;
  overflow: auto;
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: 10px;
  box-shadow: 0 16px 40px rgba(0,0,0,0.4);
  padding: 18px;
}
.site-search-modal .site-search-hint {
  margin: 12px 0 0;
  font: 12px JetBrains Mono, monospace;
  color: var(--color-text-muted);
}
.site-search-modal .site-search-hint kbd {
  padding: 1px 5px; border: 1px solid var(--color-border);
  border-radius: 3px; background: var(--color-surface-alt);
}
/* Theme overrides for Pagefind UI's CSS variables */
.site-search-modal {
  --pagefind-ui-primary: var(--color-primary);
  --pagefind-ui-text: var(--color-text);
  --pagefind-ui-background: var(--color-surface);
  --pagefind-ui-border: var(--color-border);
  --pagefind-ui-tag: var(--color-surface-alt);
  --pagefind-ui-border-width: 1px;
  --pagefind-ui-border-radius: 6px;
  --pagefind-ui-image-border-radius: 6px;
  --pagefind-ui-image-box-ratio: 3 / 2;
  --pagefind-ui-font: Inter, system-ui, sans-serif;
}
/* Pagefind ships its own CSS that hard-codes some colors, overriding the
   --pagefind-ui-text variable for the input field and its placeholder. In
   dark mode that leaves the typed query effectively invisible. Force the
   relevant elements to inherit the page's text color. */
.site-search-modal .pagefind-ui__search-input,
.site-search-modal .pagefind-ui__search-input:focus,
.site-search-modal input[type="text"] {
  color: var(--color-text) !important;
  background: var(--color-surface) !important;
  caret-color: var(--color-primary);
}
.site-search-modal .pagefind-ui__search-input::placeholder { color: var(--color-text-muted) !important; }
.site-search-modal .pagefind-ui__result-title,
.site-search-modal .pagefind-ui__result-title a,
.site-search-modal .pagefind-ui__result-link,
.site-search-modal .pagefind-ui__result-excerpt,
.site-search-modal .pagefind-ui__message { color: var(--color-text) !important; }
.site-search-modal .pagefind-ui__result-tags,
.site-search-modal .pagefind-ui__filter-name { color: var(--color-text-secondary) !important; }
.site-search-modal mark { background: rgba(255, 209, 102, 0.35); color: inherit; }
/* Highlight whichever result row is "focused" so the Enter-to-open hint
   is meaningful — see initPagefindSearch() in app.js for the focus logic. */
.site-search-modal .pf-focused-result { outline: 2px solid var(--color-primary); outline-offset: 2px; border-radius: 4px; }
body.site-search-open { overflow: hidden; }

/* --- Keyboard-shortcut hints ----------------------------------------------
   Visible by default; suppressed on touch-primary devices unless a physical
   keypress has been detected (app.js sets .has-keyboard on <html>). Approx-
   imation: any-pointer:coarse + any-hover:none ≈ "no fine pointer, no hover"
   ≈ a touch-only device. iPads with paired keyboards but no trackpad still
   match this; the JS reveals the hint on first physical keydown. */
.kbd-shortcut-hint { display: inline-flex; align-items: center; gap: 0.25em; }
.kbd-shortcut-hint kbd {
  font-family: JetBrains Mono, ui-monospace, Menlo, monospace;
  font-size: 0.85em;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--color-surface, rgba(0,0,0,0.08));
  border: 1px solid var(--color-border, rgba(0,0,0,0.18));
  color: var(--color-text);
}
@media (hover: none) and (pointer: coarse) {
  :root:not(.has-keyboard) .kbd-shortcut-hint { display: none; }
}

/* ===== Narrowcasting Sandbox (Fig 9.4) — shared with the standalone
   /narrowcasting.html page so the demo styles consistently on both. ===== */
.nc-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 65vh;
  background: #0e1a26;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  overflow: hidden;
  cursor: default;
  touch-action: none;
}
.nc-stage svg { width: 100%; height: 100%; display: block; }
.nc-icon { cursor: grab; user-select: none; }
.nc-icon.dragging { cursor: grabbing; }
.nc-particle { fill: #ffc857; stroke: #ffc857; opacity: 0.85; pointer-events: none; }
.nc-particle.percussion { fill: #ff6b6b; stroke: #ff6b6b; }
.nc-particle.harmony    { fill: #9d8df1; stroke: #9d8df1; }
.nc-particle.melody     { fill: #11999e; stroke: #11999e; }
.nc-particle.pink       { fill: #ffc857; stroke: #ffc857; }
.nc-particle.click      { fill: #fafafa; stroke: #fafafa; }
.nc-vector { stroke: rgba(255,255,255,0.18); stroke-dasharray: 4 5; fill: none; pointer-events: none; }
.nc-source-body { fill: rgba(17,153,158,0.18); stroke: #11999e; stroke-width: 1.5; }
.nc-source-body.muted    { fill: rgba(232,69,69,0.10); stroke: #e84545; }
.nc-source-body.soloed   { fill: rgba(255,200,87,0.22); stroke: #ffc857; stroke-width: 2.2; }
.nc-source-body.silenced { fill: rgba(255,255,255,0.04); stroke: rgba(255,255,255,0.18); stroke-dasharray: 3 3; }
.nc-sink-body   { fill: rgba(157,141,241,0.18); stroke: #9d8df1; stroke-width: 1.5; }
.nc-sink-body.deafened { fill: rgba(232,69,69,0.10); stroke: #e84545; }
.nc-sink-body.attended { fill: rgba(255,200,87,0.22); stroke: #ffc857; stroke-width: 2.2; }
.nc-sink-body.silenced { fill: rgba(255,255,255,0.04); stroke: rgba(255,255,255,0.18); stroke-dasharray: 3 3; }
.nc-sink-nose   { fill: #ffc857; stroke: rgba(0,0,0,0.35); stroke-width: 0.75; cursor: grab; }
.nc-nose-stalk  { stroke: #ffc857; stroke-width: 3; stroke-linecap: round; pointer-events: stroke; cursor: grab; }
.nc-label       { fill: rgba(255,255,255,0.78); font: 12px/1 Inter, system-ui, sans-serif; pointer-events: none; user-select: none; }
.nc-label.small { fill: rgba(255,255,255,0.5); font-size: 10px; }
.nc-tag {
  font-size: 10px;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgba(255,255,255,0.06);
  color: var(--color-text-muted);
  font-family: var(--font-mono, ui-monospace, monospace);
}
.nc-tag.audible   { background: rgba(17,153,158,0.25); color: #fff; }
.nc-tag.silenced  { background: rgba(232,69,69,0.25); color: #fff; }

/* Head-track button: enlarge the 🤳 selfie emoji and clip the lower arm so
   the hand-with-phone reads at small magnification. The emoji glyph is
   ~1.7× the surrounding text and shifted up inside an overflow-hidden box;
   the arm portion that would normally extend below the baseline is clipped. */
.ht-emoji {
  display: inline-block;
  width: 1.1em;
  height: 1em;
  overflow: hidden;
  position: relative;
  vertical-align: -0.18em;
  line-height: 1;
}
.ht-emoji > span {
  position: absolute;
  font-size: 1.7em;
  line-height: 1;
  top: -0.08em;
  left: 50%;
  transform: translateX(-50%);
  pointer-events: none;
}

/* Narrowcasting panel — per-channel two-line card layout. */
.nc-card { padding: 6px 0; }
.nc-card + .nc-card { border-top: 1px solid var(--color-border); margin-top: 4px; }
.nc-card-top, .nc-card-bottom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.nc-card-bottom { margin-top: 4px; }
.nc-inline { vertical-align: middle; }
.nc-tag.hearing  { background: rgba(17,153,158,0.25); color: #fff; }
.nc-tag.deafened { background: rgba(232,69,69,0.25); color: #fff; }

.nc-swatch { border: 1px solid rgba(128,128,128,0.55); box-sizing: content-box; }
/* Egocentric + autofocus (subjective slice): the original-scene layers
   stay visible so every icon literally slides into the slice rather than
   vanishing/respawning. Vectors are the one exception — the world-frame
   vectors aren't meaningful in the slice; the overlay draws fresh slice
   vectors in #ncAutofocus.
   • Non-selected sources fade out (opacity, not display, so a future
     hover/tooltip on them isn't broken; pointer-events:none guards clicks).
   • Selected source + every sink get data-autofocus-active during the slice.
   • Tagged elements crossfade their labels: the ♂/♀ glyph fades out and a
     "subjective" baseline-N label fades in, producing the "promoted subscript"
     effect requested for the subjective view. */
.nc-stage.nc-egofocus #ncVectors { display: none !important; }
.nc-stage.nc-egofocus #ncSources g[data-src]:not([data-autofocus-active]) {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
/* Hide the nose + stalk on omni SOURCES only — an omnidirectional source
   capsule radiates equally in every direction, so its yaw doesn't affect
   the room and the yellow triangle would be misleading. Omni SINKS keep
   their nose: even when reception is omni, the listener still has a
   forward direction (the head-frame the binaural render uses), and the
   user needs to see which way each sink is facing. The opacity
   transition gives a quick fade-in for sources when the user reassigns
   the pattern to something directional (the yaw is still tracked
   internally even while hidden, so it's ready to show). Source icons
   carry data-src; sink icons carry data-sink — the [data-src] guard is
   what makes this rule source-only. */
.nc-sink-nose, .nc-nose-stalk { transition: opacity 0.3s ease; }
.nc-icon[data-omni="true"][data-src] .nc-sink-nose,
.nc-icon[data-omni="true"][data-src] .nc-nose-stalk { opacity: 0; }
/* Subjective-slice labels: world-frame label (♂/♀ glyph) starts visible;
   subjective label (bare index) starts hidden. During the slice the JS
   crossfades them via inline style.opacity keyed on convergeT — this
   syncs the glyph→baseline-N flip with the rearrangement motion, instead
   of running it as a CSS-only prelude that finishes before icons move. */
.nc-source-subj-label, .nc-sink-subj-label { opacity: 0; }
.nc-del {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  border-radius: 50%;
  width: 20px; height: 20px; line-height: 18px;
  padding: 0;
  font-size: 11px;
  cursor: pointer;
}
.nc-del:hover { background: rgba(232,69,69,0.18); color: #e84545; border-color: #e84545; }
.nc-add {
  margin-top: 6px;
  background: transparent;
  border: 1px dashed var(--color-border);
  color: var(--color-text-muted);
  border-radius: 6px;
  padding: 3px 10px;
  font-size: var(--font-size-xs);
  cursor: pointer;
}
.nc-add:hover:not(:disabled) { background: rgba(17,153,158,0.10); color: var(--color-text); border-color: var(--color-text-muted); }
.nc-add:disabled { opacity: 0.4; cursor: not-allowed; }

/* Part legend under the spectrogram keyboard. Only shown when a piece has
   more than one named part; the swatches match the lit-key colours exactly. */
.spec3d-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  justify-content: center;
  max-width: 640px;
  margin: 8px auto 0;
  font-size: 0.78rem;
  color: var(--color-text-secondary);
}
.spec3d-legend:empty { display: none; }
.spec3d-legend-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.spec3d-swatch {
  width: 11px; height: 11px; border-radius: 2px;
  border: 1px solid rgba(0, 0, 0, 0.28);
  flex: 0 0 auto;
}

/* Share control auto-added to each demo header. Sits at the end of the header
   row, out of the way of the figure number and title. */
.demo-share { display: inline-flex; align-items: center; gap: 8px; }
.demo-header .demo-share { justify-self: end; align-self: center; }
/* On a phone the header stacks, so the control takes its own row rather than
   competing with the title for width. */
@media (max-width: 640px) {
  .demo-header { grid-template-columns: auto 1fr; }
  .demo-header .demo-share { grid-column: 1 / -1; justify-self: start; margin-top: 6px; }
  /* The share control has its own row here, so the description no longer needs
     to be placed ahead of it. */
  .demo-header > .demo-description { order: 0; }
}
.demo-share-status { font-size: 0.76rem; color: var(--color-text-secondary); }
/* The share control is injected as the container's first child when there is
   no .demo-header to hang it on — true of a plain .figure, such as the cents
   comparison in chapter 1 — so it needs the same breathing room there. */
.demo-container > .demo-share,
.figure > .demo-share { margin: 0 0 var(--space-sm) 0; }
