/* ============================================================
   One Note Now — style.css
   Unified stylesheet (home + internal + soundpage + manifesto)
   ============================================================ */

/* Self-hosted fonts — no third-party (Google) request, no visitor IP sent to Google. */
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:400;font-display:swap;src:url('/fonts/cormorant-garamond-italic-400-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:400;font-display:swap;src:url('/fonts/cormorant-garamond-italic-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Instrument Serif';font-style:italic;font-weight:400;font-display:swap;src:url('/fonts/instrument-serif-italic-400-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Instrument Serif';font-style:italic;font-weight:400;font-display:swap;src:url('/fonts/instrument-serif-italic-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Instrument Serif';font-style:normal;font-weight:400;font-display:swap;src:url('/fonts/instrument-serif-normal-400-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Instrument Serif';font-style:normal;font-weight:400;font-display:swap;src:url('/fonts/instrument-serif-normal-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}


:root {
  --bg:      #0b0b0b;
  --fg:      #f2f2f2;
  --accent:  #f2f2f2;
  --gap:     3.2rem;
  --line:    rgba(255,255,255,.15);
  --accent-pay:   #9cf0e6;
  --accent-pay-2: #de49a0;
  --accent-hot:   #ff3e9e;
  --accent-err:   #ff6b6b;
  --accent-stop:  #ff4d4d;

  /* Layout dimensions — soundpage stage */
  --arc-svg-size:    408px;
  --btn-test-size:   61px;
  --btn-release-size: 104px;
  --btn-silence-size: 132px;
  --pitch-mode-h:    36px;
  --stage-shift:    -50px;
  --stage-gap:       24px;

  /* Motion scale — 3 tiers */
  --dur-quick: 120ms;
  --dur-med:   200ms;
  --dur-slow:  350ms;
  --panel:   #131419;
  --link-internal: #b7b7b7; --link-internal-hover: #e0e0e0;
}

html {
  height: 100%;
  margin: 0;
  background: var(--bg);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-synthesis-weight: none;
  font-stretch: 100%;
}

/* ── Body variants ───────────────────────────────────────── */
/* Internal pages (about, manifesto, soundpage) override flex */

/* ── Page fade-in ──────────────────────────────────────── */
body { opacity: 0; animation: pageFadeIn .35s ease-out .05s forwards; }
@keyframes pageFadeIn { to { opacity: 1; } }

/* Reduce motion preference — global catch-all.
   WCAG 2.3.3 (Animation from Interactions) + practical UX care: users who set
   their OS to "reduce motion" expect a calm interface without sliding, fading,
   pulsing or blurring transitions. Targeted overrides for specific elements
   (instructions modal, soundpage intro blur veil, echo popup, home background)
   are kept further down in their own @media blocks — they tune the *end state*
   to something visually coherent, while this block neutralises every
   animation/transition the codebase has, present and future. */
@media (prefers-reduced-motion: reduce) {
  body { opacity: 1; animation: none; }
  *, *::before, *::after {
    animation-duration:        .01ms !important;
    animation-iteration-count: 1     !important;
    animation-delay:           0ms   !important;
    transition-duration:       .01ms !important;
    transition-delay:          0ms   !important;
    scroll-behavior:           auto  !important;
  }
}

body.page-layout {
  display: block;
  text-align: left;
  min-height: 100vh;
  overflow-y: auto;
}

/* ── Home ───────────────────────────────────────────────── */
/* Home page: full-viewport flex centering */
body:not(.page-layout) {
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  min-height: 100vh;
}

.center-stack {
  /* border-box so the horizontal padding stays inside width:100% — otherwise
     100% + 2×22px padding overflows the viewport and adds a horizontal
     scrollbar on the home page. */
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--gap);
}

h1 {
  font-family: 'Instrument Serif', Georgia, serif;
  font-weight: 400;
  font-size: clamp(3rem, 8vw, 5.5rem);
  line-height: 0.97;
  letter-spacing: .01em;
  margin: 0 0 24px;
}

.btn-like {
  display: inline-block;
  background-color: var(--accent);
  color: var(--bg);
  border: none;
  padding: 20px 48px;
  font-size: clamp(1.6rem, 2.6vw, 2rem);
  font-weight: 400;
  letter-spacing: .02em;
  cursor: pointer;
  border-radius: 6px;
  transition: background-color var(--dur-slow), transform var(--dur-med), box-shadow .3s;
  box-shadow: 0 8px 20px rgba(0,0,0,.2);
  text-decoration: none;
}
.btn-like:hover { background-color: #e0e0e0; transform: translateY(-3px); box-shadow: 0 12px 24px rgba(0,0,0,.25); }

.links { display: flex; align-items: center; gap: 14px; }
.sep   { opacity: .4; user-select: none; }

.link { font-size: clamp(1.2rem, 2.8vw, 2rem); opacity: .8; color: var(--fg); text-decoration: none; letter-spacing: .04em; transition: color var(--dur-med) ease, opacity var(--dur-med) ease; }
.link:hover { color: var(--accent-pay); opacity: 1; }

/* ── About ──────────────────────────────────────────────── */
.about-wrap {
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
  padding: 32px 22px;
  box-sizing: border-box;
  text-align: center;
}
.about-wrap p { font-size: clamp(1rem, 2vw, 1.2rem); line-height: 1.55; color: rgba(255,255,255,.92); margin: 18px 0; max-width: 62ch; margin-left: auto; margin-right: auto; }
/* v2.23: .about-wrap a colour rule moved to the unified internal-link
   block at the end of this file (no more #3366ff blue). */
.about-wrap .back { margin-top: 40px; font-size: 1.25rem; text-decoration: none !important; color: var(--fg) !important; opacity: .85; display: inline-block; }
.about-wrap .back:hover { opacity: 1; color: var(--accent-pay) !important; }

/* ── About page — section titles, threshold table, signature ─────────
   Used by the new structured About. Section titles are restrained,
   slightly less prominent than the page title but clearly typographic.
   The thresholds table is a compact reference at the bottom of the
   page, never decorative. The signature is a small closing line. */
.about-wrap .about-section-title {
  font-size: clamp(1.1rem, 2.2vw, 1.4rem);
  font-weight: 600;
  letter-spacing: .03em;
  margin: 40px auto 12px;
  max-width: 62ch;
  text-align: left;
  opacity: .92;
}
.about-wrap .about-section-title:first-of-type {
  margin-top: 24px;
}
.about-wrap p {
  text-align: left;
}
.about-wrap .about-thresholds {
  margin: 18px auto 24px;
  max-width: 62ch;
  width: 100%;
  border-collapse: collapse;
  font-size: clamp(0.85rem, 1.6vw, 0.95rem);
  color: rgba(255,255,255,.85);
}
.about-wrap .about-thresholds th,
.about-wrap .about-thresholds td {
  padding: 9px 8px;
  border-bottom: 1px solid rgba(255,255,255,.10);
  text-align: left;
  white-space: nowrap;
}
.about-wrap .about-thresholds th {
  font-weight: 600;
  letter-spacing: .03em;
  opacity: .75;
  font-size: .88em;
  text-transform: uppercase;
}
.about-wrap .about-thresholds tbody tr:last-child td {
  border-bottom: 0;
}
@media (max-width: 480px) {
  .about-wrap .about-thresholds {
    font-size: 0.78rem;
  }
  .about-wrap .about-thresholds th,
  .about-wrap .about-thresholds td {
    padding: 7px 4px;
  }
}
.about-wrap .about-signature {
  margin: 36px auto 0;
  max-width: 62ch;
  font-size: .92rem;
  font-style: italic;
  opacity: .65;
  text-align: left;
}
.about-wrap .about-signature.about-signature-link {
  margin-top: 18px;
}
.about-wrap .about-signature a {
  color: rgba(255,255,255,.85);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.about-wrap .about-signature a:hover {
  color: var(--fg);
}

/* ── Work counter — last-99 voices remaining ────────────────────────
   Tiny, silent display that activates only in the closing phase of
   the composition. Hidden during the entire growth of the work. When
   visible, it should feel like a quiet announcement, not a banner.
   When sealed, it carries a slightly heavier weight — final, plural,
   monumental. */
.work-counter {
  /* Hidden by default so it takes NO layout space until the closing phase.
     Previously this was only opacity:0 (still a flex-item with a margin and a
     3.2rem gap to the title); work-counter.js then sets display:none once
     /api/state confirms we're not in the closing phase, which shrank the
     centred stack and nudged the home content upward on load (a layout shift).
     With display:none here, the silent state changes nothing → no shift.
     The --active / --sealed classes restore display:block when it must show. */
  display: none;
  font-size: clamp(0.85rem, 1.6vw, 1rem);
  font-style: italic;
  letter-spacing: .04em;
  text-align: center;
  margin: 12px auto 0;
  opacity: 0;
  transition: opacity .4s ease;
  pointer-events: none;
}
.work-counter--active {
  display: block !important;
  opacity: .75;
  color: var(--fg);
}
.work-counter--sealed {
  display: block !important;
  opacity: .9;
  color: var(--fg);
  font-style: normal;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  font-size: clamp(0.95rem, 1.8vw, 1.1rem);
}

/* ── Contact page ─────────────────────────────────────────────────────
   Specifics for contact.html. Same `about-wrap` container, but with a
   prominent email link, a tighter introduction, and a tidy list of
   topics. The email is shown larger than body copy so it reads as the
   page's main affordance. */
.about-wrap .contact-email {
  font-family: 'Instrument Serif', Georgia, serif;
  font-style: italic;
  text-decoration: none;
  color: var(--accent-pay);
  border-bottom: 1px solid color-mix(in hsl, var(--accent-pay) 35%, transparent);
  padding-bottom: 1px;
  transition: color var(--dur-quick) ease, border-color var(--dur-quick) ease;
}
.about-wrap .contact-email:hover {
  color: #fff;
  border-color: rgba(255,255,255,.6);
}
.about-wrap .contact-topics {
  margin-bottom: 6px;
  opacity: .85;
}
.about-wrap .contact-list {
  list-style: none;
  padding: 0;
  margin: 0 auto 18px;
  max-width: 62ch;
  text-align: left;
  font-size: clamp(0.95rem, 1.9vw, 1.1rem);
  line-height: 1.6;
  color: rgba(255,255,255,.82);
}
.about-wrap .contact-list li {
  padding: 6px 0 6px 22px;
  position: relative;
}
.about-wrap .contact-list li::before {
  content: '·';
  position: absolute;
  left: 6px;
  top: 6px;
  color: rgba(255,255,255,.4);
  font-size: 1.3em;
  line-height: 1;
}

/* ── Shared title ───────────────────────────────────────── */
/* Title for manifesto and about pages */
.title-shared {
  font-family: 'Instrument Serif', Georgia, serif;
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  line-height: 1.1;
  letter-spacing: .01em;
  margin: 0 0 40px;
  font-weight: 400;
  text-align: center;
}
.title-shared span { display: block; font-weight: inherit; }
.about-wrap, .page { padding-top: 32px; padding-left: 24px; padding-right: 24px; text-align: center; }


.internal a       { color: var(--link-internal); text-decoration: underline; }
.internal a:hover { color: var(--link-internal-hover); }

/* ── Manifesto / .page ──────────────────────────────────── */
.page {
  max-width: 720px;
  margin: 0 auto;
  padding-bottom: 80px;
  padding-left: 22px;
  padding-right: 22px;
  text-align: center;
  box-sizing: border-box;
}
/* h1 inside .page wrapper — overrides home h1 transform */
.page h1 {
  font-family: 'Instrument Serif', Georgia, serif;
  transform: none !important;
  margin: 0 0 64px;
  font-weight: 400;
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  line-height: 1.1;
  letter-spacing: .01em;
}
.page p {
  font-size: clamp(1rem, 2vw, 1.25rem);
  line-height: 1.55;
  margin: 14px 0 18px;
  opacity: .95;
}
.page .back { margin-top: 40px; font-size: 1.25rem; text-decoration: none !important; color: var(--fg) !important; opacity: .85; display: inline-block; }
.page .back:hover { opacity: 1; color: var(--accent-pay) !important; }

/* ── Soundpage layout ───────────────────────────────────── */
body.page-layout.soundpage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  /* No bottom padding: the footer's own padding is the only breathing room,
     so it reaches the page bottom exactly like Privacy/Terms. (The old 60px
     was clearance for the removed version badge.) */
  padding: 24px 20px 0;
  min-height: 0;
  box-sizing: border-box;
  overflow-y: auto;
  text-align: center;
}

/* Title for soundpage only */
.title {
  font-family: 'Instrument Serif', Georgia, serif;
  font-weight: 400; letter-spacing: .01em;
  font-size: clamp(2.8rem, 6vw, 4.8rem); opacity: .95;
  margin: 0 0 56px; text-align: center;
  white-space: nowrap;
}

.stage {
  visibility: hidden;
  width: min(1020px, 96vw);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-rows: auto auto auto;
  grid-template-areas:
    "arcL silence arcR"
    "arcL center  arcR"
    "arcB arcB    arcB";
  gap: var(--stage-gap);
  margin-top: 8px;
  align-items: center;
}

.arc { display: flex; flex-direction: column; align-items: center; gap: 0; }
.arcL { grid-area: arcL; grid-row: 1 / 3; margin-top: var(--stage-shift); align-self: start; }
.arcR { grid-area: arcR; grid-row: 1 / 3; margin-top: var(--stage-shift); align-self: start; }
/* No alignment compensation needed between the two columns any more:
   both are title + dial from the top (the Note/Frequency toggle moved
   BELOW the pitch dial in v2.27). In breath mode the toggle row uses
   visibility:hidden to preserve its space below the dial. */
.arcB { grid-area: arcB; align-items: center; margin-top: 12px; }
.silenceSlot { grid-area: silence; display: flex; align-items: center; justify-content: center; padding: 8px 0; }

.arc svg { width: 100%; height: auto; max-width: var(--arc-svg-size); outline: none; }

/* Wrapper around each dial's SVG: a positioning box that exactly matches
   the SVG's own box (so percentage coordinates line up, independent of
   the SVG's internal scale). It hosts the HTML overlays: the Bloom and
   Intensity wing labels on the timbre dial, and the value-entry .inputs
   (octave stepper / frequency field / duration field) INSIDE the pitch
   and duration dials. */
.dial-wrap { position: relative; width: 100%; max-width: var(--arc-svg-size); container-type: inline-size; }

/* Value entry inside the dial. The 260° arc leaves the bottom of the
   circle open and the dial's value text ends at ~59% of the viewBox, so
   the lower band is free real estate. Anchored at the top (not centred)
   so the block can only grow DOWNWARD into the arc's opening, never up
   into the value text, at any dial scale. */
.dial-wrap > .inputs {
  position: absolute; left: 50%; top: 67%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  margin: 0; white-space: nowrap;
}
.dial-wrap > .inputs.hidden { display: none; }
.dial-wrap .inputs label { margin-right: 0; }
.dial-wrap .inputs input { width: 8ch; text-align: center; }
/* The base #octaveInputs rule lays label + stepper out as a row (and has
   ID specificity); inside the dial they stack like the other inputs. */
.dial-wrap #octaveInputs { flex-direction: column; gap: 6px; }

/* Editable value display — the dial's big value text IS the input
   (frequency in Frequency mode, duration always). Positioned at the SVG
   value text's former spot and styled exactly like it: SVG .value is
   font-size 20 in the 300-unit viewBox → 20/300 = 6.667cqw of the
   .dial-wrap container, letter-spacing .05em. A thin underline is the
   only edit affordance, brightening on hover/focus. */
.dial-wrap > .inputs.value-edit {
  flex-direction: row; align-items: baseline; gap: .3em;
  transform: translate(-50%, -50%);
}
/* (same specificity as the base .dial-wrap > .inputs rule's top: 67%) */
.dial-wrap > .inputs.value-edit--pitch { top: 54.5%; }  /* pitchTextBot's y: 120 of 220 */
.dial-wrap > .inputs.value-edit--dur   { top: 52.7%; }  /* old durText y: 116 of 220 */
.value-edit input, .value-edit .unit {
  font-size: 6.667cqw; letter-spacing: .05em; font-weight: 400;
  color: var(--fg); font-family: inherit; line-height: 1.15;
}
.dial-wrap .value-edit input {
  background: transparent; border: none; outline: none;
  border-bottom: 1px solid rgba(255,255,255,.22);
  border-radius: 0; padding: 0 0 1px; text-align: center;
  appearance: textfield; -moz-appearance: textfield;
  transition: border-color var(--dur-quick) ease;
}
.value-edit input::-webkit-outer-spin-button,
.value-edit input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.value-edit input:hover { border-bottom-color: rgba(255,255,255,.45); }
.value-edit input:focus { border-bottom-color: var(--fg); }
.dial-wrap .value-edit--pitch input { width: 5ch; }  /* up to 20000 */
.dial-wrap .value-edit--dur   input { width: 4.4ch; } /* always "X.XX" */

/* Dial titles — Pitch / Timbre / Duration above each dial, so the page
   names its controls without the instructions modal. Same uppercase
   spaced language as the SOUND—BREATH toggle, but quieter: these are
   labels, not buttons. In breath mode "Pitch" and "Timbre" would lie
   (the left dial becomes the breath fingerprint display, the right the
   breath's Intensity — both already named inside the dial), so they go
   visibility:hidden (space preserved); "Duration" stays true and stays.

   The title is sized to match the dial's INNER value text ("440 Hz",
   "1.50 s"): that text is SVG at font-size 20 in a 300-unit viewBox, so
   it renders at 20 × (dialWidth / 300) px and scales with the dial. The
   title box is therefore the dial's own width (a query container), and
   its text is 20/300 = 6.667cqw — exactly the same rendered height at
   every viewport. */
.dial-title {
  width: 100%; max-width: var(--arc-svg-size);
  /* Negative bottom margin: the title leans into the SVG's empty top
     band (the arc ink starts ~23 viewBox units down), sitting visibly
     closer to the dial. pointer-events:none so the overlap never steals
     drags from the dial. */
  margin: 0 auto -10px; text-align: center;
  container-type: inline-size;
  text-transform: uppercase; letter-spacing: .14em;
  font-weight: 600; color: var(--fg); opacity: .5;
  user-select: none; pointer-events: none;
}
.dial-title > span { font-size: 6.667cqw; }
body.is-breath-mode .dial-title--pitch,
body.is-breath-mode .dial-title--wave { visibility: hidden; }

/* Breath mode: the pitch dial is a passive signature display, not a control.
   The value line ("signature · NNN Hz") is rendered small and dim — a quiet
   readout under "Your breath", not a settable value (fits inside the inner
   ring, unlike the full-size value text). */
body.is-breath-mode #pitchTextBot {
  font-size: 11px;
  fill: rgba(242, 242, 242, .5);
  letter-spacing: .04em;
}
.wing-label {
  position: absolute; top: 96.7%; transform: translate(-50%, -50%);
  font-size: 15px; color: var(--fg); white-space: nowrap; pointer-events: none;
}


.center { grid-area: center; display: flex; flex-direction: column; align-items: center; gap: 14px; }

/* ── Buttons ─────────────────────────────────────────────── */
.btn-test {
  width: var(--btn-test-size); height: var(--btn-test-size); border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  background: #dcdcdc; color: var(--bg);
  border: 1px solid rgba(0,0,0,.08);
  letter-spacing: .06em; font-weight: 600; font-size: 0.55rem;
  transition: transform var(--dur-quick) ease, box-shadow var(--dur-med) ease, background var(--dur-med), opacity .2s;
}
.btn-test:hover { transform: translateY(-2px) scale(1.04); box-shadow: 0 10px 20px rgba(0,0,0,.25); }
.btn-test.disabled { opacity: .5; cursor: not-allowed; filter: grayscale(.2); }

.btn-release {
  position: relative; width: var(--btn-release-size); height: var(--btn-release-size); border-radius: 50%;
  border: 1px solid var(--line); background: transparent; color: var(--fg);
  display: grid; place-items: center; text-align: center; padding: 20px; cursor: pointer;
  transition: transform var(--dur-quick) ease, box-shadow var(--dur-med) ease, background-color var(--dur-med) ease, color var(--dur-med) ease, border-color var(--dur-med) ease;
}
.btn-release:hover { transform: translateY(-2px); box-shadow: 0 12px 26px rgba(0,0,0,.25), 0 0 32px rgba(156,240,230,.25); }
.btn-release.loading::after {
  content: ''; position: absolute; width: 22px; height: 22px;
  border: 2px solid rgba(255,255,255,.6); border-top-color: transparent;
  border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.label-release { text-transform: uppercase; letter-spacing: .06em; font-weight: 600; font-size: 0.9rem; line-height: 1.25; text-align: center; }
.btn-release.silence { background: var(--fg) !important; color: var(--bg) !important; border-color: var(--fg) !important; }
.btn-release.success { box-shadow: 0 0 0 6px rgba(255,255,255,.15); }

/* ── SOUND / BREATH mode toggle (v2.20.2) ──────────────────────────────
   Replaces the v2.20 binary "SILENCE" / "BREATH" button. The active mode
   is rendered in full opacity, the inactive in reduced opacity. Click
   anywhere on the button toggles. The slash separator stays subtle so
   the eye reads the two labels as the primary content. */
.btn-mode-toggle {
  width: var(--btn-silence-size);
  height: var(--btn-silence-size);
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.18);
  background: linear-gradient(180deg, #1c1c1c 0%, #111 100%);
  color: var(--fg);
  cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0;
  letter-spacing: .12em;
  font-weight: 600;
  font-size: .80rem;
  -webkit-tap-highlight-color: transparent; outline: none;
  transition: transform var(--dur-quick) ease, box-shadow var(--dur-med) ease, border-color var(--dur-med) ease;
  position: relative;
  padding: 0;
  line-height: 1.15;
}
.btn-mode-toggle:hover {
  transform: translateY(-1px) scale(1.03);
  box-shadow: 0 10px 22px rgba(0,0,0,.25);
  border-color: rgba(255,255,255,.30);
}
.btn-mode-toggle .mode-label {
  display: block;
  transition: opacity var(--dur-med) ease, color var(--dur-med) ease;
  opacity: .35;
  color: rgba(255,255,255,.65);
}
.btn-mode-toggle .mode-sep {
  display: block;
  opacity: .40;
  font-size: .70rem;
  margin: 1px 0;
}
/* SOUND mode active (default — when state.breathMode is false) */
body:not(.is-breath-mode) .btn-mode-toggle .mode-sound  { opacity: 1; color: #ffffff; }
body:not(.is-breath-mode) .btn-mode-toggle .mode-breath { opacity: .35; }
/* BREATH mode active */
body.is-breath-mode .btn-mode-toggle .mode-breath { opacity: 1; color: #ffffff; }
body.is-breath-mode .btn-mode-toggle .mode-sound  { opacity: .35; }

/* Subtle pulse around button only when in breath mode (breathing rhythm) */
body.is-breath-mode .btn-mode-toggle::after {
  content: ''; position: absolute; inset: -3px;
  border: 1px solid rgba(255,255,255,.20);
  border-radius: 50%; pointer-events: none;
  animation: mode-breath-pulse 3.6s ease-in-out infinite;
}
@keyframes mode-breath-pulse {
  0%, 100% { transform: scale(1.0); opacity: .25; }
  50%      { transform: scale(1.07); opacity: .55; }
}
@media (prefers-reduced-motion: reduce) {
  body.is-breath-mode .btn-mode-toggle::after { animation: none; }
}

/* In breath mode, the pitch dial outer arc is a passive frame, not a
   slider track. The round line-cap (which makes the slider feel
   "interactive") would render as little grey dots at the arc's ends —
   we want a clean cut instead. */
/* In breath mode, hide the Note/Frequency toggle row BUT preserve its
   space, so the pitch dial (arcL) and the wave dial (arcR) remain
   vertically aligned across modes. Use visibility:hidden, not display:none. */
.pitch-mode.hidden { visibility: hidden; }

/* ── Pitch dial in breath mode ───────────────────────────────────────────
   The pitch dial is a passive fingerprint display in breath mode. We:
   1) Make the outer arc thin & neutral (frame only, not a slider).
   2) Use butt line-caps so the arc ends don't render as little grey
      dots (which the round caps would create at the semicircle ends).
   3) Hide the active arc, inner ring, and handle completely. We use
      `visibility: hidden` on the SVG elements (more reliable than
      display: none for SVG children, and avoids any rendering ghosts
      from filters or drop-shadows). */
body.is-breath-mode #pitchArc {
  stroke-linecap: butt;
  stroke-width: 4 !important;
  stroke: rgba(200,200,200,.30) !important;
}
body.is-breath-mode #pitchActive,
body.is-breath-mode #pitchInner,
body.is-breath-mode #pitchHandle {
  visibility: hidden;
}

/* Center the multi-line release label perfectly inside the round release button */
.btn-release {
  display: flex !important;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 18px;
}
.btn-release .label-release {
  display: flex; align-items: center; justify-content: center;
  text-align: center;
  width: 100%;
  line-height: 1.15;
  margin: 0;
}

/* ── Breath dots in timeline ────────────────────────────────────────────
   A breath note sits at mid-height (no pitch axis). Soft, low-saturation
   circle with a thin inner ring to mark it as a different kind of
   contribution from a tonal note. */
.note--breath {
  border: 1.5px solid rgba(255,255,255,.45);
  box-shadow: 0 0 0 2px rgba(255,255,255,.05);
}
.note--breath.playing { border-color: rgba(255,255,255,.95); }
.note--silent { /* legacy v2.19 silence — kept untouched */ }


/* ── Accessible focus rings ────────────────────────────── */
:focus-visible { outline: 2px solid var(--accent-pay); outline-offset: 3px; border-radius: 4px; }
/* The dial SVGs carry `outline:none` (`.arc svg`, specificity 0,1,1) for the
   pointer aesthetic, which out-specifies the generic :focus-visible above and
   left the keyboard-operable dials (role="slider") with no visible focus.
   Restore a focus ring that out-specifies the reset. WCAG 2.4.7 (AA). */
.arc svg:focus-visible { outline: 2px solid var(--accent-pay); outline-offset: 3px; }


/* ── Prevent accidental text selection on interactive UI ─── */
.arc svg, .btn-test, .btn-release, .btn, .pmode-btn, .title, .octave-btn, .label-release {
  user-select: none;
  -webkit-user-select: none;
}

/* ── SVG track/handle styles ─────────────────────────────────────────────
   Scoped to `path.` (the dial arcs) on purpose: `stroke`/`stroke-width` are
   INHERITED SVG properties, and a bare `.active` selector also matches
   `.mobile-panel.active` / `.mobile-dot.active`. That made the whole mobile
   panel inherit stroke:accent-hot + width:14, painting a pink outline over
   the dial's value text. Anchoring to path. keeps the style on the arcs and
   off every unrelated `.active` element. */
path.track  { stroke: rgba(200,200,200,.35); stroke-width: 18; stroke-linecap: round; fill: none; opacity: .9; }
path.active { stroke: var(--accent-hot);     stroke-width: 14; stroke-linecap: round; fill: none; opacity: 1; }
path.inner  { stroke: rgba(255,255,255,.35); stroke-width: 3;  stroke-linecap: round; fill: none; opacity: .65; }

/* No transitions on active arcs */
#pitchActive, #waveActive, #durActive { transition: none !important; }
.handle { fill: var(--fg); cursor: grab; stroke: var(--bg); stroke-width: 3px; filter: drop-shadow(0 2px 2px rgba(0,0,0,.35)); transition: r var(--dur-quick) ease; }
.handle:active { cursor: grabbing; }
.value  { fill: var(--fg); font-weight: 400; letter-spacing: .05em; text-anchor: middle; dominant-baseline: middle; font-size: 20px; pointer-events: none; user-select: none; -webkit-user-select: none; }

/* ── Arc state classes ───────────────────────────────────── */
.disabled-arc  { opacity: .45; filter: grayscale(.35) contrast(.9); transition: opacity var(--dur-med) ease, filter var(--dur-med) ease; }
.focus-release { filter: drop-shadow(0 0 10px rgba(255,255,255,.25)); }

/* ── Timeline ────────────────────────────────────────────── */
.timeline {
  width: 100%; height: 240px;
  border: 1px solid var(--line); border-radius: 10px; margin-top: 28px;
  position: relative; overflow: auto;
  background: linear-gradient(to bottom, rgba(255,255,255,.03), transparent 40%, transparent 60%, rgba(255,255,255,.03));
}
.timeline.dense .note { box-shadow: none !important; transition: opacity var(--dur-med) ease, transform var(--dur-med) ease !important; }

.note {
  position: absolute; border-radius: 50%;
  will-change: transform, opacity, left, top, width, height, background-color;
  transform: translateZ(0); cursor: pointer;
  transition: all .45s cubic-bezier(.25,.8,.35,1);
}
/* Bokeh edge: feather only the outer rim of tonal note dots — softer, closer
   to the home halos than a hard circle. An alpha-only radial mask, so it is
   colour-agnostic (no grey fringe) and leaves the breath ring and the
   .playing outline crisp. Breaths and legacy silences keep their own look. */
.note:not(.note--breath):not(.note--silent) {
  -webkit-mask-image: radial-gradient(circle, #000 60%, transparent 72%);
          mask-image: radial-gradient(circle, #000 60%, transparent 72%);
}
.note.playing { outline: 2px solid rgba(255,255,255,.9); box-shadow: 0 0 10px rgba(255,255,255,.35); z-index: 2; opacity: 1 !important; }

/* ── Toolbar ─────────────────────────────────────────────── */
.toolbar { display: flex; gap: 10px; align-items: center; justify-content: center; margin-top: 54px; }
.btn { background: var(--fg); color: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; font-weight: 600; }
.btn:hover { filter: brightness(.95); }
.btn.stop { background: var(--accent-stop); color: var(--fg); border-color: var(--accent-stop); }

/* ── Touch handling ──────────────────────────────────────── */
#svgPitch, #svgWave, #svgDur,
#svgPitch *, #svgWave *, #svgDur * { touch-action: none; -ms-touch-action: none; pointer-events: auto; }

/* ── Pitch mode toggle (Note/Frequency) ──────────────────── */
.pitch-mode { height: var(--pitch-mode-h); display: flex; gap: 10px; align-items: center; justify-content: center; margin: 14px 0 0; }
.pmode-btn { background: transparent; color: var(--fg); border: 1px solid var(--line); border-radius: 999px; padding: 8px 14px; font-size: 13px; letter-spacing: .02em; cursor: pointer; opacity: .9; }
.pmode-btn:hover { opacity: 1; }
.pmode-btn[aria-pressed="true"] { background: var(--fg); color: var(--bg); border-color: var(--fg); }

.inputs.hidden    { display: none; }
.inputs label     { display: inline-block; margin-right: 12px; }

/* Octave selector */
#octaveInputs   { display: flex; align-items: center; gap: 12px; font-size: 15px; }
.octave-stepper { display: inline-flex; align-items: center; gap: 8px; }
.octave-btn { width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line); background: rgba(255,255,255,.08); color: var(--fg); cursor: pointer; font-size: 14px; }
.octave-btn:hover { filter: brightness(1.05); }
.octave-display { min-width: 2ch; text-align: center; font-variant-numeric: tabular-nums; }
body[data-pitch-mode="frequency"] #octaveInputs { display: none !important; }
/* In breath mode the pitch dial is a passive display; all pitch-related
   inputs (frequency entry, octave stepper) must be hidden. The ID
   selector above otherwise wins over the class-based .inputs.hidden
   rule, so !important here. (The inputs are absolute overlays inside
   the dial, so hiding them takes no space and shifts nothing; the
   Note/Frequency toggle below the dial keeps its space separately via
   .pitch-mode.hidden { visibility: hidden }.) */
body.is-breath-mode #freqInputs,
body.is-breath-mode #octaveInputs { display: none !important; }

/* ── a11y ─────────────────────────────────────────────────── */
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* ── Payment overlay ─────────────────────────────────────── */
#pay-overlay {
  position: fixed; inset: 0; z-index: 10000;
  display: grid; place-items: start center;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 16px 0;
  background:
    radial-gradient(1200px 420px at 50% 110%, color-mix(in hsl, var(--accent-pay)   26%, transparent), transparent 55%),
    radial-gradient(1200px 420px at 60% 120%, color-mix(in hsl, var(--accent-pay-2) 18%, transparent), transparent 60%),
    rgba(0,0,0,.62);
  backdrop-filter: blur(8px) saturate(1.03);
  animation: payFadeIn .16s ease-out;
}
@keyframes payFadeIn { from { opacity: 0; } to { opacity: 1; } }

.pay-card {
  position: relative;
  width: min(420px, 94vw);
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02)), var(--panel);
  border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 14px 14px 12px;
  box-shadow: 0 20px 50px rgba(0,0,0,.50), 0 0 0 1px rgba(255,255,255,.03) inset;
  overflow-x: hidden;
  overflow-y: auto;
  transform: translateY(6px) scale(.985); animation: payPop .18s cubic-bezier(.2,.9,.2,1) forwards;
}
@keyframes payPop { to { transform: none; } }

.pay-card::before, .pay-card::after {
  content: ''; position: absolute; inset: auto -20% -30% -20%; height: 45%;
  filter: blur(22px); pointer-events: none; opacity: .75;
  background:
    radial-gradient(1000px 200px at 40% 130%, color-mix(in hsl, var(--accent-pay)   35%, transparent), transparent 65%),
    radial-gradient(1000px 200px at 65% 120%, color-mix(in hsl, var(--accent-pay-2) 28%, transparent), transparent 70%);
}
.pay-card::after { inset: -10% -30% auto -30%; height: 32%; filter: blur(16px); opacity: .55; }

.pay-head {
  position: relative;
  display: block;
  margin-bottom: 6px; padding-bottom: 6px;
}
.pay-head #pay-close,
.pay-head #pay-close.btn {
  position: absolute;
  top: 0;
  right: 0;
  transform: none;
  background: transparent;
  border: 1px solid rgba(255,255,255,.14);
  width: 28px;
  height: 28px;
  min-width: 0;
  border-radius: 50%;
  padding: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  line-height: 1;
  color: rgba(255,255,255,.70);
}
.pay-head::after { content: ''; position: absolute; left: -18px; right: -18px; bottom: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,.12), transparent); }
.pay-title  { font-weight: 800; letter-spacing: .08em; text-transform: uppercase; font-size: .95rem; margin: 0; text-align: center; }
.pay-copy   { color: rgba(255,255,255,.70); font-size: .82rem; margin: 0; padding: 0 2px; text-align: center; }
.pay-copy strong { color: var(--fg); }
#payment-element { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.12); border-radius: 10px; padding: 8px; transition: border-color var(--dur-quick) ease; margin-top: 2px; }
#payment-element:focus-within { border-color: var(--accent-pay); box-shadow: 0 0 0 2px color-mix(in hsl, var(--accent-pay) 50%, transparent); }

.pay-spinner {
  position: relative; height: 28px; display: grid; place-items: center; color: rgba(255,255,255,.70);
}
.pay-spinner::before, .pay-spinner::after {
  content: ''; position: absolute; width: 64px; height: 10px;
  background: radial-gradient(6px 6px at 6px 50%, var(--accent-pay), transparent 60%) 0 0/16px 100% repeat-x;
  opacity: .9; mask: linear-gradient(90deg, transparent, #000 15% 85%, transparent);
}
.pay-spinner::before { transform: translateY(-6px); animation: waveShift 1.2s linear infinite; }
.pay-spinner::after  { transform: translateY(6px) scaleX(-1); animation: waveShift 1.2s linear infinite reverse; }
@keyframes waveShift { from { background-position-x: 0; } to { background-position-x: 64px; } }

.pay-err { color: var(--accent-err); min-height: 22px; font-size: .92rem; }

/* Echo input — stands out as a small framed field tinted with the
   colour of the note the user is about to release. The accent comes
   from --accent which is set on #pay-overlay when the dialog opens.
   Inline styles in payment.js are kept minimal; visual identity lives
   here so we can change it freely later. */
.pay-echo {
  width: 100%;
  box-sizing: border-box;
  background: color-mix(in hsl, var(--accent) 8%, transparent);
  border: 1px solid color-mix(in hsl, var(--accent) 35%, transparent);
  border-radius: 10px;
  padding: 10px 14px;
  color: var(--accent);
  font-size: 14px;
  font-style: italic;
  font-family: 'Instrument Serif', Georgia, serif;
  letter-spacing: .02em;
  outline: none;
  text-align: center;
  margin-bottom: 10px;
  transition: border-color var(--dur-quick) ease, background var(--dur-quick) ease;
}
.pay-echo::placeholder {
  color: color-mix(in hsl, var(--accent) 55%, transparent);
  font-style: italic;
}
.pay-echo:focus {
  border-color: var(--accent);
  background: color-mix(in hsl, var(--accent) 14%, transparent);
}
/* Silence mode (white accent): the tint above would look gray,
   so we slightly lift the contrast. */
#pay-overlay.pay-silence .pay-echo {
  color: rgba(255,255,255,.95);
}
#pay-overlay.pay-silence .pay-echo::placeholder {
  color: rgba(255,255,255,.50);
}

.pay-legal {
  /* Now a <label> wrapping a checkbox and explanatory text. The block
     turns into a small clickable consent surface above the action row.
     Larger and slightly more prominent than a passive disclaimer because
     it carries an explicit consent, which is the foundation of the
     contractual relationship with the participant. */
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 8px 2px 0;
  padding: 10px 12px;
  background: rgba(255,255,255,.03);
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 8px;
  font-size: .78rem;
  line-height: 1.5;
  color: rgba(255,255,255,.70);
  text-align: left;
  letter-spacing: .01em;
  cursor: pointer;
  transition: border-color var(--dur-quick) ease, background var(--dur-quick) ease;
}
.pay-legal:hover {
  border-color: rgba(255,255,255,.16);
  background: rgba(255,255,255,.05);
}
.pay-legal .pay-consent {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 2px 0 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.pay-legal .pay-consent-text {
  flex: 1 1 auto;
  user-select: none;
}
.pay-legal a {
  color: rgba(255,255,255,.85);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.pay-legal a:hover { color: var(--fg); }

/* ── Consent warning (v2.20.3) ─────────────────────────────────────────
   When the user clicks Confirm without ticking the consent checkbox, the
   warning text appears here and the label below briefly shakes/highlights
   to direct attention to the checkbox. */
.pay-consent-warning {
  min-height: 18px;
  margin: 6px 0 2px;
  font-size: .82rem;
  line-height: 1.35;
  color: #ff9a9a;
  letter-spacing: .01em;
}
@keyframes pay-legal-attention {
  0%, 100% { transform: translateX(0); border-color: rgba(255,154,154,.55); }
  25%      { transform: translateX(-3px); }
  75%      { transform: translateX(3px); }
}
.pay-legal--needs-attention {
  border: 1px solid rgba(255,154,154,.55) !important;
  border-radius: 8px;
  animation: pay-legal-attention 0.55s ease-out 1;
  background-color: rgba(255,154,154,.05);
  padding: 8px 10px;
}
@media (prefers-reduced-motion: reduce) {
  .pay-legal--needs-attention { animation: none; }
}

.pay-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 12px; border-top: 1px solid rgba(255,255,255,.12); padding-top: 12px; }

#pay-cancel.btn { background: transparent; color: rgba(255,255,255,.70); border: 1px solid rgba(255,255,255,.16); }
#pay-cancel.btn:hover { color: var(--fg); border-color: rgba(255,255,255,.25); }

#pay-confirm.btn {
  position: relative;
  background: linear-gradient(90deg, var(--accent), color-mix(in hsl, var(--accent) 60%, var(--accent-pay-2) 40%));
  color: #0d0f10; border: 0; border-radius: 12px; padding: 10px 16px;
  font-weight: 800; letter-spacing: .04em;
  box-shadow: 0 8px 22px color-mix(in srgb, var(--accent) 30%, transparent), 0 4px 18px rgba(222,73,160,.20);
  transition: transform var(--dur-quick) ease, filter var(--dur-quick) ease, box-shadow var(--dur-quick) ease;
}
#pay-confirm.btn:hover { transform: translateY(-1px); filter: saturate(1.06); box-shadow: 0 14px 30px rgba(53,224,198,.30), 0 10px 24px rgba(222,73,160,.28); }

/* Silence mode: clean white button (no gradient) — coherent with the silence circle */
#pay-overlay.pay-silence #pay-confirm.btn {
  background: #ffffff;
  color: #0b0b0b;
  box-shadow: 0 8px 22px rgba(255,255,255,.18), 0 4px 18px rgba(255,255,255,.10);
}
#pay-overlay.pay-silence #pay-confirm.btn:hover {
  box-shadow: 0 14px 30px rgba(255,255,255,.28), 0 10px 24px rgba(255,255,255,.18);
}

/* ── Responsive ──────────────────────────────────────────── */
@media (max-width: 420px) { .timeline { height: 200px; } }

@media (max-height: 750px) and (min-width: 601px) {
  .btn-release { width: 150px; height: 150px; font-size: .85rem; }
  .btn-test    { width: 76px;  height: 76px;  font-size: .85rem; }
  .stage       { gap: 10px; }
  .timeline    { height: 160px; }
}

@media (max-height: 620px) and (min-width: 601px) {
  .btn-release { width: 130px; height: 130px; font-size: .8rem; }
  .btn-test    { width: 66px;  height: 66px;  font-size: .8rem; }
  .timeline    { height: 120px; }
}

/* ================================================================

   Mobile step layout (≤ 600px)
   ============================================================ */

.mobile-wrap {
  display: none;
  position: relative; /* offset parent for the anchored Instructions button */
  flex-direction: column;
  align-items: center;
  width: min(96vw, 420px);
  gap: 20px;
  padding-top: 16px;
}

@media (max-width: 600px) {
  .mobile-wrap { display: flex; }
  .stage { display: none !important; }
  body.page-layout.soundpage { align-items: center; justify-content: flex-start; }
  /* Pull the whole instrument up toward the title so the footer / "Back to
     Home" stay on screen on a tall phone (the 56px desktop gap was large). */
  .title { margin-bottom: 10px; }
  /* mobile.js moves the Instructions button into the wrap; anchor it in the
     gap between the tabs and the dial (right-aligned to the content) so it
     scrolls with the page and follows the layout instead of floating fixed. */
  .mobile-wrap > .btn-instructions { position: absolute; top: 60px; right: 12px; }
}

/* Progress bar */
.mobile-progress {
  display: flex;
  align-items: center;
  gap: 0;
  width: 100%;
  justify-content: center;
}

.mobile-dot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  opacity: .38;
  transition: opacity var(--dur-med) ease;
  min-width: 48px;
  text-align: center;
}
.mobile-dot span { font-size: 18px; line-height: 1; }
.mobile-dot small { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }
.mobile-dot.active { opacity: 1; }
.mobile-dot.completed { opacity: .65; }

.mobile-dot-line {
  flex: 1;
  height: 1px;
  background: rgba(255,255,255,.15);
  margin: 0 4px;
  margin-bottom: 14px;
}

/* Panels */
.mobile-panels { width: 100%; }
.mobile-panel  { display: none; }
.mobile-panel.active { display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* The .arc nodes are designed for the desktop grid (their width comes from
   the grid column). Inside the mobile flex panel they have no column, so
   give them the full panel width — the dial SVG (width:100%, max-width
   --arc-svg-size) then fills it and centres instead of collapsing.
   We also neutralise the desktop grid offsets that otherwise misalign the
   dials per tab: arcL/arcR carry margin-top:var(--stage-shift) (−50px,
   which pulled the dial up over the tab bar) and arcB carries margin-top:
   12px. Zeroing them makes Pitch/Timbre/Duration start at the same y. */
.mobile-panel .arc { width: 100%; margin-top: 0; }
/* Breathing room between the tab bar and the dial (also the room the anchored
   Instructions button sits in). */
.mobile-panels { margin-top: 22px; }
/* The progress bar already names each step (Pitch · Timbre · Duration),
   so the in-dial title would be redundant on mobile. */
.mobile-panel .dial-title { display: none; }

/* Reserve one height for every DIAL step (not the Mode/Release steps, which are
   free to be their natural, shorter height) and top-align the dial, so the
   Back/Next bar sits at the same y and every dial's top lines up. The reserve is
   the same in both modes — the sound Pitch step is the tallest: its dial (viewBox
   300×220 → ~0.733 of width) carries the Note/Frequency toggle below it (~54px),
   taller than the pad (Intensity, ~0.814). Reserving it uniformly keeps the nav
   aligned across BOTH modes; leaving Mode/Release unreserved lets the Release
   step's contribution buttons + "listen" invite stay on screen without scroll.
   The value tracks the dial width (min(96vw, --arc-svg-size)) so it holds from a
   320px phone up to the 600px breakpoint. */
.mobile-panels {
  display: flex; flex-direction: column; justify-content: flex-start;
}
.mobile-panel:not(.mobile-mode-panel):not(.mobile-release-panel) {
  min-height: calc(min(96vw, 408px) * 0.733 + 54px);
}
/* In breath mode the pitch Note/Frequency toggle is irrelevant; it is already
   visibility:hidden, but on mobile we collapse it entirely so the Breath step
   doesn't reserve its row and end up taller than the others. */
body.is-breath-mode .mobile-panel .pitch-mode { display: none; }

/* Narrow non-touch advisory (toggled by mobile.js). Hidden by default via
   the [hidden] attribute; when shown it sits centered in the soundpage flow. */
.narrow-notice {
  max-width: 34ch; margin: 12vh auto 0; padding: 0 24px;
  text-align: center; color: rgba(255,255,255,.8);
}
.narrow-notice[hidden] { display: none; }
.narrow-notice-title {
  font-family: 'Instrument Serif', Georgia, serif;
  font-size: clamp(1.6rem, 6vw, 2.2rem); margin: 0 0 12px; opacity: .95;
}
.narrow-notice p { font-size: 15px; line-height: 1.6; margin: 0; }

/* Mode panel (step 0): the Sound/Breath choice, made first. */
.mobile-mode-panel { gap: 20px; padding: 24px 0; }
.mobile-mode-caption {
  margin: 0; max-width: 26ch; text-align: center;
  font-size: 14px; line-height: 1.5; opacity: .7;
}

/* Release panel — tight vertical padding so the contribution buttons, the
   "listen" invite and "Back to Home" all stay on screen on a phone. */
.mobile-release-panel { gap: 24px; padding: 6px 0; }
/* Same goal: trim the top margin of the "listen" invite when it sits at the
   bottom of the mobile wrap (its desktop 28px is roomier than a phone needs). */
#mobile-steps > .listen-rinvio { margin-top: 16px; }

/* Navigation */
.mobile-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.mobile-nav-btn {
  background: rgba(255,255,255,.08);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 20px;
  font-size: 13px;
  letter-spacing: .04em;
  cursor: pointer;
  transition: background var(--dur-quick) ease, transform var(--dur-quick) ease;
}
.mobile-nav-btn:hover { background: rgba(255,255,255,.14); transform: translateY(-1px); }

/* Summary bar */
.mobile-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  opacity: .7;
  flex-wrap: wrap;
  justify-content: center;
}
.ms-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.ms-sep { opacity: .35; }

/* ── Timeline last-dot gentle pulse ─────────────────────── */
.note.just-added {
  animation: dotPulse 1.6s ease-out;
  border-radius: 50%;
}
@keyframes dotPulse {
  0%   { box-shadow: 0 0 0 0 var(--accent-pay); }
  70%  { box-shadow: 0 0 0 14px rgba(156,240,230,0); }
  100% { box-shadow: 0 0 0 0 rgba(156,240,230,0); }
}

/* ── Instructions button (top-right, fixed) ──────────────── */
.btn-instructions {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.14);
  color: var(--fg);
  border-radius: 999px;
  padding: 8px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  cursor: pointer;
  transition: background var(--dur-quick) ease, transform var(--dur-quick) ease, border-color var(--dur-quick) ease;
}
.btn-instructions:hover {
  background: rgba(156,240,230,.12);
  border-color: var(--accent-pay);
  transform: translateY(-1px);
}
.btn-instructions svg { opacity: .8; }
.btn-instructions:hover svg { opacity: 1; }

/* ── Soundpage intro: full-screen blur veil via body::before ───────────────
   `transition: filter` with blur() does not interpolate smoothly in most
   browsers — it "snaps" instead of fading. We use a dedicated pseudo-element
   instead: a fixed full-screen layer that holds the backdrop blur, and we
   animate its OPACITY (which always transitions cleanly). The inline script
   in soundpage.html adds .intro-pending to <html> before the first paint, so
   the veil is already visible (opacity 1, blur active) at page load.
   When the user closes the modal, instructions.js removes .intro-pending and
   the veil fades out smoothly, revealing a sharp UI. The veil sits below the
   modal (z-index 9999 < 10000) so the modal stays on top of it. */
body.soundpage::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 1s cubic-bezier(0.22, 1, 0.36, 1);
}
html.intro-pending body.soundpage::before {
  opacity: 1;
}

/* During the intro, skip the body page fade-in. Otherwise the body starts at
   opacity 0 and fades to 1 over ~350ms — and during that fade the user sees
   the modal + veil also fading up from transparent, which reads as a flash.
   Pinning the body to opacity 1 from the start makes the modal pop in clean. */
html.intro-pending body {
  opacity: 1 !important;
  animation: none !important;
}

/* Same idea for the modal's own opening animations (instrFadeIn on the overlay,
   instrSlideIn on the card). When arriving via ?intro=1 we want the modal to
   appear ALREADY in place, with no fade-up and no slide-in — those transitions
   are what the user perceives as the residual "flash". For the manual
   Instructions button click later in the session, the animations still apply. */
html.intro-pending .instructions-overlay,
html.intro-pending .instructions-card {
  animation: none !important;
}

/* Last bit of the flash: the page UI (dials, title, counter, instructions
   button, footer) renders for a few frames before the blur veil finishes
   compositing — so the user catches a glimpse of sharp dials BEHIND the
   modal. visibility:hidden guarantees they are never rasterised at all
   during the intro, and `!important` overrides main.js's RAF assignment of
   `stage.style.visibility = 'visible'`. The class is removed by
   instructions.js closeModal, so the page reveals normally afterwards. */
html.intro-pending .stage,
html.intro-pending .title,
html.intro-pending .work-counter--soundpage,
html.intro-pending .btn-instructions,
html.intro-pending .listen-rinvio,
html.intro-pending body.soundpage > .back,
html.intro-pending body.soundpage > .public-footer {
  visibility: hidden !important;
}

/* ── Instructions modal ──────────────────────────────────── */
.instructions-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
  padding: 20px;
  animation: instrFadeIn var(--dur-med) ease-out;
}
@keyframes instrFadeIn { from { opacity: 0; } to { opacity: 1; } }
.instructions-overlay[hidden] { display: none; }

.instructions-card {
  position: relative;
  background: #131419;
  border: 1px solid rgba(255,255,255,.10);
  border-radius: 18px;
  /* Symmetric vertical padding (28/28) so the title is the same distance
     from the top edge as "Got it" is from the bottom edge — visually the
     card reads as truly centred, not subtly bottom-heavy. */
  padding: 28px 32px;
  max-width: 600px;
  width: 100%;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  box-shadow: 0 20px 60px rgba(0,0,0,.5);
  animation: instrSlideIn var(--dur-slow) cubic-bezier(.2,.7,.3,1);
}
/* The card is focused on open (so the dialog is announced) but it is a
   container, not a control — never show a focus ring around it. The visible
   focus indicator belongs on the interactive controls inside. */
.instructions-card:focus { outline: none; }

@keyframes instrSlideIn {
  from { opacity: 0; transform: translateY(20px) scale(.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.instructions-title {
  font-family: 'Instrument Serif', Georgia, serif;
  font-weight: 400;
  font-size: 1.9rem;
  margin: 0 0 22px;
  letter-spacing: .01em;
}

.instructions-list {
  margin: 0;
  padding: 0 0 0 22px;
  font-size: 0.96rem;
  line-height: 1.55;
  color: rgba(255,255,255,.88);
  /* Justified, to match the "considered document" register of the legal pages
     (which are also justified). Overrides the centred text the modal would
     otherwise inherit from the soundpage body. */
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
}
.instructions-list li {
  margin-bottom: 14px;
  padding-left: 4px;
  /* Balance the line breaks so a short tail (e.g. "be recovered") is not left
     alone on the last line. */
  text-wrap: balance;
}
.instructions-list li::marker {
  color: var(--accent-pay);
  font-weight: 600;
}
.instructions-list em {
  color: rgba(255,255,255,.96);
  font-style: italic;
}
.instructions-list strong {
  color: var(--fg);
  font-weight: 600;
  letter-spacing: .02em;
}

.instructions-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid rgba(255,255,255,.08);
}
.instructions-done {
  background: linear-gradient(90deg, var(--accent-pay), var(--accent-pay-2));
  color: #0d0f10;
  border: 0;
  padding: 10px 22px;
  font-weight: 700;
  letter-spacing: .04em;
  cursor: pointer;
  border-radius: 10px;
  transition: transform var(--dur-quick) ease, filter var(--dur-quick) ease;
}
.instructions-done:hover { transform: translateY(-1px); filter: saturate(1.08); }

/* Responsive: small screens */
@media (max-width: 480px) {
  .btn-instructions { padding: 7px 12px; font-size: 11px; }
  .instructions-card { padding: 22px 22px 18px; }
  .instructions-title { font-size: 1.6rem; }
  .instructions-list { font-size: 0.92rem; }
}

/* ── Echo popup ("eco del gesto") ─────────────────────────── */
/* Subtle, fragile, non-competitive with the sound. */
.echo-popup {
  position: fixed;
  transform: translate(-50%, -100%);
  z-index: 9999;
  pointer-events: none;
  background: transparent;
  color: rgba(255,255,255,.78);
  font-family: 'Instrument Serif', Georgia, serif;
  font-style: italic;
  font-size: 0.95rem;
  letter-spacing: .01em;
  padding: 4px 10px;
  white-space: nowrap;
  max-width: 90vw;
  opacity: 0;
  transition: opacity .35s ease;
}
.echo-popup.visible { opacity: 1; }

/* During playback: a soft frosted "bokeh" plaque — rounded, blurred backdrop
   and a gentle bluish glow, so the note's echo is clearly legible against the
   night background while still feeling soft. Faster fade, synced to the sound. */
.echo-popup.playback { transition: opacity .2s ease; }

/* Listen page — the "echo of the gesture" for BOTH click and playback: a soft
   frosted bokeh plaque (rounded, blurred, gentle bluish glow), more visible and
   clearly legible. It sits in the band above the timeline's top edge (see the
   generous timeline margin-top), well separated from the "Press play" sub-line. */
body.listen .echo-popup {
  /* No plaque — just the phrase floating on its soft bokeh halo (::before). */
  color: rgba(255,255,255,.96);
  font-size: 1.05rem;
  padding: 9px 20px;
  text-shadow: 0 1px 8px rgba(0,0,0,.6), 0 0 14px rgba(0,0,0,.4);
}
/* Soft multi-hue bokeh halo around the phrase — echoes the home's blurred,
   note-coloured halos: a cluster of translucent radial blobs (the cool end of
   the spectrum) bleeding out beyond the plaque, heavily blurred. */
body.listen .echo-popup::before {
  content: '';
  position: absolute;
  inset: -22px;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 38%, hsla(232,78%,70%,.34) 0%, transparent 58%),
    radial-gradient(circle at 72% 62%, hsla(288,64%,72%,.28) 0%, transparent 58%),
    radial-gradient(circle at 52% 26%, hsla(182,64%,70%,.22) 0%, transparent 56%);
  filter: blur(15px);
}

@media (prefers-reduced-motion: reduce) {
  .echo-popup { transition: none; }
}


/* ============================================================
   Legal pages (terms.html, privacy.html)
   Adapted from .internal style — wider, left-aligned for readability
   ============================================================ */

.legal-wrap {
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
  /* Title aligned with the other internal pages (about/contact use 32px). */
  padding: 32px 22px 80px;
  box-sizing: border-box;
  text-align: left;
}

.legal-wrap .title-shared {
  text-align: center;
  margin-bottom: 8px;
}

.legal-meta {
  text-align: center;
  font-size: 0.85rem;
  opacity: 0.55;
  letter-spacing: 0.04em;
  margin: 0 0 48px 0;
}

.legal-section {
  margin: 0 auto 32px;
  /* Fixed (font-independent) column. `ch` was making each element a different
     width — 1ch depends on the element's own font-size — so the intro, the
     headings and the body paragraphs ended up on three different left edges.
     A rem column keeps them all on the same edge. */
  max-width: 34rem;
}

.legal-section h2 {
  font-size: clamp(1.15rem, 2.2vw, 1.35rem);
  font-weight: 600;
  letter-spacing: 0.02em;
  margin: 0 0 14px 0;
  opacity: 0.95;
}

.legal-section h3 {
  /* Sub-chapter (e.g. 4.1) — sits in the hierarchy ABOVE the body text
     (clamp 1–1.15rem) and below the chapter h2 (clamp 1.15–1.35rem). */
  font-size: clamp(1.05rem, 1.9vw, 1.2rem);
  font-weight: 600;
  letter-spacing: 0.01em;
  margin: 22px 0 8px 0;
  opacity: 0.95;
}

.legal-section p {
  font-size: clamp(0.92rem, 1.7vw, 1rem);
  line-height: 1.65;
  opacity: 0.85;
  margin: 0 0 12px 0;
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
}

.legal-intro {
  font-size: clamp(0.95rem, 1.8vw, 1.05rem);
  line-height: 1.6;
  opacity: 0.85;
  margin: 0 auto 40px;
  /* Same fixed column as .legal-section so its edges line up with the body. */
  max-width: 34rem;
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
}

.legal-section ul {
  /* Match the body-paragraph ruler below (was a smaller clamp, so bullet text
     read smaller than the surrounding prose). */
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  line-height: 1.6;
  color: var(--fg);
  opacity: .88;
  margin: 0 0 14px 0;
  padding-left: 22px;
}

.legal-section li {
  margin: 0 0 8px 0;
}

.legal-section strong {
  color: var(--fg);
  font-weight: 600;
  opacity: 1;
}

.legal-related {
  margin: 48px 0 16px;
  font-size: 0.95rem;
  opacity: 0.8;
  text-align: center;
}

.legal-wrap .back {
  display: inline-block;
  margin: 24px auto 0;
  font-size: 1rem;
  text-decoration: none !important;
  color: var(--fg) !important;
  opacity: 0.75;
}
.legal-wrap .back:hover { opacity: 1; }

/* The trailing back-link should be centered on the page like the title */
.legal-wrap > .back {
  display: block;
  text-align: center;
  width: fit-content;
  margin-left: auto;
  margin-right: auto;
}

/* Mobile tweaks */
@media (max-width: 520px) {
  .legal-wrap { padding-left: 18px; padding-right: 18px; }
  .legal-section { margin-bottom: 28px; }
}


/* ============================================================
   Public footer (index, about, manifesto, terms, privacy)
   Sober, low-emphasis, fixed at the bottom of the viewport on
   tall pages, normal flow on short pages.
   ============================================================ */

.public-footer {
  width: 100%;
  margin: 56px auto 0;
  padding: 18px 22px calc(18px + env(safe-area-inset-bottom));
  text-align: center;
  font-size: .72rem;
  letter-spacing: .04em;
  color: rgba(255,255,255,.62); /* WCAG AA: was .40 (~3.8:1) → ~7:1 on #0b0b0b */
  box-sizing: border-box;
}

.public-footer a {
  color: rgba(255,255,255,.62); /* WCAG AA: was .55 (~3.1:1 once home opacity stacked) */
  text-decoration: none;
  transition: color var(--dur-quick) ease;
}
.public-footer a:hover { color: rgba(255,255,255,.85); }

.public-footer .sep {
  display: inline-block;
  margin: 0 .55em;
  color: rgba(255,255,255,.25);
}

/* On home page (where layout is centered/stacked) make the body a vertical
   flex column so the footer naturally sits at the bottom while the stack
   stays centered in the remaining space. */
body.home { flex-direction: column; }
body.home .center-stack { flex: 1; width: 100%; }
body.home .public-footer { margin-top: 0; flex-shrink: 0; }

@media (max-width: 520px) {
  .public-footer { font-size: .68rem; padding-left: 14px; padding-right: 14px; }
  .public-footer .sep { margin: 0 .4em; }
}


/* ====================================================================
   v2.21 — /listen page styles (required by listen.html since v2.21)
   These were missing from the v2.20 baseline; included here so a
   v2.20-direct upgrade to v2.22 has all required styles in one drop.
   ==================================================================== */
/* Deep blue-night base — the same family as the home, a soft navy bloom up
   top fading to near-black, tying /listen to the home's artistic identity.
   Painted on a FIXED pseudo-element, NOT the body background: a radial gradient
   on the scrolling body is viewport-sized and repeats vertically once the page
   exceeds the viewport, leaving a seam (the dark band over the toolbar). A
   fixed layer paints the navy once, behind all content, at any scroll position. */
body.listen {
  display: block;
  text-align: center;
}
body.listen::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(135% 110% at 50% -10%, #0c1631 0%, #070b1a 48%, #04040a 100%);
}

.listen-wrap {
  max-width: 920px;
  width: 100%;
  margin: 0 auto;
  padding: clamp(36px, 6vh, 72px) 22px 80px;
  box-sizing: border-box;
  text-align: center;
}

/* Hero title — larger and tighter, closer to the home title's scale */
body.listen .title-shared {
  font-size: clamp(2.8rem, 6.5vw, 4.4rem);
  letter-spacing: .005em;
  margin-bottom: 14px;
}

.listen-sub {
  font-size: clamp(.95rem, 1.6vw, 1.08rem);
  line-height: 1.55;
  opacity: .7;
  letter-spacing: .015em;
  margin: 0 auto 30px;
  max-width: 52ch;
}

/* The timeline is the stage: a floating, glowing dark panel — a faint navy
   bloom and an inset vignette make the colour-coded notes read as light. */
body.listen .timeline {
  height: clamp(280px, 48vh, 420px);
  /* Generous band above the timeline: the echo-of-the-gesture plaque appears
     here, between the timeline's top edge and the "Press play" sub-line. */
  margin-top: 60px;
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 16px;
  background:
    radial-gradient(110% 80% at 50% 50%, rgba(70,100,170,.10) 0%, transparent 72%),
    linear-gradient(to bottom, rgba(255,255,255,.025), transparent 28%, transparent 72%, rgba(255,255,255,.025));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.05),
    inset 0 0 70px rgba(0,0,0,.45),
    0 6px 16px rgba(0,0,0,.30);   /* tight float — must not reach the toolbar */
}

.work-counter--listen { margin: 0 auto 18px; opacity: .75; }

.listen-empty {
  font-size: .95rem;
  opacity: .7;
  margin-top: 18px;
  font-style: italic;
}
.listen-empty a {
  color: var(--fg);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.listen-wrap .back {
  display: inline-block;
  margin-top: 48px;
  font-size: 1.05rem;
  text-decoration: none;
  color: var(--fg);
  opacity: .78;
  transition: opacity var(--dur-med) ease;
}
.listen-wrap .back:hover { opacity: 1; }

/* ── Unified "Back to Home" link across ALL internal pages ───────────────
   One identity everywhere: same size and the same cyan hover (previously
   only about/contact/manifesto had it; terms/privacy/listen did not). Each
   page keeps its own margin (set in its wrapper's .back rule above); only the
   button's appearance is unified here. Placed after all per-page rules so it
   wins on these shared properties. */
.about-wrap .back,
.page .back,
.legal-wrap .back,
.listen-wrap .back,
body.soundpage > .back {
  font-size: 1.25rem;
  text-decoration: none !important;
  color: var(--fg) !important;
  opacity: .85;
  transition: color var(--dur-med) ease, opacity var(--dur-med) ease;
}
.about-wrap .back:hover,
.page .back:hover,
.legal-wrap .back:hover,
.listen-wrap .back:hover,
body.soundpage > .back:hover {
  opacity: 1;
  color: var(--accent-pay) !important;
}

/* Soundpage: the back link is a direct child of <body> (no wrapper as on
   other pages), so block + center + top margin live on this rule. */
body.soundpage > .back {
  display: block;
  text-align: center;
  margin: 40px auto 0;
}

/* Ghost button variant — used only on /listen (Refresh) */
.btn.ghost {
  background: transparent;
  color: var(--fg);
  border: 1px solid rgba(255,255,255,.25);
}
.btn.ghost:hover { background: rgba(255,255,255,.06); }
.btn:disabled    { opacity: .45; cursor: not-allowed; }

/* Listen toolbar — pill buttons with a gentle lift and a calmer rhythm.
   Scoped to body.listen so the soundpage buttons are untouched. */
body.listen .toolbar { margin-top: 40px; gap: 12px; }
body.listen .toolbar .btn {
  border-radius: 999px;
  padding: 10px 22px;
  letter-spacing: .03em;
  transition: transform var(--dur-quick) ease, filter var(--dur-quick) ease, background var(--dur-quick) ease;
}
body.listen .toolbar .btn:hover { transform: translateY(-1px); }

@media (max-width: 420px) {
  body.listen .timeline { height: clamp(220px, 42vh, 320px); }
  .listen-wrap          { padding-top: clamp(24px, 5vh, 52px); }
}

/* ── Release confirmation toast ─────────────────────────────────────────
   Appears on /listen after a successful release from /soundpage.
   Floats fixed at the bottom, glass-morphism style matching the echo-popup. */
.release-toast {
  position: fixed;
  bottom: 36px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255,255,255,.10);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,.15);
  color: rgba(255,255,255,.92);
  font-size: .95rem;
  letter-spacing: .02em;
  padding: 14px 28px;
  border-radius: 999px;
  max-width: min(520px, 90vw);
  text-align: center;
  z-index: 200;
  opacity: 1;
  transition: opacity .5s ease;
  white-space: nowrap;
}
.release-toast--fade { opacity: 0; }
@media (max-width: 420px) {
  .release-toast { white-space: normal; bottom: 24px; padding: 12px 20px; }
}
@media (prefers-reduced-motion: reduce) {
  .release-toast { transition: none; }
}


/* ====================================================================
   v2.22.0 — Home (B' v3): disciplined typography
   Three scales only: title (large), poetic stanza (medium),
                      everything else (small).
   One case rule: sentence case (or Title Case for proper nouns).
   Two voices:
     - sans-serif      → product voice (title, button, gestures, footer)
     - Cormorant       → work's voice (poetic stanza, closing counter,
       Garamond italic    soundpage rinvio link to /listen)
   ==================================================================== */

/* Home — tighter rhythm, room around the stanza */
body.home .center-stack {
  gap: 26px;
  padding: 8vh 22px;
}

/* Title — slightly more restrained than the global h1 */
body.home h1 {
  font-size: clamp(2.6rem, 7vw, 4.4rem);
  margin: 0;
  letter-spacing: .01em;
}

/* The work's voice — used on home + listen + closing counter */
.poetic-stanza {
  font-family: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.15rem, 2.2vw, 1.55rem);
  line-height: 1.4;
  letter-spacing: .005em;
  margin: 0;
  max-width: 32ch;
  opacity: .94;
  color: var(--fg);
}

/* Doors of the work — quiet, paired, beneath the stanza */
.doors-of-work {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  letter-spacing: .02em;
  opacity: .6;
  margin-top: -10px;
}
.doors-of-work .doors-prefix { opacity: .8; }
.doors-of-work a {
  color: var(--fg);
  text-decoration: none;
  transition: opacity var(--dur-med) ease;
}
.doors-of-work a:hover,
.doors-of-work a:focus-visible {
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.doors-of-work .sep { opacity: .55; }

/* Add One Note — proportional discipline (replaces v2.0 clamp 1.6–2rem) */
body.home .btn-like {
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  font-weight: 500;
  letter-spacing: .03em;
  padding: 14px 32px;
  border-radius: 6px;
  box-shadow: 0 8px 20px rgba(0,0,0,.18);
}

/* Listen — the second gesture, same size as the button, no background */
.listen-gesture {
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  font-weight: 400;
  letter-spacing: .03em;
  color: var(--fg);
  text-decoration: none;
  padding: 6px 4px;
  opacity: .92;
  transition: opacity var(--dur-med) ease, letter-spacing var(--dur-med) ease;
}
.listen-gesture:hover,
.listen-gesture:focus-visible {
  opacity: 1;
  letter-spacing: .06em;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.listen-gesture:focus-visible {
  outline: 2px solid rgba(255,255,255,.4);
  outline-offset: 4px;
  border-radius: 3px;
}

/* Closing counter (last 99) — italic serif, same voice as the stanza.
   Targets only the active state; the sealed state keeps its existing
   sans-uppercase gravity. */
.work-counter--active {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-style: italic;
}

/* Public footer on home — disciplined to the metatext scale */
body.home .public-footer {
  font-size: clamp(0.8rem, 1.2vw, 0.9rem);
  /* opacity:.65 removed: it stacked on the rgba colours and pushed the footer
     links/copyright below WCAG AA. The muted look now lives in the .62 colours
     above, which pass AA on their own. */
  letter-spacing: .02em;
}

/* Soundpage — quiet poetic link to /listen, replacing the inline timeline */
.listen-rinvio {
  display: inline-block;
  margin: 28px auto 0;
  padding: 12px 8px;
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-style: italic;
  font-size: clamp(0.95rem, 1.4vw, 1.1rem);
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  color: var(--fg);
  opacity: .68;
  max-width: 36ch;
  transition: opacity var(--dur-med) ease;
}
.listen-rinvio:hover,
.listen-rinvio:focus-visible { opacity: 1; }
.listen-rinvio-arrow {
  display: inline-block;
  margin-left: 6px;
  transition: transform var(--dur-med) ease;
}
.listen-rinvio:hover .listen-rinvio-arrow,
.listen-rinvio:focus-visible .listen-rinvio-arrow { transform: translateX(4px); }

/* Mobile: keep proportions honest */
@media (max-width: 540px) {
  body.home .center-stack { gap: 22px; padding: 6vh 18px 4vh; }
  body.home h1            { font-size: clamp(2.4rem, 11vw, 3.2rem); }
  .poetic-stanza          { font-size: clamp(1.1rem, 4.2vw, 1.35rem); max-width: 28ch; }
}

/* ====================================================================
   v2.23.0 — Level B typography unification
   Two voices, two functions:
     - sans-serif       → product voice (UI, structure, legal)
     - Cormorant italic → work's voice (manifesto, poetic openings,
                          poetic closings, signatures, soundpage rinvio,
                          closing counter, home + listen stanzas)
   No grey-down hierarchy of links. No leftover blue. One body-paragraph
   ruler applied across pages that carry running text.
   ==================================================================== */

/* ── Body paragraphs — unified rhythm across the structural pages
      (.about-wrap, .legal-wrap .legal-section). All share:
      size 1–1.15rem, line-height 1.6, opacity .88, max-width 62ch. ── */
.about-wrap p,
.legal-section p {
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  line-height: 1.6;
  color: var(--fg);
  opacity: .88;
  margin: 16px auto;
  max-width: 62ch;
  text-align: justify;
}
/* Legal body paragraphs fill the section column (above) instead of the unified
   62ch measure, so their left/right edges line up with the intro and headings. */
.legal-section p {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
}

/* ── Internal links — one rule for all internal-text pages (no more
      blue #3366ff in about, no scattered greys). White-tenue underline,
      hover restores full opacity. ── */
.about-wrap a:not(.back):not(.contact-email),
.legal-section a,
.legal-wrap a:not(.back) {
  color: var(--fg);
  opacity: .88;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: opacity var(--dur-med) ease;
}
.about-wrap a:not(.back):not(.contact-email):hover,
.legal-section a:hover,
.legal-wrap a:not(.back):hover { opacity: 1; }

/* Email addresses never break mid-token across a line. */
.legal-wrap a[href^="mailto:"],
.about-wrap a[href^="mailto:"] { white-space: nowrap; }

/* Manifesto: every paragraph speaks in the work's voice */
.page p {
  font-family: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.15rem, 2vw, 1.45rem);
  line-height: 1.55;
  letter-spacing: .002em;
  color: var(--fg);
  opacity: .94;
  margin: 22px auto;
  max-width: 40ch;
  font-display: swap;
}

/* ====================================================================
   Home background — Echo halos + cursor halo
   ==================================================================== */
body.home { position: relative; }

#home-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
  /* Deep blue-night base — a faint navy wash up top fading to near-black at
     the edges, so the coloured bokeh reads as light against darkness. */
  background:
    radial-gradient(120% 120% at 50% 28%,
      #0a1226 0%, #070a16 55%, #04040a 100%);
}

body.home .center-stack {
  position: relative;
  z-index: 1;
}

/* Footer must also sit above #home-bg, or the echo halos veil its text */
body.home .public-footer {
  position: relative;
  z-index: 1;
}

/* Echo halos — two layers per disc:
   .hbg-echo      → wrapper: position + size; handles the entrance/exit
                    crossfade when the work's recent notes change.
   .hbg-echo-core → the bokeh itself: gradient (set inline), blur, and the
                    ongoing "breathing" pulse. Kept separate so the crossfade
                    opacity and the pulse opacity don't fight over one property.
   Position (left/top), size and colour are set per-disc in home-background.js;
   the blur gives the out-of-focus softness of the reference images. */
.hbg-echo {
  position: absolute;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 1.6s ease;
  will-change: opacity;
}
.hbg-echo.visible { opacity: 1; }

.hbg-echo-core {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  filter: blur(8px);
  animation: hbg-pulse linear infinite;
  will-change: opacity;
}

@keyframes hbg-pulse {
  0%, 100% { opacity: 0.18; }
  50%       { opacity: 0.52; }
}

/* Cursor halo: soft disc following the pointer. White by default; JS tints
   it with the work's note hues on each re-entry (see home-background.js). */
.hbg-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 380px;
  height: 380px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,.05) 0%, transparent 65%);
  opacity: 0;
  transition: opacity .7s ease, background .9s ease;
  pointer-events: none;
  will-change: transform, opacity;
}

@media (prefers-reduced-motion: reduce) {
  /* No fade, no pulse, no live loop (the JS skips it). Show a calm, static
     field. The wrapper appears immediately; the core sits at a fixed glow. */
  .hbg-echo      { transition: none; opacity: 1; }
  .hbg-echo-core { animation: none; opacity: .32; }
  .hbg-cursor    { display: none; }
}


/* ====================================================================
   prefers-reduced-motion: targeted end-states (must be LAST)
   --------------------------------------------------------------------
   The global catch-all near the top of this file already zeroes every
   animation/transition duration. This block pins the *visual end-state*
   for elements where "instant transition" alone would look abrupt or
   leave the element in an awkward intermediate appearance.
   ==================================================================== */
@media (prefers-reduced-motion: reduce) {
  /* Instructions modal — appears in place, no slide, no fade. */
  .instructions-overlay { animation: none !important; opacity: 1 !important; }
  .instructions-card    { animation: none !important; transform: none !important; opacity: 1 !important; }

  /* Soundpage intro blur veil — pinned to a static state (visible only when
     intro-pending is set, hidden otherwise). The catch-all already removed
     the opacity transition; this just guarantees there is no animation. */
  body.soundpage::before { transition: none !important; }
}
