/* Self-hosted fonts (was Google Fonts CDN) — same families/weights/unicode-ranges,
   minus the unused 'vietnamese' subset. font-display:swap preserved.
   Removes the third-party connection + render-blocking external stylesheet. */
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/fraunces-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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-400-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-400-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-500-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-500-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-500-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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/fonts/ibm-plex-mono-500-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;
}

  /* ---------- Theme tokens ---------- */
  :root {
    --paper:        #FBFAF6;
    --paper-raised: #FFFFFF;
    --faq-bg:       #F2F1EB;   /* FAQ reads as its own chapter off the base cream */
    --ink:          #1C1B17;
    --ink-soft:     #54524A;
    --ink-faint:    #76736A;   /* darkened from #8B887D to clear WCAG AA 4.5:1 on cream (used on pricing/credits/fine-print, not just decoration) */
    --rule:         #E6E3D9;
    --rule-soft:    #EFEDE5;
    --accent:       #2A7DFF;   /* links / UI accent — Soulver's system-tint blue (matches the editor caret) */
    --accent-soft:  #E3EEFF;
    --accent-ink:   #1A5FD0;
    --sheet-bg:     #FFFFFF;
    --sheet-ans-bg: #F4F7FC;
    --sheet-edge:   rgba(0,0,0,.15);   /* demo-window border. Translucent black (not the warm --rule) so the edge keeps a crisp macOS-like contrast over both the white sheet and the cream page; dark mode overrides with an opaque value. */
    --highlight:    #FFF3D6;
    --shadow:       0 1px 2px rgba(28,27,23,.04), 0 8px 28px rgba(28,27,23,.06);
    --maxw:         680px;   /* body reading column — narrow enough that left-aligned prose reads as a centered slab under the wider centered hero, but wide enough the longest H3 stays one line */
    --maxw-wide:    860px;
  }
  /* Accessibility: skip-to-content link — off-screen until keyboard focus. */
  .skip-link {
    position: absolute; left: 12px; top: -56px; z-index: 1000;
    background: var(--paper-raised); color: var(--ink);
    padding: 10px 16px; border-radius: 9px;
    font: 600 14px/1 "Hanken Grotesk", system-ui, sans-serif;
    text-decoration: none; box-shadow: var(--shadow);
    transition: top .15s ease;
  }
  .skip-link:focus { top: 12px; outline: 2px solid var(--accent); outline-offset: 3px; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] {
      --paper:        #16161A;
      --paper-raised: #1D1D22;
      --faq-bg:       #191A1D;
      --ink:          #EDEBE3;
      --ink-soft:     #AEACA2;
      --ink-faint:    #87857B;   /* lightened from #76746C to clear WCAG AA 4.5:1 on the dark raised surface (#1d1d22), where testimonial credits etc. sit */
      --rule:         #2C2C33;
      --rule-soft:    #24242A;
      --accent:       #5EA8FF;
      --accent-soft:  #182A44;
      --accent-ink:   #8FC1FF;
      --sheet-bg:     #1B1B20;
      --sheet-ans-bg: #16243A00;
      --sheet-edge:   #313139;
      --highlight:    #3A3320;
      --shadow:       0 1px 2px rgba(0,0,0,.3), 0 10px 30px rgba(0,0,0,.35);
    }
  }
  :root[data-theme="dark"] {
    --paper:        #16161A;
    --paper-raised: #1D1D22;
    --faq-bg:       #191A1D;
    --ink:          #EDEBE3;
    --ink-soft:     #AEACA2;
    --ink-faint:    #87857B;   /* lightened from #76746C to clear WCAG AA 4.5:1 on the dark raised surface (#1d1d22), where testimonial credits etc. sit */
    --rule:         #2C2C33;
    --rule-soft:    #24242A;
    --accent:       #5EA8FF;
    --accent-soft:  #182A44;
    --accent-ink:   #8FC1FF;
    --sheet-bg:     #1B1B20;
    --sheet-ans-bg: #1A2840;
    --sheet-edge:   #313139;
    --highlight:    #3A3320;
    --shadow:       0 1px 2px rgba(0,0,0,.3), 0 10px 30px rgba(0,0,0,.35);
  }

  /* ---------- Reset / base ---------- */
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
  body {
    background: var(--paper);
    color: var(--ink);
    font-family: "Hanken Grotesk", -apple-system, system-ui, sans-serif;
    font-size: 18px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    transition: background .4s ease, color .4s ease;
  }
  /* No custom ::selection — use the OS/browser default (the user's own
     system highlight colour). Zac's call: standard, unsurprising. */
  a { color: var(--accent); text-decoration: none; }
  a:hover { text-decoration: underline; text-underline-offset: 3px; }
  /* In-prose links carry a subtle resting underline (a non-color cue) so they're
     distinguishable without relying on color alone — WCAG 1.4.1. The dark-mode
     accent is too close in luminance to the grey prose text for color to do this
     job, so the underline is the fix. Scoped to body prose only; nav/footer/CTA
     links stay clean. Muted underline strengthens to full accent on hover. */
  .section-lede a, .lede-aside a, figcaption a, .ans a {
    text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px;
    text-decoration-color: color-mix(in srgb, var(--accent) 42%, transparent);
  }
  .section-lede a:hover, .lede-aside a:hover, figcaption a:hover, .ans a:hover {
    text-decoration-color: var(--accent); text-underline-offset: 2px;
  }

  .wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 28px; }
  .wrap-wide { max-width: var(--maxw-wide); margin: 0 auto; padding: 0 28px; }
  /* The hero is centered text; give it a roomier centered column so the big
     H1 stays on one line and reads generous, while the body's reading column
     (--maxw) is deliberately narrower so left-aligned prose still sits as a
     centered slab on the same page-center axis as the hero. */
  .hero .wrap { max-width: var(--maxw-wide); }
  /* Demo sheets share the body reading column — flush-left with their labels
     so the whole section reads as one centered column. */

  section { padding: 72px 0; border-top: 1px solid var(--rule-soft); }
  /* Essence flows straight out of the hero (no divider); both now live
     inside .topwash, so target it directly rather than :first-of-type. */
  #essence { border-top: 0; }

  h1, h2, h3 { font-family: "Fraunces", Georgia, serif; font-weight: 500; line-height: 1.12; letter-spacing: -.01em; }
  .eyebrow {
    font-family: "Hanken Grotesk", sans-serif;
    font-size: 13px; font-weight: 600; letter-spacing: .12em;
    text-transform: uppercase; color: var(--ink-faint);
    margin-bottom: 22px;
  }
  .section-h2 { font-size: clamp(28px, 4.4vw, 42px); margin-bottom: 44px; }
  .section-h2 + .block > h3 { margin-top: 0; }
  .section-h2 + .faq-group { margin-top: 0; padding-top: 30px; }
  /* Centered axis, but only for the top-level header zone: the big section
     titles (.section-h2) are centered to match the centered hero and to pair
     with the centered segmented control. Every heading that introduces a text
     block (.beat h3, .block h3, .blk-sub, .faq-group h3) stays LEFT, flush with
     its own text block — a sub-heading and its prose are one coupled unit and
     must share a left edge; a centered label floating over left body reads as
     a mistake. So: header zone centered, editorial body left. */
  .section-h2 { text-align: center; }
  /* "Thoughtfully crafted for each device." is the one section title longer than
     the 680px body column allows at the 42px cap, so it wrapped to two lines even
     on wide screens. Since section titles are centered on the page axis (not the
     body's left edge), let this one break out of the narrow column and take the
     width it needs — still wrapping gracefully once the viewport is genuinely
     narrower than the line. */
  #apps .section-h2 {
    position: relative; left: 50%; transform: translateX(-50%);
    width: max-content; max-width: calc(100vw - 56px);
  }
  .section-lede { color: var(--ink-soft); font-size: 19px; max-width: 56ch; margin-bottom: 8px; }
  .lede-aside { color: var(--ink-faint); font-size: 16px; max-width: 56ch; margin: 0 0 8px; }
  .section-lede.tight, .lede-aside.tight { margin-bottom: 40px; }
  /* Footnote asterisk → click/keyboard popover (the Wolfram|Alpha aside,
     tucked out of the way but still bound to the claim it qualifies).
     No Popover API → the [popover] div just renders inline: graceful
     fallback to a plain visible footnote. */
  .fn-star {
    font: inherit; background: none; border: 0; margin: 0;
    display: inline-block; padding: 1px 5px;
    color: var(--ink-faint); cursor: help;
    font-size: .78em; vertical-align: super; line-height: 1;
  }
  .fn-star:hover, .fn-star:focus-visible { color: var(--accent); }
  .fn-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
  .fn-pop:popover-open {
    position: fixed; inset: auto; margin: 0;
    max-width: 280px; padding: 9px 13px;
    font-size: 15px; color: var(--ink-soft);
    background: var(--paper-raised); border: 1px solid var(--rule);
    border-radius: 10px; box-shadow: var(--shadow);
  }

  /* ---------- Top-right controls ---------- */
  .controls {
    position: fixed; top: 18px; right: 18px; z-index: 50;
    display: flex; gap: 10px; align-items: flex-start;
  }
  .ctrl-btn {
    width: 38px; height: 38px; border-radius: 50%;
    border: 1px solid var(--rule); background: var(--paper-raised);
    color: var(--ink-soft); cursor: pointer; font-size: 16px;
    display: grid; place-items: center; box-shadow: var(--shadow);
    transition: transform .2s ease, color .2s ease, background-color .25s ease, border-color .25s ease;
    padding: 0;
  }
  .ctrl-btn:hover { transform: scale(1.06); color: var(--accent); }
  .lang-wrap, .plat-wrap { position: relative; }
  .lang-menu, .plat-menu {
    position: absolute; top: 46px; right: 0; min-width: 168px;
    background: var(--paper-raised); border: 1px solid var(--rule);
    border-radius: 12px; box-shadow: var(--shadow); padding: 6px;
    display: none; flex-direction: column;
  }
  .lang-menu.open, .plat-menu.open { display: flex; }
  .lang-menu button, .plat-menu button {
    background: none; border: 0; text-align: left; cursor: pointer;
    font: inherit; font-size: 15px; color: var(--ink-soft);
    padding: 8px 12px; border-radius: 7px; white-space: nowrap;
  }
  .lang-menu button:hover, .plat-menu button:hover { background: var(--rule-soft); color: var(--ink); }
  .lang-menu button[aria-current="true"], .plat-menu button[aria-current="true"] { color: var(--accent); font-weight: 600; }
  .lang-poc {
    font-size: 12px; color: var(--ink-faint); padding: 8px 12px 4px;
    border-top: 1px solid var(--rule-soft); margin-top: 4px;
  }
  /* Music control: a little player that pops out of the top bar. */
  .music-wrap { position: relative; }
  #musicBtn { font-size: 18px; line-height: 1; }
  .music-menu {
    position: absolute; top: 46px; right: 0;
    width: 280px; max-width: calc(100vw - 36px);
    background: var(--paper-raised); border: 1px solid var(--rule);
    border-radius: 14px; box-shadow: var(--shadow); padding: 8px;
    display: none;
  }
  .music-menu.open { display: block; }
  /* Audio-only: a small "now playing" card backed by a hidden YouTube player. */
  .music-card { display: flex; align-items: center; gap: 11px; padding: 3px 4px 3px 5px; }
  .music-art { width: 50px; height: 50px; border-radius: 8px; object-fit: cover; flex: 0 0 auto; background: var(--rule-soft); }
  .music-meta { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
  .music-title { font-weight: 600; color: var(--ink); font-size: 15px; }
  .music-artist { color: var(--ink-soft); font-size: 13px; }
  .music-play {
    flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%;
    border: 1px solid var(--rule); background: var(--paper); color: var(--ink-soft);
    cursor: pointer; display: grid; place-items: center;
    transition: color .2s ease, border-color .2s ease;
  }
  .music-play svg { display: block; }
  .music-play:hover { color: var(--accent); border-color: var(--accent-soft); }
  /* Loading: the YouTube player buffers for a beat after you hit play — spin
     the ring until the track actually starts so the press doesn't feel dead. */
  .music-play.is-loading { cursor: default; }
  .music-spinner { animation: music-spin .7s linear infinite; transform-origin: center; }
  @keyframes music-spin { to { transform: rotate(360deg); } }
  #musicYT { position: absolute; left: -9999px; top: 0; width: 240px; height: 135px; }
  /* When the track is playing, the saucer's button fills with a soft accent
     blue — a quiet "now playing" tell that persists even with the popover closed. */
  #musicBtn.is-playing {
    background: color-mix(in srgb, var(--accent) 12%, var(--paper-raised));
    border-color: color-mix(in srgb, var(--accent) 22%, var(--rule));
  }
  @media (prefers-reduced-motion: reduce) { .music-spinner { animation-duration: 1.4s; } }

  /* ---------- Download icon ---------- */
  .dl-ico { vertical-align: -4px; margin-left: 8px; }

  /* ---------- Hero ---------- */
  .hero { padding: 132px 0 30px; text-align: center; }
  /* ---- Top ambient wash ----
     A faint, slowly drifting colour field shared by the hero + essence
     sections (wrapped together in .topwash). Pulls the app-icon palette
     (warm amber + cool blue, a hint of violet) at very low opacity —
     strongest across the hero, trailing down through the beats, then
     masked to nothing just before the testimonials so it never ends
     abruptly. Content rides above it; reduced-motion freezes the drift. */
  .topwash { position: relative; isolation: isolate; overflow: hidden; }
  .topwash > * { position: relative; z-index: 1; }
  .topwash::before {
    content: ""; position: absolute; z-index: 0; pointer-events: none;
    top: -6%; left: -10%; width: 120%; height: 112%;
    background:
      radial-gradient(50% 22% at 24% 9%,  rgba(253,193,3,.16),  transparent 72%),
      radial-gradient(52% 24% at 76% 13%, rgba(86,171,250,.14), transparent 72%),
      radial-gradient(60% 22% at 30% 42%, rgba(253,193,3,.09),  transparent 74%),
      radial-gradient(60% 24% at 72% 62%, rgba(86,171,250,.08), transparent 74%),
      radial-gradient(80% 26% at 50% 82%, rgba(165,104,205,.06), transparent 78%);
    filter: blur(45px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 76%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 0%, #000 76%, transparent 100%);
    /* Cache the gaussian blur on its own GPU layer. Without this, WebKit
       re-rasterizes the full-frame blur(45px) on ANY repaint that overlaps
       this region — the theme-fade, a reflow from elsewhere — which is the
       single biggest cause of Safari choppiness here (Chrome composites it
       cheaply, so it only shows on Safari). Promoting to a static layer makes
       the blur render ONCE; subsequent repaints just re-composite the cached
       texture. The content never changes, so the cache never invalidates. */
    transform: translateZ(0);
    backface-visibility: hidden;
  }
  /* The wash is static. It was previously animated (heroWash: a slow 30s
     scale+rotate drift), but animating a blur(45px) layer forces Safari to
     re-rasterize a full-frame gaussian blur every frame, forever — ~10%+ CPU
     at idle for a drift the eye can't catch. Not worth it. Gradient stays. */
  /* dark mode — soft warm bloom behind the hero (replaces the dimmed light wash).
     [data-theme="dark"] is unconditional; [data-theme="auto"] MUST be gated by
     prefers-color-scheme (below) or auto users in a LIGHT system get the dark
     bloom instead of the colourful light wash — i.e. the wash looks "missing". */
  :root[data-theme="dark"] .topwash::before {
    background:
      radial-gradient(37% 21% at 51% 16%, rgba(244,232,215,.114), transparent 80%);
    filter: blur(90px);
    opacity: 1;
  }
  /* dither overlay — breaks up 8-bit gradient banding (not blurred; GPU-cached, static) */
  :root[data-theme="dark"] .topwash::after {
    content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size: 200px 200px;
    opacity: .03;
    transform: translateZ(0); backface-visibility: hidden;
  }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .topwash::before {
      background:
        radial-gradient(37% 21% at 51% 16%, rgba(244,232,215,.114), transparent 80%);
      filter: blur(90px);
      opacity: 1;
    }
    :root[data-theme="auto"] .topwash::after {
      content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
      background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
      background-size: 200px 200px;
      opacity: .03;
      transform: translateZ(0); backface-visibility: hidden;
    }
  }
  .hero .app-icon {
    width: 120px; height: 120px; display: block; margin: 0 auto 30px;
    filter: drop-shadow(0 8px 20px rgba(0,0,0,.12));
    /* Safari renders CSS drop-shadow on SVG with rounded shapes by
       sampling at the SVG bounding box, which can leave faint vertical
       bands at the left/right edges. Forcing a compositing layer makes
       WebKit render the filter in an off-screen layer that doesn't
       suffer from those edge artifacts. */
    transform: translateZ(0);
    will-change: filter;
  }
  .hero h1 {
    font-size: clamp(34px, 6.6vw, 70px); font-weight: 500; letter-spacing: -.025em;
    margin-bottom: 22px;
    text-wrap: balance;   /* on narrow screens it breaks after "Notepad,"
                             into two balanced lines instead of overflowing */
  }
  .hero .sub b {
    font-weight: 600;
    color: var(--ink);
  }
  .hero .sub {
    font-size: clamp(18px, 2.4vw, 22px); color: var(--ink-soft); line-height: 1.7;
    max-width: 30em; margin: 0 auto 30px; text-wrap: balance;
  }
  .cta-primary {
    display: none; background: var(--ink); color: var(--paper);   /* direct download hidden — App Store / Setapp only at launch */
    font-weight: 600; font-size: 18px; padding: 16px 30px; border-radius: 11px;
  }
  .cta-primary:hover { text-decoration: none; }
  :root[data-theme="dark"] .cta-primary,
  :root[data-theme="auto"] .cta-primary { color: var(--paper); }
  .cta-appstore { display: inline-block; }
  .cta-appstore img {
    display: block; height: 64px; width: auto;
    /* The localized App Store badge SVG contains real <text>; recent Safari
       on Apple Silicon lets you select/Live-Text it (the "App Sto" highlight).
       It's a button, not copy — suppress selection + the long-press callout. */
    user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
    pointer-events: none;   /* the <a> takes the click; the img is just art */
  }
  .cta-appstore:hover { text-decoration: none; }
  .cta-meta { margin-top: 30px; font-size: 15px; color: var(--ink-faint); letter-spacing: .01em; }
  .cta-meta a { color: var(--ink-soft); border-bottom: 1px solid var(--rule); }
  .cta-meta a:hover { color: var(--accent); text-decoration: none; }
  .cta-alts { margin-top: 34px; font-size: 15px; color: var(--ink-soft); line-height: 2; }
  .cta-alts a { color: var(--ink-soft); border-bottom: 1px solid var(--rule); }
  .cta-alts a:hover { color: var(--accent); text-decoration: none; }
  .cta-alts .lbl { color: var(--ink-faint); font-size: 13.5px; }

  /* Essence stays hidden on first paint so its heading doesn't peek
     below the fold while the hero is still cascading in — the two
     animations were competing for attention. Reveals on first scroll
     (any amount), or after a 3.5s fallback in case the visitor never
     scrolls. Reduced-motion always shows it immediately. */
  #essence { opacity: 0; transition: opacity .6s ease; }
  body.scrolled-once #essence,
  body.essence-revealed #essence { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    #essence { opacity: 1; transition: none; }
  }

  /* ---------- Essence (sub-beats) ----------
     Four beats, each text + sheet side-by-side, alternating sides
     (text-LEFT on odd beats, text-RIGHT on even). On mobile (<760px)
     each beat stacks: text first, sheet below. Container is wider
     than the body --maxw to give the sheet enough room to breathe. */
  #essence .wrap { max-width: 940px; }
  .beat {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 64px;
    align-items: stretch;   /* sheet spans top-of-heading to bottom-of-
                               description, framing the text block. */
    margin: 0 0 141px;      /* ~10% more breathing room between beats */
  }
  /* The sheet expands to fill the grid cell's vertical extent; its
     content (3-4 rows) sits at the natural top, the bg + chrome +
     answer column run the full height. The base .sheet rule has
     margin: 26px 0 — kill it here so the sheet's TOP aligns with the
     text block's top, not 26px below. */
  #essence .beat-sheet { display: flex; }
  #essence .beat-sheet .sheet { height: 100%; margin: 0; }
  .beat:last-of-type { margin-bottom: 0; }
  .beat h3 { font-size: clamp(23px, 3.4vw, 31px); margin-bottom: 12px; text-wrap: pretty; }
  .beat p { color: var(--ink-soft); max-width: 60ch; text-wrap: pretty; }
  .beat p + p { margin-top: 14px; }
  .beat p em { color: var(--ink); font-style: italic; }
  .beat p b { font-weight: 600; color: var(--ink); }
  /* Alternate sides: text-LEFT on odd beats (default), text-RIGHT on
     even beats. Using `order` flips the visual position without
     touching DOM source order (so each beat's text precedes its
     sheet in the markup, which is the correct reading order for
     screen readers and the stacked mobile layout). */
  .beat:nth-child(even) .beat-text  { order: 2; }
  .beat:nth-child(even) .beat-sheet { order: 1; }
  .beat-sheet { margin: 0; }
  /* Text embraces the central gutter: on odd beats (text-LEFT /
     sheet-RIGHT) the text right-aligns so it sits next to the sheet
     on its right; on even beats (sheet-LEFT / text-RIGHT) the text
     left-aligns (default) so it sits next to the sheet on its left.
     Either way the text "leans into" the sheet rather than reading
     out toward the page edges. */
  .beat:nth-child(odd) .beat-text { text-align: right; }
  /* Sheet sizing inside an Essence beat: tighter answer column than
     the full-width screenshots elsewhere, since the column is fixed
     at ~420px here and a 160/200px ans-col would leave too little
     room for the expression. Applies to animated AND static sheets.
     width: 100% fills the grid cell so the sheet's width is fixed —
     otherwise the sheet content-sizes itself and the caret moving
     between rows of different lengths causes a subtle horizontal
     jump as the typing progresses. */
  #essence .sheet.app {
    --ans-col: 140px;
    font-size: 16px;
    width: 100%;
    /* Tighter, slightly darker shadow than the default --shadow's
       wide 28px diffuse. The essence sheets sit closer to a real
       Soulver window's drop shadow at this rendering size. */
    box-shadow: 0 1px 2px rgba(28,27,23,.12), 0 4px 8px rgba(28,27,23,.10), 0 14px 30px rgba(28,27,23,.09);
  }
  /* "Total <label>" line inside an essence sheet — bold value + a thin
     rule above it. Same Soulver-faithful styling the static screenshots
     use (the `:not(.anim)` rule below), restated here so it applies to
     the animated essence sheets too. */
  #essence .sheet.app .row.sub .ans {
    position: relative;
    font-weight: 700;
    padding-top: 4px;
  }
  #essence .sheet.app .row.sub .ans::before {
    content: ""; position: absolute; top: 1px;
    left: 10px; right: 14px;
    border-top: 1px solid var(--slv-sub-rule);
  }
  @media (max-width: 760px) {
    .beat {
      grid-template-columns: 1fr;
      row-gap: 24px;
      margin-bottom: 56px;
    }
    .beat:nth-child(even) .beat-text,
    .beat:nth-child(even) .beat-sheet { order: 0; }
    .beat:nth-child(odd) .beat-text { text-align: left; }
  }

  /* ---------- Soulver sheet rendering ---------- */
  .sheet {
    --ans-col: 200px;
    font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
    font-size: 14px; line-height: 1.62;
    border: 1px solid var(--sheet-edge);
    border-radius: 12px;
    box-shadow: var(--shadow);
    margin: 26px 0;
    overflow: hidden;
    /* continuous answer column: tinted band down the full right edge */
    background:
      linear-gradient(90deg,
        var(--sheet-bg) calc(100% - var(--ans-col)),
        var(--rule-soft) calc(100% - var(--ans-col)),
        var(--rule-soft) calc(100% - var(--ans-col) + 1px),
        var(--sheet-ans-bg) calc(100% - var(--ans-col) + 1px));
  }
  .sheet .row { display: grid; grid-template-columns: 1fr var(--ans-col); align-items: center; padding: 0 0 0 14px; }
  .sheet .row .expr { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); padding-right: 22px; }
  .sheet .row .ans {
    color: var(--accent-ink); font-weight: 500; text-align: left;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    padding: 0 12px 0 10px;  /* breathing room from the divider — too
                                close (6px) crowded the divider; too far
                                (22px) floated the answer mid-column.
                                14px matches real Soulver's spacing. */
  }
  .sheet .row .note { color: var(--ink-faint); font-style: italic; }
  .sheet .row .tag {
    color: var(--accent); background: var(--accent-soft);
    border-radius: 5px; padding: 1px 7px; font-size: 12.5px;
    margin-left: 4px; white-space: nowrap;
  }
  .sheet .pad { height: 11px; }
  .sheet .sheet-h1 { padding: 14px 20px 6px; color: var(--ink); font-weight: 500; }
  .sheet .sheet-h2 { padding: 12px 20px 4px; color: var(--ink-soft); font-weight: 500; }
  .sheet .comment { padding: 0 20px; color: var(--ink-faint); font-style: italic; }
  .sheet .divider { margin: 12px 20px; border-top: 1px dashed var(--rule); }
  .sheet-top, .sheet-bottom { height: 10px; }

  /* --- typed sheet (prototype: Essence anchor) — opt-in via data-anim+app.
     The left column is *typed* character-by-character with a live caret
     (pre-rendered then revealed, so zero layout shift); each answer
     resolves a beat after its line finishes; the total pops in last.
     Scroll-triggered, plays once. --- */
  .sheet.app .ch { visibility: hidden; }
  .sheet.app .ch.on { visibility: visible; }
  .sheet.app .caret {
    display: inline-block; width: 2px; height: 1.3em;
    vertical-align: -0.3em; margin: 0 -1px; border-radius: 1px;
    background: #2A7DFF;   /* Soulver's system-tint blue, matching the
                              Live-data card's caret colour and the real
                              app's editor caret. Negative margins cancel
                              the 2px width so the caret takes 0 layout
                              space — otherwise typing through a tight
                              line pushes it over its column width and
                              causes a mid-animation wrap (the row
                              expands while the caret is in it, then
                              collapses when the caret moves on). */
    animation: slvCaret 1.05s steps(1, end) infinite;
  }
  @keyframes slvCaret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
  /* Answers just appear (no slide/fade) — hidden until the sequence
     reveals them, then an instant flip, like the real app computing live. */
  .sheet.app .row .ans { opacity: 0; }
  .sheet.app .row .ans.on { opacity: 1; }
  .sheet.app .sheet-total {
    opacity: 0; transform: translateY(7px) scale(.96);
    transition: opacity .42s ease, transform .5s cubic-bezier(.34,1.45,.5,1);
  }
  .sheet.app .sheet-total.on { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .sheet.app .ch { visibility: visible; }
    .sheet.app .caret { display: none; }
    .sheet.app .row .ans, .sheet.app .sheet-total {
      opacity: 1 !important; transform: none !important; transition: none !important;
    }
  }
  /* Static app sheet — data-app WITHOUT data-anim: a still "screenshot".
     No typist ever runs for it (the IO only observes .sheet.anim.app), so
     reveal the text + answers up front, no caret, no reveal animation —
     exactly a real Soulver window sitting at rest. */
  .sheet.app:not(.anim) .ch { visibility: visible; }
  .sheet.app:not(.anim) .caret { display: none; }
  .sheet.app:not(.anim) .row .ans,
  .sheet.app:not(.anim) .sheet-total {
    opacity: 1; transform: none; transition: none;
  }
  /* These screenshots carry real, sometimes long, verified expressions
     (e.g. "monthly repayment on $650,000 for 30 years at 5.8%"). Shown a
     touch smaller with a narrower answer column — a real window sized to
     fit its content — so long lines don't soft-wrap and the demo stays
     tidy. The animated Essence anchor keeps its calibrated 20px. */
  .sheet.app:not(.anim) {
    font-size: 16px; line-height: 1.62; --ans-col: 160px;
    padding-bottom: 14px;
  }
  .sheet.app:not(.anim) .row { padding-top: 1px; padding-bottom: 1px; }
  .sheet.app:not(.anim) .row .expr { padding-right: 14px; white-space: normal; overflow-wrap: anywhere; }
  .sheet.app:not(.anim) .row .ans { white-space: nowrap; padding: 0 10px 0 10px; }
  /* Gallery screenshots sit lighter on the page than the calibrated
     Essence anchor — a softer, shallower lift, not full macOS depth. */
  .sheet.app:not(.anim) {
    box-shadow: 0 1px 2px rgba(28,27,23,.11), 0 4px 8px rgba(28,27,23,.09), 0 12px 26px rgba(28,27,23,.08);
  }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.app:not(.anim) {
      box-shadow: 0 1px 2px rgba(0,0,0,.42), 0 4px 9px rgba(0,0,0,.36), 0 14px 30px rgba(0,0,0,.34);
    }
  }
  :root[data-theme="dark"] .sheet.app:not(.anim) {
    box-shadow: 0 1px 2px rgba(0,0,0,.42), 0 4px 9px rgba(0,0,0,.36), 0 14px 30px rgba(0,0,0,.34);
  }

  /* --- reveal sheet (prototype: Notepad anchor) — opt-in via data-reveal+app.
     The sheet starts as a plain notepad (white sheet, plain ink, no answer
     column visible). The answer column panel — already fully formed —
     slides in from the right edge and locks against the notepad; tokens
     light up during the slide. Then a beat later the floating total
     springs up. Three beats, scroll-triggered, plays once. */
  .sheet.reveal.app {
    /* Line-height a notch tighter than the typed Essence anchor (1.82) —
       matches real Soulver's density. The static gallery cards in #engine
       use 1.62 for the same reason. */
    font-size: 20px; line-height: 1.7;
    --ans-col: 200px;
    padding-bottom: 80px;
    background: var(--sheet-bg);    /* notepad mode: uniform, no gradient */
    position: relative;
    overflow: hidden;               /* the off-screen column is clipped here */
  }
  /* The answer column panel — pre-formed, slides in from the right edge. */
  .sheet.reveal.app::after {
    content: '';
    position: absolute; top: 0; bottom: 0; right: 0;
    width: var(--ans-col);
    box-sizing: border-box;
    background: var(--sheet-ans-bg);
    border-left: 1px solid var(--rule-soft);
    border-radius: 0 16px 16px 0;
    transform: translateX(100%);
    transition: transform .58s cubic-bezier(.6, 0, .15, 1);
    z-index: 0;
  }
  .sheet.reveal.app.opened::after { transform: translateX(0); }
  /* Answer values ride in with the panel — identical motion, same easing. */
  .sheet.reveal.app .row .ans {
    transform: translateX(var(--ans-col));
    transition: transform .58s cubic-bezier(.6, 0, .15, 1);
    opacity: 1;
    position: relative; z-index: 1;
    white-space: nowrap; padding: 0 12px 0 10px;
  }
  .sheet.reveal.app.opened .row .ans { transform: translateX(0); }
  .sheet.reveal.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.reveal.app .row .expr { padding-right: 22px; white-space: normal; }
  /* Header had 12px padding-bottom that put it visually further from row 1
     than rows are from each other. Zero it so all gaps read evenly. */
  .sheet.reveal.app .sheet-h1 { padding-bottom: 0; }
  /* No typing in this mode — chars are visible from the start, no caret.
     Syntax tokens inherit their default Soulver colors from .sheet.app, so
     the notepad already reads as Soulver from the first frame. */
  .sheet.reveal.app .ch { visibility: visible; }
  .sheet.reveal.app .caret { display: none; }
  /* Floating total stays hidden until .totaled — a separate beat after lock.
     Just a clean fade-in; no translateY/scale (the panel-slide is the
     theatrical moment, the total just appears). */
  .sheet.reveal.app .sheet-total {
    opacity: 0;
    transition: opacity .42s ease;
    bottom: 28px;     /* sits a touch higher than the default 14px */
    z-index: 1;
  }
  .sheet.reveal.app.totaled .sheet-total { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    .sheet.reveal.app::after { transform: translateX(0); transition: none; }
    .sheet.reveal.app .row .ans { transform: translateX(0); transition: none; }
    .sheet.reveal.app .sheet-total  { opacity: 1; transform: none; transition: none; }
  }

  /* --- subanim sheet (prototype: Subtotals card) — opt-in via data-subanim+app.
     A pointer cursor enters the sheet, glides to the blank line beneath a
     group of three expression lines, double-clicks, and a subtotal lands.
     Scroll-triggered, plays once. */
  .sheet.subanim.app {
    font-size: 20px; line-height: 1.7;
    --ans-col: 200px;
    padding-bottom: 18px;
    position: relative;
    overflow: hidden;
  }
  .sheet.subanim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.subanim.app .row .expr { padding-right: 22px; white-space: normal; }
  .sheet.subanim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; opacity: 1; }
  .sheet.subanim.app .sheet-h1 { padding-bottom: 0; }
  .sheet.subanim.app .ch { visibility: visible; }
  .sheet.subanim.app .caret { display: none; }
  /* Subtotal row hidden initially; just appears (no motion) on .dropped —
     matches the real app, where double-clicking the blank line snaps the
     value in without any transition. */
  .sheet.subanim.app .row.sub { opacity: 0; }
  .sheet.subanim.app.dropped .row.sub { opacity: 1; }
  /* iOS-style tap indicator — a translucent dark circle that glides
     horizontally from below the text to the subtotal row's answer position,
     then pulses once to signal a tap. (Centred on its (left, top) anchor
     via translate(-50%, -50%) so positions read as the circle's centre.) */
  .sheet.subanim.app .sheet-cursor {
    position: absolute;
    width: 38px; height: 38px;
    border-radius: 50%;
    background: rgba(20, 20, 30, 0.22);
    pointer-events: none;
    z-index: 5;
    left: 90px; top: 191px;          /* start — below text, near left edge */
    transform: translate(-50%, -50%) scale(1);
    opacity: 0;
    transition: left .65s cubic-bezier(.4, 0, .2, 1),
                opacity .35s ease;
  }
  .sheet.subanim.app.cursor-shown  .sheet-cursor { opacity: 1; }
  /* Target: centred on the subtotal value's centre point. */
  .sheet.subanim.app.cursor-target .sheet-cursor { left: 428px; }
  .sheet.subanim.app.cursor-click  .sheet-cursor { animation: tapPulse .42s ease; }
  .sheet.subanim.app.cursor-fade   .sheet-cursor { opacity: 0; }
  @keyframes tapPulse {
    0%   { transform: translate(-50%, -50%) scale(1);  }
    45%  { transform: translate(-50%, -50%) scale(.62); }
    100% { transform: translate(-50%, -50%) scale(1);  }
  }
  @media (prefers-reduced-motion: reduce) {
    .sheet.subanim.app .row.sub { opacity: 1; transform: none; transition: none; }
    .sheet.subanim.app .sheet-cursor { display: none; }
  }

  /* --- refanim sheet (prototype: Line References card) — opt-in via
     data-refanim+app. The tap circle starts on row 3 (where typing will
     happen), moves to row 1's answer, taps — first chip lands. Caret
     types " to ". Tap circle moves to row 2's answer, taps — second
     chip lands. Caret types " as %", answer materialises. */
  .sheet.refanim.app {
    font-size: 20px; line-height: 1.7;
    --ans-col: 200px;
    padding-bottom: 24px;
    position: relative;
    overflow: hidden;
  }
  .sheet.refanim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.refanim.app .row .expr { padding-right: 22px; white-space: normal; }
  .sheet.refanim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; opacity: 1; }
  .sheet.refanim.app .sheet-h1 { padding-bottom: 0; }
  .sheet.refanim.app .ch { visibility: visible; }
  /* Caret IS used in refanim — connector chars (" to ", " as %") get
     typed in character-by-character with a blinking caret in tow. The
     earlier `:not(.anim) .caret { display: none }` rule (for static
     gallery sheets) needs explicit override here since .refanim is also
     :not(.anim). */
  .sheet.refanim.app .caret { display: inline-block; background: var(--slv-num); }
  /* The result row's expression is empty initially. Each beat reveals one
     piece in sequence: tap 1 → first chip, then " to ", tap 2 → second
     chip, then " as %" + answer.
     NB: `:nth-of-type` doesn't work for chips here — the connector .ch
     spans sit between the chips, so the count is off. The sibling
     combinator (`.tok-lineref ~ .tok-lineref`) selects "any chip that
     follows another chip" → i.e. all chips after the first. */
  .sheet.refanim.app .row:has(.tok-lineref) .tok-lineref { opacity: 0; transition: opacity .28s ease; }
  /* Connector chars start invisible. JS reveals them one at a time during
     the typing beats (with a blinking caret moving along the way) — so no
     class-based reveal, just the initial-hidden state. */
  .sheet.refanim.app .row:has(.tok-lineref) .ch-between,
  .sheet.refanim.app .row:has(.tok-lineref) .ch-after    { opacity: 0; }
  .sheet.refanim.app .row:has(.tok-lineref) .ans         { opacity: 0; transition: opacity .28s ease; }
  /* click1-done: first chip visible AND the answer column lights up with
     line1's value ($42,000) — what Soulver computes when the expression
     is just a single reference. */
  .sheet.refanim.app.click1-done .row:has(.tok-lineref) .tok-lineref { opacity: 1; }
  .sheet.refanim.app.click1-done .row:has(.tok-lineref) .tok-lineref ~ .tok-lineref { opacity: 0; }
  .sheet.refanim.app.click1-done .row:has(.tok-lineref) .ans { opacity: 1; }
  /* click2-done: both chips visible. The answer text JS-swaps to the diff
     ($6,300) — what "x to y" computes when no unit is yet appended. */
  .sheet.refanim.app.click2-done .row:has(.tok-lineref) .tok-lineref,
  .sheet.refanim.app.click2-done .row:has(.tok-lineref) .tok-lineref ~ .tok-lineref { opacity: 1; }
  /* After " as %" types in, JS swaps the answer text to the final
     percentage (15%). No animation on the swap — the text just changes. */
  /* Tap circle — same visual as subanim, but choreography uses two glide
     positions (row 1's answer, then row 2's). Initial position sits below
     the third row's left edge. */
  .sheet.refanim.app .sheet-cursor {
    position: absolute;
    width: 38px; height: 38px;
    border-radius: 50%;
    background: rgba(20, 20, 30, 0.22);
    pointer-events: none;
    z-index: 5;
    left: 430px; top: 64px;            /* start — above row 1's answer, in
                                          the answer column area */
    transform: translate(-50%, -50%) scale(1);
    opacity: 0;
    transition: left .55s cubic-bezier(.4, 0, .2, 1),
                top  .55s cubic-bezier(.4, 0, .2, 1),
                opacity .35s ease;
  }
  .sheet.refanim.app.cursor-shown   .sheet-cursor { opacity: 1; }
  /* Target 1 — descends to row 1's answer. */
  .sheet.refanim.app.cursor-target1 .sheet-cursor { left: 430px; top: 94px; }
  /* Target 2 — same x, row 2's answer y. The hovering circle stays in the
     answer column the whole time — it never returns to row 3 (that's the
     blue text caret's job). */
  .sheet.refanim.app.cursor-target2 .sheet-cursor { left: 430px; top: 128px; }
  .sheet.refanim.app.cursor-click   .sheet-cursor { animation: tapPulse .42s ease; }
  .sheet.refanim.app.cursor-fade    .sheet-cursor { opacity: 0; }
  @media (prefers-reduced-motion: reduce) {
    .sheet.refanim.app .row:has(.tok-lineref) .tok-lineref { opacity: 1; transition: none; }
    .sheet.refanim.app .row:has(.tok-lineref) .ans { opacity: 1; transition: none; }
    .sheet.refanim.app .row:has(.tok-lineref) .ch-between,
    .sheet.refanim.app .row:has(.tok-lineref) .ch-after { opacity: 1; transition: none; }
    .sheet.refanim.app .sheet-cursor { display: none; }
  }

  /* --- varanim sheet (prototype: Multi-word variables card) — opt-in via
     data-varanim+app. Sheet renders normally with initial values, then a
     cascade fires: the variable value swaps ($120 → $150), and each
     downstream answer follows in sequence. Each changing cell gets a
     brief blue background pulse so the eye tracks the value change. */
  .sheet.varanim.app {
    font-size: 20px; line-height: 1.7;
    --ans-col: 200px;
    padding-bottom: 24px;
    position: relative;
    overflow: hidden;
  }
  .sheet.varanim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.varanim.app .row .expr { padding-right: 22px; white-space: normal; }
  .sheet.varanim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; }
  .sheet.varanim.app .sheet-h1 { padding-bottom: 0; }
  .sheet.varanim.app .ch { visibility: visible; }
  .sheet.varanim.app .caret { display: none; }

  /* --- Static Tags sheet — opt-in via data-taganim+app. No animation;
     the sheet just renders at the Notepad-card sizing. The static layout
     is doing the work — readers can see the cross-line link by reading
     the tags and the totals together. */
  .sheet.taganim.app {
    font-size: 20px; line-height: 1.7;
    --ans-col: 200px;
    padding-bottom: 24px;
    position: relative;
    overflow: hidden;
  }
  .sheet.taganim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.taganim.app .row .expr { padding-right: 22px; white-space: normal; }
  .sheet.taganim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; }
  .sheet.taganim.app .sheet-h1 { padding-bottom: 0; }
  .sheet.taganim.app .ch { visibility: visible; }
  .sheet.taganim.app .caret { display: none; }
  /* Blank-line separator: full row-height, so the gap above the totals
     reads like an empty row instead of a cramped pad. */
  .sheet.taganim.app .pad { height: 34px; }

  /* --- Trip-planning sheet — opt-in via data-tpanim+app. The animation
     simulates the right-click → "Make Time Point" interaction: a cursor
     taps the row, a Soulver-style popover appears, the green pill
     highlights, the menu dismisses and the ⬇️/⬆️ marker materialises in
     the answer column. Two markers per cycle (start of trip, end of trip). */
  .sheet.tpanim.app {
    font-size: 20px; line-height: 1.7;
    --ans-col: 290px;       /* date ranges + "(N nights)" need a wider column */
    padding-bottom: 24px;
    position: relative;
    overflow: hidden;
  }
  .sheet.tpanim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.tpanim.app .row .expr { padding-right: 22px; white-space: normal; }
  .sheet.tpanim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; transition: opacity .2s ease; }
  .sheet.tpanim.app .sheet-h1 { padding-bottom: 0; }
  .sheet.tpanim.app .ch { visibility: visible; }
  .sheet.tpanim.app .caret { display: none; }

  /* Dates render as plain text on the trip-planning card (no underline) —
     the .tok-date class is still emitted by the tokenizer but unstyled. */

  /* Tap circle — same iOS-style translucent dot used elsewhere, with
     positions specific to the trip-planning card's two rows. */
  .sheet.tpanim.app .sheet-cursor {
    position: absolute;
    width: 38px; height: 38px;
    border-radius: 50%;
    background: rgba(20, 20, 30, 0.22);
    pointer-events: none;
    z-index: 12;                      /* above the contextual menu (z-index:8) so it reads as the cursor that opened it */
    left: 350px; top: 94px;          /* default — just past the start of row 1's answer (where the ⬇️ marker will appear) */
    transform: translate(-50%, -50%) scale(1);
    opacity: 0;
    transition: left .55s cubic-bezier(.4, 0, .2, 1),
                top  .55s cubic-bezier(.4, 0, .2, 1),
                opacity .35s ease;
  }
  .sheet.tpanim.app.tp-cur-shown .sheet-cursor { opacity: 1; }
  .sheet.tpanim.app.tp-cur-start .sheet-cursor { left: 350px; top: 94px; }
  .sheet.tpanim.app.tp-cur-end   .sheet-cursor { left: 350px; top: 230px; }
  /* Cursor positions for when it moves UP from a row to tap the
     "Make time point" pill inside the open contextual menu. Menu
     occupies y≈48–88 for menu1 (above row 1) and y≈184–224 for
     menu2 (above row 5); pill centre is ~20px down from menu top. */
  .sheet.tpanim.app.tp-cur-pill1 .sheet-cursor { left: 400px; top: 59px;  }
  .sheet.tpanim.app.tp-cur-pill2 .sheet-cursor { left: 400px; top: 195px; }
  .sheet.tpanim.app.tp-cur-click .sheet-cursor { animation: tapPulse .42s ease; }
  .sheet.tpanim.app.tp-cur-fade  .sheet-cursor { opacity: 0; }

  /* Context-menu popover — appears anchored to the tapped row, styled
     like Soulver's native menu (rounded card, soft shadow, green pill +
     plain item beneath). */
  .sheet.tpanim.app .tp-menu {
    position: absolute;
    background: var(--paper-raised);
    border-radius: 13px;
    padding: 6px;
    /* Size to the pill's content (was a fixed 160px sized for English) so
       longer translations — "Создать временную точку", "Zeitpunkt erstellen"
       — don't overflow the pill. min-width keeps short labels from shrinking. */
    width: max-content;
    min-width: 160px;
    box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 10px 28px rgba(0,0,0,.18);
    z-index: 8;
    opacity: 0;
    transform: scale(.92);
    transform-origin: top left;
    transition: opacity .18s ease, transform .18s cubic-bezier(.34, 1.45, .5, 1);
    pointer-events: none;
    /* Default position — JS swaps tp-menu-start / tp-menu-end to anchor
       to the right row's vertical position. */
  }
  .sheet.tpanim.app.tp-menu-open .tp-menu {
    opacity: 1;
    transform: scale(1);
  }
  /* Menu anchors: the cursor sits at the left of the answer column
     (where the ⬇️/⬆️ marker will materialise), and the menu blooms
     UP-RIGHT from there. With transform-origin: bottom left, the
     bottom-left corner of the menu is the anchor — placed just to
     the left of the cursor so the menu visually "grows out of" the
     click point. */
  .sheet.tpanim.app.tp-menu-start .tp-menu {
    left: 320px; top: auto; bottom: 204px;  /* sheet H 292 − row 1 top 88 = 204 */
    transform-origin: bottom left;
  }
  .sheet.tpanim.app.tp-menu-end   .tp-menu {
    left: 320px; top: auto; bottom: 68px;   /* sheet H 292 − row 5 top 224 = 68 */
    transform-origin: bottom left;
  }
  .sheet.tpanim.app .tp-menu-pill {
    background: #5DC75F;
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    padding: 6px 16px 6px 12px;       /* extra right buffer so "Point" isn't hugging the pill edge */
    border-radius: 7px;
    display: flex;
    align-items: center;
    gap: 7px;
    transition: background .15s ease;
    white-space: nowrap;
  }
  .sheet.tpanim.app.tp-menu-hot .tp-menu-pill {
    background: #4DB54F;
    box-shadow: 0 0 0 2px rgba(93, 199, 95, 0.25);
  }
  .sheet.tpanim.app .tp-pill-icon {
    width: 16px; height: 16px;
    flex: 0 0 16px;
    color: #fff;
  }
  .sheet.tpanim.app .tp-menu-item {
    color: var(--ink);
    font-size: 14px;
    padding: 7px 14px;
    margin-top: 2px;
  }
  /* Dark-mode tweaks for the menu so it still reads as a "lifted card"
     against the dark sheet bg. */
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.tpanim.app .tp-menu {
      box-shadow: 0 1px 3px rgba(0,0,0,.5), 0 14px 32px rgba(0,0,0,.55);
    }
  }
  :root[data-theme="dark"] .sheet.tpanim.app .tp-menu {
    box-shadow: 0 1px 3px rgba(0,0,0,.5), 0 14px 32px rgba(0,0,0,.55);
  }

  /* --- Live-data sheet — opt-in via data-ldanim+app. The animation
     mimics Soulver's actual UX for "= ?" queries: type the natural-
     language question (rendered in the variable/reference green
     because Soulver treats it as a lookup token), the iOS-style
     activity spinner spins in the answer column while the lookup
     "resolves", and the resolved value snaps in. Then move on to the
     next row. Loops. */
  .sheet.ldanim.app {
    font-size: 18px; line-height: 1.85;  /* slightly smaller than 20px so the longest queries fit on one line */
    --ans-col: 230px;
    padding-bottom: 24px;
    position: relative;
  }
  .sheet.ldanim.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.ldanim.app .row .expr {
    padding-right: 22px;
    white-space: nowrap;
    min-width: 0;            /* prevent the 1fr grid track from growing to fit the longest query — keeps the sheet's overall width identical to every other card. Overflow stays inside the column. */
    /* base expr is plain text — green only applies to recognised
       natural-language entities (tok-var) and to known stock tickers. */
  }
  /* Soulver-faithful token colours for the resolved-inline values
     and the per-row tokenised expressions. */
  .sheet.ldanim.app .row .expr .tok-num   { color: var(--slv-num); }
  .sheet.ldanim.app .row .expr .tok-unit  { color: var(--slv-unit); }
  .sheet.ldanim.app .row .expr .tok-var   { color: var(--slv-var); }
  /* Date entities — plain text (no underline; the dashed border
     leaks through the opacity:0 .ch spans during typing, so any
     decoration on the parent tok-* span is a no-go for animated
     rows). */
  .sheet.ldanim.app .row .expr .tok-date { /* default text colour */ }
  /* Place entities — Soulver-blue, no decoration (same reason). */
  .sheet.ldanim.app .row .expr .tok-place { color: var(--slv-num); }
  .sheet.ldanim.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; }
  /* opacity (not visibility) on .ch — parent tok-* span decorations
     would otherwise leak through visibility:hidden children. No
     transition: the previous 40ms fade was catchable by the eye as
     a brief semi-transparent ghost of each character before it
     snapped to fully visible — every other animated card just
     toggles binary, so this one matches. */
  .sheet.ldanim.app .ch { opacity: 0; visibility: visible; }
  .sheet.ldanim.app .ch.on { opacity: 1; }
  /* When the "?" placeholder is resolved inline, the original .ch is
     collapsed to zero width so the resolved value sits flush against
     the "= " before it (otherwise an invisible "?" leaves a gap). */
  .sheet.ldanim.app .ch.ld-was-q { display: none; }

  /* Persistent blue text caret hopping along the typing row. */
  .sheet.ldanim.app .caret {
    display: inline-block;
    width: 2px;
    height: 1.05em;
    background: #2A7DFF;
    vertical-align: text-bottom;
    margin: 0 -1px 0 0;
    animation: ldCaretBlink 1s steps(2, end) infinite;
  }
  @keyframes ldCaretBlink { 50% { opacity: 0; } }
  .sheet.ldanim.app.ld-typing-done .caret { display: none; }

  /* iOS-style activity indicator — 8 spokes around a 16px circle,
     each at decreasing opacity, rotated in steps for a "chasing
     light" effect. Lives inline in the answer column while the
     query is resolving. */
  .sheet.ldanim.app .ld-spinner {
    display: inline-block;
    position: relative;
    width: 16px;
    height: 16px;
    vertical-align: -3px;
    animation: ldSpin 0.8s steps(8) infinite;
  }
  .sheet.ldanim.app .ld-spinner span {
    position: absolute;
    left: 7px;
    top: 1px;
    width: 2px;
    height: 4px;
    border-radius: 1px;
    background: var(--slv-text, #1f1f24);
    transform-origin: 1px 7px;
  }
  .sheet.ldanim.app .ld-spinner span:nth-child(1) { transform: rotate(  0deg); opacity: 1.00; }
  .sheet.ldanim.app .ld-spinner span:nth-child(2) { transform: rotate( 45deg); opacity: 0.84; }
  .sheet.ldanim.app .ld-spinner span:nth-child(3) { transform: rotate( 90deg); opacity: 0.72; }
  .sheet.ldanim.app .ld-spinner span:nth-child(4) { transform: rotate(135deg); opacity: 0.58; }
  .sheet.ldanim.app .ld-spinner span:nth-child(5) { transform: rotate(180deg); opacity: 0.44; }
  .sheet.ldanim.app .ld-spinner span:nth-child(6) { transform: rotate(225deg); opacity: 0.30; }
  .sheet.ldanim.app .ld-spinner span:nth-child(7) { transform: rotate(270deg); opacity: 0.18; }
  .sheet.ldanim.app .ld-spinner span:nth-child(8) { transform: rotate(315deg); opacity: 0.10; }
  @keyframes ldSpin { to { transform: rotate(360deg); } }

  /* Answer column states. Each row carries one of:
     .ld-pending  → answer column blank
     .ld-loading  → spinner spinning
     .ld-resolved → final value shown
     The .ans element contains two child spans (.ld-spinner-slot and
     .ld-text) that JS toggles between via the row's state class. */
  .sheet.ldanim.app .row .ans { opacity: 1; }
  .sheet.ldanim.app .ld-spinner-slot,
  .sheet.ldanim.app .ld-text { display: none; }
  .sheet.ldanim.app .row.ld-loading .ld-spinner-slot { display: inline-block; }
  .sheet.ldanim.app .row.ld-resolved .ld-text {
    display: inline-block;
    animation: ldFadeIn .25s ease;
  }
  @keyframes ldFadeIn {
    from { opacity: 0; transform: translateY(-2px); }
    to   { opacity: 1; transform: none; }
  }

  /* --- Structure sheet — opt-in via data-structure+app. Pure static
     render (no animation) that showcases Soulver's document features:
     H1 titles, H2 sub-headings, horizontal dividers, line highlights,
     inline comments, and empty-expr "total" rows. */
  .sheet.structure.app {
    font-size: 17px; line-height: 1.7;
    --ans-col: 150px;   /* values are short ($XXX) — tighter column gives the expression more room for inline comments and the highlighted summary text */
    padding-bottom: 18px;
  }
  .sheet.structure.app .row { padding-top: 0; padding-bottom: 0; }
  .sheet.structure.app .row .expr { padding-right: 22px; white-space: nowrap; min-width: 0; }
  .sheet.structure.app .row .ans  { white-space: nowrap; padding: 0 12px 0 10px; }
  /* Document title — Soulver's H1 (heavy, accent-green hash prefix). */
  .sheet.structure.app .sheet-h1 {
    font-size: 21px; font-weight: 700;
    padding: 14px 30px 10px;
  }
  /* Labels — a line ending with ":" (e.g. "Plane:", "Train:"). Bold,
     left-aligned within the expression column. Soulver itself uses
     this as its only sub-section grouping affordance; there are no
     "sub-headings" as such. */
  .sheet.structure.app .sheet-label {
    font-weight: 700;
    padding: 12px 18px 2px;
    color: var(--slv-text);
  }
  /* Highlighted label — yellow background hugs the text only (not
     the full row width), so it reads as "this label is the row's
     selected element". */
  .sheet.structure.app .sheet-label.is-hl span {
    background: var(--highlight);
    border-radius: 4px;
    padding: 1px 6px;
    margin-left: -6px;
  }
  /* Subtotal row — empty expr + value in answer column. Mirrors the
     Subtotals card exactly: a thin hairline inset from BOTH edges
     of the answer column (so it floats inside the column, not
     touching the bg edges) and the value below it bold. The
     `.row.sub` class is the canonical Soulver-faithful marker —
     same one the Subtotals card uses for its drop-in animation. */
  .sheet.structure.app .row.sub .ans {
    position: relative;
    font-weight: 700;
    padding-top: 4px;
  }
  .sheet.structure.app .row.sub .ans::before {
    content: "";
    position: absolute;
    top: 1px;
    left: 10px;
    right: 14px;
    border-top: 1px solid var(--slv-sub-rule);
  }
  /* Standalone comments — a whole "//" line. Muted text, plain (not
     italic — Soulver renders these upright). */
  .sheet.structure.app .comment {
    padding: 4px 30px;
    color: var(--slv-hash);
    font-style: normal;
    font-size: 15px;
  }
  .sheet.structure.app .comment .c-pre { margin-right: 2px; }
  /* Inline comment on a row — appears AFTER the expression as a
     muted aside (e.g. "Airport taxi $45 // ~50 min drive"). */
  .sheet.structure.app .inline-comment {
    color: var(--slv-hash);
    font-size: 0.88em;
    margin-left: 10px;
  }
  /* Inline label — a "Word(s):" prefix before the expression that
     reads as a tag/category, not a computed value. Muted like the
     standalone .sheet-label, slightly tighter than the comment. */
  .sheet.structure.app .inline-label {
    color: var(--ink-soft);
    font-weight: 500;
  }
  /* FAQ-embedded sheet variant — the in-FAQ "how do I stop Soulver
     from evaluating" example renders smaller and reads as a syntax
     reference, not a hero sheet. Heading drops to body size, inline
     labels render bold-black so they read as a kind of strong text. */
  .faq .ans .sheet.structure.app .sheet-h1 {
    font-size: inherit;
    font-weight: 700;
    padding: 0 30px;
  }
  .faq .ans .sheet.structure.app .inline-label {
    color: var(--slv-text);
    font-weight: 700;
  }
  /* Highlighted comment — bg hugs only the text (wrapped in a
     .hl-bg span by the renderer) so the highlight reads as "this
     aside is the takeaway", not "this whole band is highlighted". */
  .sheet.structure.app .comment.is-hl .hl-bg {
    background: var(--highlight);
    border-radius: 4px;
    padding: 2px 8px;
  }
  .sheet.structure.app .comment.is-hl-blue   .hl-bg { background: #DDEFFE; }
  .sheet.structure.app .comment.is-hl-orange .hl-bg { background: #FBE8D6; }
  .sheet.structure.app .comment.is-hl-green  .hl-bg { background: #DBF2DB; }
  .sheet.structure.app .comment.is-hl-pink   .hl-bg { background: #FAE5ED; }
  .sheet.structure.app .comment.is-hl-purple .hl-bg { background: #F2E6FA; }
  /* Horizontal divider — a soft hairline that stops at the answer
     column boundary AND is inset from the right by the same amount
     the subtotal rule is, so the two horizontal lines on the card
     read as belonging to the same visual system. */
  .sheet.structure.app .divider {
    margin: 10px 0 10px 30px;
    margin-right: calc(var(--ans-col) + 14px);
    border-top: 1px solid var(--rule);
  }
  /* Line highlights — Soulver supports multiple pastel tints; the
     source picks one with "← highlighted blue", "← highlighted
     orange", etc. (no colour name → default yellow). The bg hugs
     only the expression text (not the row), and never extends into
     the answer column. Base rule sets layout; colour variants ride
     on top to override just the background. */
  /* Highlight bg lives on the inline .hl-wrap span (not the .expr
     grid cell) so it hugs just the text width — reads as "this
     phrase is the takeaway", not "this whole band is highlighted".
     Matches the .hl-bg treatment used for comment highlights. */
  .sheet.structure.app .row.is-hl .hl-wrap {
    background: var(--highlight);
    border-radius: 4px;
    padding: 2px 8px;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
  }
  .sheet.structure.app .row.is-hl-blue   .hl-wrap { background: #DDEFFE; }
  .sheet.structure.app .row.is-hl-orange .hl-wrap { background: #FBE8D6; }
  .sheet.structure.app .row.is-hl-green  .hl-wrap { background: #DBF2DB; }
  .sheet.structure.app .row.is-hl-pink   .hl-wrap { background: #FAE5ED; }
  .sheet.structure.app .row.is-hl-purple .hl-wrap { background: #F2E6FA; }
  /* Highlighted text reads as Soulver does it — uniform dark colour
     (no syntax tokens leaking through the highlight) and slightly
     bolder so the row carries weight against the pastel bg. Applies
     to row highlights AND comment highlights. */
  .sheet.structure.app .row.is-hl .hl-wrap,
  .sheet.structure.app .row.is-hl .hl-wrap *,
  .sheet.structure.app .comment.is-hl .hl-bg,
  .sheet.structure.app .comment.is-hl .hl-bg * {
    color: #000;
  }
  .sheet.structure.app .comment.is-hl .hl-bg {
    font-weight: 600;
  }

  /* Helper class used during animation loops — applied to the sheet for a
     single tick when resetting state between cycles, so the cursor / chip
     reveals / clip-path / etc. SNAP back to initial state instead of
     animating backwards (which would look like a reverse-replay). */
  .sheet.no-trans,
  .sheet.no-trans::before,
  .sheet.no-trans::after,
  .sheet.no-trans *,
  .sheet.no-trans *::before,
  .sheet.no-trans *::after {
    transition: none !important;
    animation: none !important;
  }

  /* --- real Soulver-app chrome (prototype: Essence anchor) — opt-in via
     data-app. Window frame + traffic lights, system font, neutral answer
     column, syntax tokens, floating running total. Scoped so the other
     30+ sheets keep the existing rendering until this is sifted. --- */
  .sheet.app {
    --sheet-ans-bg: #F5F5F7;
    /* Soulver standardLight syntax colors (SyntaxColors.swift) */
    --slv-text: #383838; --slv-num: #0088F8; --slv-unit: #D151C7;
    --slv-var: #00A303; --slv-hash: #929393; --tot-bg: #FFFFFF;
    --slv-sub-rule: rgba(60,60,67,.16);
    --ans-col: 200px;
    font-family: -apple-system, "SF Pro Text", "SF Pro", system-ui, "Segoe UI", Roboto, sans-serif;
    /* The page forces -webkit-font-smoothing:antialiased (line ~75), which
       renders web text thinner than native macOS. Real Soulver is regular
       weight but reads heavier on-device; nudge up to close that gap so the
       pseudo-screenshot matches the actual app. */
    font-weight: 480;
    font-size: 20px; line-height: 1.82;
    border-radius: 16px;
    /* macOS-window-ish depth: stronger than a flat card shadow, but
       deliberately short of real macOS (which is heavier still). */
    box-shadow: 0 1px 3px rgba(28,27,23,.08), 0 10px 28px rgba(28,27,23,.13), 0 30px 64px rgba(28,27,23,.19);
    position: relative; padding-bottom: 80px;
    /* The looping demo sheets were the dominant idle-CPU cost: when the Tools
       panel is in view, ~8 sheets animate at once and each one's text-reveal
       transition forced a full-document relayout every frame (~36 layouts/s,
       ~8% CPU). `contain: layout` fixes that at the source — it makes each
       sheet a layout-containment boundary, so an animating sheet reflows only
       itself, never the whole document.
       We deliberately do NOT use `content-visibility: auto` here (it was the
       prior approach): in WebKit its per-frame "relevance" checks stutter the
       theme-fade transition, and its `contain-intrinsic-size` placeholder
       flashed a tall block in the stretch-grid panels on first scroll. Plain
       layout containment keeps the CPU win with none of that — no size
       containment (so no flash / honest sizing), no paint containment (so
       drop-shadows aren't clipped). Off-screen idle CPU is handled separately
       by `.anim-paused` (pauses animations out of view). */
    contain: layout;
  }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.app {
      --sheet-ans-bg: #232329;
      --slv-text: #DDDDDD; --slv-num: #9EF1FF; --slv-unit: #F0A1FF;
      --slv-var: #6BE08A; --slv-hash: #8F8F8F; --tot-bg: #26262C;
      --slv-sub-rule: rgba(255,255,255,.15);
      box-shadow: 0 1px 3px rgba(0,0,0,.4), 0 12px 30px rgba(0,0,0,.45), 0 30px 64px rgba(0,0,0,.55);
    }
  }
  :root[data-theme="dark"] .sheet.app {
    --sheet-ans-bg: #232329;
    --slv-text: #DDDDDD; --slv-num: #9EF1FF; --slv-unit: #F0A1FF;
    --slv-var: #6BE08A; --slv-hash: #8F8F8F; --tot-bg: #26262C;
    --slv-sub-rule: rgba(255,255,255,.15);
    box-shadow: 0 1px 3px rgba(0,0,0,.4), 0 12px 30px rgba(0,0,0,.45), 0 30px 64px rgba(0,0,0,.55);
  }
  .sheet.app .sheet-chrome { position: relative; height: 36px; }
  .sheet.app .sheet-chrome .tl {
    position: absolute; left: 10px; top: 9px;
    display: flex; align-items: center; gap: 8px;
  }
  .sheet.app .sheet-chrome .tl span { width: 12px; height: 12px; border-radius: 50%; display: block; }
  .sheet.app .sheet-chrome .tl .r { background: #FF5F57; }
  .sheet.app .sheet-chrome .tl .y { background: #FEBC2E; }
  .sheet.app .sheet-chrome .tl .g { background: #28C840; }
  .sheet.app .row { padding-left: 18px; }
  .sheet.app .row .expr { color: var(--slv-text); padding-right: 22px; }
  .sheet.app .row .ans {
    color: var(--slv-text); font-weight: 500;
    text-align: left; padding: 0 12px 0 10px;
  }
  .sheet.app .tok-num { color: var(--slv-num); }
  .sheet.app .tok-unit { color: var(--slv-unit); }
  .sheet.app .tok-var { color: var(--slv-var); }
  /* Line-reference chip — a small dark-ink pill on a soft-blue tint.
     Soulver's native `lightThemeBlue` is #D6E1F6 (0.841/0.883/0.965),
     but we lift it ~30% lighter for the web — at this rendering scale a
     touch more luminance reads cleaner and less heavy. */
  .sheet.app .tok-lineref {
    display: inline-block;
    background: #E4ECF8;
    color: #1f1f24;                /* fixed dark text — stays readable on the
                                      light chip bg in dark mode too. */
    padding: 0 4px;                /* tighter than real Soulver suggested —
                                      9px left too much air around the value. */
    margin: 0 1px;
    border-radius: 4px;
    font-weight: 500;
    line-height: 1.28;
  }
  /* Animated sheets: line-reference chips don't type — they insert.
     The chip hides itself (transparent bg, scaled down, invisible
     inner chars) until the typist's chip-insertion logic adds the
     .chip-inserted class, at which point the whole chip pops in as
     a unit. Padding stays so the row's layout doesn't shift when
     the chip "arrives". */
  .sheet.app.anim .tok-lineref {
    background: transparent;
    transform: scale(0.7);
    opacity: 0;
    transition:
      background 0s,
      opacity .18s ease,
      transform .22s cubic-bezier(.34, 1.56, .64, 1);
  }
  .sheet.app.anim .tok-lineref.chip-inserted {
    background: #E4ECF8;
    transform: scale(1);
    opacity: 1;
  }
  /* Hashtag chip — used both for trailing `#tag` metadata (peeled off the
     end of a line) and inline references (e.g. `#alex total`). Soft
     tint with dark text — same pattern as the line-ref chip. Each
     unique tag gets a distinct background color (assigned by the
     renderer based on order of first appearance) so the eye can spot
     matching tags across lines at a glance. */
  .sheet.app .tok-tag {
    display: inline-block;
    color: #1f1f24;                       /* fixed dark text (chip bgs are
                                             light pastels, so dark text reads
                                             cleanly in both light and dark modes) */
    background: #DBF2DB;                  /* fallback / no-color-class */
    border-radius: 5px;
    padding: 1px 8px;
    margin: 0 1px;
    font-size: 0.88em;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
  }
  /* Pastel chip bg per unique tag — opaque hex so chips stay light in
     dark mode (rgba would blend with the dark sheet bg → dark chip). */
  .sheet.app .tok-tag.tag-c0 { background: #DBF2DB; }    /* green   */
  .sheet.app .tok-tag.tag-c1 { background: #FBE8D6; }    /* orange  */
  .sheet.app .tok-tag.tag-c2 { background: #F2E6FA; }    /* purple  */
  .sheet.app .tok-tag.tag-c3 { background: #DDEFFE; }    /* blue    */
  .sheet.app .tok-tag.tag-c4 { background: #FAE5ED; }    /* pink    */
  .sheet.app .sheet-h1 { font-weight: 700; color: var(--slv-text); padding: 6px 18px 12px; }
  .sheet.app .sheet-h1 .hh { color: var(--slv-hash); font-weight: 600; margin-right: 6px; }
  .sheet.app .pad { height: 7px; }
  /* Total floats centred within the answer column: equal inset from the
     divider and the right edge, content left-aligned. line-height MUST be
     reset — the .sheet.app 1.82 is inherited and was making it too tall. */
  .sheet.app .sheet-total {
    position: absolute;
    left: calc(100% - var(--ans-col) + 14px); right: 14px; bottom: 14px;
    display: flex; align-items: baseline; gap: 7px;
    line-height: 1.15;
    background: var(--tot-bg);
    border-radius: 5px; padding: 9px 14px;
    box-shadow: 0 1px 2px rgba(28,27,23,.11), 0 3px 8px rgba(28,27,23,.08);
  }
  .sheet.app .sheet-total .t-label { color: var(--ink-faint); font-size: 14px; }
  .sheet.app .sheet-total .t-val { color: var(--slv-text); font-weight: 600; font-size: 17px; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.app .sheet-total {
      /* Dark-mode Total card uses an explicit hairline rather than a
         drop shadow — shadows blur into the dark sheet bg and don't
         actually define the card's edge. A 1px stroke makes the card
         clearly read as a discrete element. */
      box-shadow: none;
      border: 1px solid rgba(255, 255, 255, 0.14);
    }
  }
  :root[data-theme="dark"] .sheet.app .sheet-total {
    box-shadow: none;
    border: 1px solid rgba(255, 255, 255, 0.14);
  }

  /* === Dark-mode fixes for animation cards (Zac flagged Jan 2026) ===
     A handful of elements use translucent-dark or fixed-light colours
     that read fine on the light sheet bg but vanish on the dark sheet
     bg. These overrides invert / brighten them just for dark mode. */

  /* (1) Tap-circle — the dark translucent fill becomes invisible on
     a dark sheet bg. Flip to a translucent light fill so the cursor
     reads as a soft hover indicator either way. */
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.subanim.app .sheet-cursor,
    :root[data-theme="auto"] .sheet.refanim.app .sheet-cursor,
    :root[data-theme="auto"] .sheet.tpanim.app  .sheet-cursor {
      background: rgba(235, 235, 245, 0.24);
    }
  }
  :root[data-theme="dark"] .sheet.subanim.app .sheet-cursor,
  :root[data-theme="dark"] .sheet.refanim.app .sheet-cursor,
  :root[data-theme="dark"] .sheet.tpanim.app  .sheet-cursor {
    background: rgba(235, 235, 245, 0.24);
  }

  /* (2) Floating Total card — #26262C against the #232329 sheet bg
     is almost no contrast. Bump to a clearly-lighter grey so the
     card visibly "lifts" off the sheet. */
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .sheet.app { --tot-bg: #34343D; }
  }
  :root[data-theme="dark"] .sheet.app { --tot-bg: #34343D; }

  /* (3) Line-ref chip INSIDE a highlight — the chip's own light-blue
     pill clashes with the highlight pill it sits inside, so strip
     the chip styling and let the value render as plain text that
     inherits the highlight's colour and weight. */
  .sheet.structure.app .row.is-hl .hl-wrap .tok-lineref,
  .sheet.structure.app .comment.is-hl .hl-bg .tok-lineref {
    display: inline;
    background: none;
    color: inherit;
    padding: 0;
    margin: 0;
    border-radius: 0;
    font-weight: inherit;
  }

  figcaption {
    font-size: 15px; color: var(--ink-faint); font-style: italic;
    margin: -10px 0 0 4px;
  }

  /* generic markdown code fallback before JS upgrade */
  pre.sheet-src { display: none; }

  /* ---------- Testimonials ---------- */
  .quote-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
  @media (max-width: 680px){ .quote-grid { grid-template-columns: 1fr; } }
  .qcard {
    background: var(--paper-raised); border: 1px solid var(--rule);
    border-radius: 14px; padding: 26px 26px 22px; box-shadow: var(--shadow);
  }
  .qcard.span2 { grid-column: 1 / -1; }
  .qcard blockquote { font-size: 18px; line-height: 1.6; color: var(--ink); margin-bottom: 18px; }
  .qcard .who { font-size: 14px; color: var(--ink-faint); display: flex; align-items: center; gap: 11px; }
  .qcard .who b { color: var(--ink-soft); font-weight: 600; }
  .qcard-avatar { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 50%; object-fit: cover; }
  .ticker-wrap {
    margin-top: 40px; overflow: hidden; position: relative;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  }
  .ticker { display: flex; gap: 16px; width: max-content; animation: marquee 140s linear infinite; }
  .ticker-wrap:hover .ticker { animation-play-state: paused; }
  /* Set by the IntersectionObserver in the script below: freezes every
     animation under a container that's scrolled off-screen, so idle CPU sits
     near zero when the animation isn't in front of the reader. */
  .anim-paused, .anim-paused *, .anim-paused *::before, .anim-paused *::after {
    animation-play-state: paused !important;
  }
  .tcard {
    flex: 0 0 auto; max-width: 360px; background: var(--paper-raised);
    border: 1px solid var(--rule); border-radius: 12px; padding: 16px 20px;
    font-size: 15px; color: var(--ink-soft);
    display: flex; flex-direction: column;
  }
  .tcard .who { display: flex; align-items: center; gap: 9px; margin-top: auto; padding-top: 12px; font-size: 13px; color: var(--ink-faint); }
  .tcard-avatar { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%; object-fit: cover; }
  .tcard .who b { color: var(--ink-soft); font-weight: 600; }
  @keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
  @media (prefers-reduced-motion: reduce) {
    /* The marquee is a pure motion affordance. With reduced motion on, a
       static wrapped grid would just dump all 20 cards in front of the reader
       to scroll past — so hide the ticker entirely and let the six headline
       quote cards above stand on their own. */
    .ticker-wrap { display: none; }
  }

  /* ---------- Tools / Calculator blocks ---------- */
  .block { margin: 0 0 14px; }
  .block > h3 { font-size: clamp(21px, 3vw, 27px); margin: 54px 0 10px; }
  .block > .blk-sub { font-weight: 600; font-family: "Hanken Grotesk",sans-serif; font-size: 19px; margin: 50px 0 4px; color: var(--ink); }
  .block p { color: var(--ink-soft); max-width: 62ch; }
  .block p + p { margin-top: 12px; }

  /* ---------- Calculator example rail (horizontal screenshot gallery) ----
     Centered H2/lede stay in the reading column; the 10 verified app-sheet
     "screenshots" become a full-bleed horizontal scroll-snap strip — the
     Apple product-page pattern. First/last card align to the reading
     column via track padding; edges fade; scrollable by trackpad, wheel,
     drag or keyboard. */
  .ex-rail {
    --ex-card: 540px; --ex-gap: 26px; --ex-fade: 32px;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity;
    /* match the track's leading inset so snap doesn't eat it (first card
       then rests aligned with the centered reading column, not flush left) */
    scroll-padding-inline: clamp(28px, calc((100vw - var(--maxw)) / 2 + 28px), 32vw);
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    cursor: grab;
    padding: 6px 0 10px;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--ex-fade), #000 calc(100% - var(--ex-fade)), transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 var(--ex-fade), #000 calc(100% - var(--ex-fade)), transparent 100%);
  }
  .ex-rail.dragging { cursor: grabbing; scroll-snap-type: none; }
  .ex-rail::-webkit-scrollbar { height: 0; }
  .ex-rail { scrollbar-width: none; }
  .ex-track {
    display: flex; gap: var(--ex-gap); width: max-content;
    /* first/last card line up with the centered reading column above */
    padding-inline: clamp(28px, calc((100vw - var(--maxw)) / 2 + 28px), 32vw);
  }
  .ex-card {
    flex: 0 0 min(var(--ex-card), 84vw);
    margin: 0; scroll-snap-align: start; scroll-snap-stop: always;
    display: flex; flex-direction: column;
  }
  .ex-card > .blk-sub {
    margin: 0 0 10px 2px; font-weight: 600;
    font-family: "Hanken Grotesk", sans-serif; font-size: 18px; color: var(--ink);
  }
  .ex-card .sheet { margin: 0; width: 100%; }
  /* 3-row floor (Zac, May 2026): every card is exactly 3 data rows.
     A plain 3-row sheet is 154px; the Laps card is 167px (its inline
     subtotal adds a rule + padding so it sits close to the value).
     Floor to the tallest (167px) so the rail reads evenly. Tops align
     (see #engine .ex-track) so the slack on plain cards falls at the
     bottom. */
  .ex-card .sheet.app:not(.anim) { min-height: 167px; }
  /* Notepad rail — every sheet hits a minimum height (Trip planning's
     natural size) so the cards' animation windows line up horizontally
     across the rail. Tags & Structure & Style are naturally taller and
     just exceed this floor. */
  .ex-rail--notepad .ex-card .sheet.app { min-height: 292px; }
  /* Inline subtotal — matches Soulver: no "subtotal" text, a faint
     rule (inset from the answer-column edges) above a bold value. */
  .sheet.app:not(.anim) .row.sub .ans {
    position: relative;
    font-weight: 700; padding-top: 4px;
  }
  .sheet.app:not(.anim) .row.sub .ans::before {
    content: ""; position: absolute; top: 1px;
    left: 10px; right: 14px;
    border-top: 1px solid var(--slv-sub-rule);
  }
  .ex-card figcaption { margin: 14px 2px 0; font-size: 14px; font-style: normal; }
  .ex-end { flex: 0 0 1px; }
  /* Prev/next controls above a rail (in addition to drag / trackpad /
     keyboard). Aligned to the centred reading column, right side. */
  .ex-navwrap { display: flex; justify-content: center; margin: 22px auto 0; }
  /* When placed BEFORE the rail (right under the section heading)
     instead of after it, tighten the top and add some breathing
     room below so the arrows visually belong with the heading. */
  .ex-navwrap--above { margin: 6px auto 18px; }
  /* Engine's arrow row sits under a full lede paragraph (not just a heading),
     so it wants more separation from the text above than Tools does. */
  #engine .ex-navwrap--above { margin-top: 24px; }
  .ex-nav { display: flex; gap: 12px; }
  /* Chevron buttons in the style of Apple's product pages
     (apple.com/os/ipados — borrowed exactly, including the SVG paths).
     Borderless circular button, --rule-soft grey fill (a touch lighter
     than --rule so it sits softer on the cream page, while still clearly
     visible — unlike --paper-raised which is pure white), darker on hover
     and click. color-mix darkens the same base in both themes automatically. */
  .ex-arrow {
    width: 44px; height: 44px; border-radius: 50%;
    border: 0; background: var(--rule-soft);
    color: var(--ink-soft);
    display: grid; place-items: center; cursor: pointer;
    transition: background .2s ease, color .2s ease, transform .12s ease, opacity .15s;
  }
  .ex-arrow svg { width: 28px; height: 28px; fill: currentColor; display: block; }
  .ex-arrow:hover {
    background: color-mix(in srgb, var(--rule-soft) 86%, var(--ink) 14%);
    color: var(--ink);
  }
  .ex-arrow:active {
    background: color-mix(in srgb, var(--rule-soft) 75%, var(--ink) 25%);
    transform: scale(.92);
  }
  .ex-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .ex-arrow[disabled] { opacity: .32; cursor: default; pointer-events: none; }
  .ex-arrow[disabled]:hover { background: var(--rule-soft); color: var(--ink-soft); }
  /* Notepad rail: richer cards (heading + prose + a plain sheet). Wider
     than the engine screenshots, natural height, tops aligned (content
     length varies a lot here — these become SVG explainers later). */
  #engine .ex-track { align-items: flex-start; }
  /* This rail has no per-card captions (the demos carry the section), so the
     window sits flush at the card bottom. The rail clips vertically
     (overflow-y:hidden), so without the caption's old ~31px of below-window
     space the window's drop-shadow (reaches ~38px light / ~44px dark) was
     clipped. Restore clearance via the rail's bottom padding. */
  #engine .ex-rail { padding-bottom: 38px; --ex-gap: 36px; }
  /* Tighter mode, horizontal half: engine screenshots are sized to
     their own content width too — a real window fitted to its text,
     not a fixed 540px box with a dead zone before the answer band.
     Floor so a short card isn't cramped; ceiling so the longest
     expression (the loan line) can't blow out. */
  #engine .ex-card { flex: 0 0 auto; }
  #engine .ex-card .sheet.app:not(.anim) {
    width: max-content;
    min-width: 360px;
    max-width: min(660px, 86vw);
  }
  #tools .ex-rail { --ex-card: 600px; }
  #tools .ex-track { align-items: flex-start; }
  #tools .ex-card { display: block; }
  #tools .ex-card > h3 {
    margin: 0 0 12px; font-size: clamp(22px, 2.8vw, 27px);
    font-family: "Fraunces", serif; font-weight: 500;
    color: var(--ink); line-height: 1.22; letter-spacing: -.01em;
  }
  #tools .ex-card > p {
    margin: 0 0 12px; max-width: none;
    font-size: 17px; line-height: 1.55; color: var(--ink-soft);
  }
  #tools .ex-card .sheet { margin: 22px 0 0; }
  /* More breathing room between Notepad cards than the Calculator gallery's
     compact 26px — these cards are substantive (heading + prose + sheet +
     subline) and deserve clearer separation. */
  #tools .ex-rail { --ex-gap: 52px; }
  @media (prefers-reduced-motion: reduce) {
    .ex-rail { scroll-behavior: auto; }
  }
  @media (max-width: 760px) {
    .ex-rail { --ex-card: 88vw; --ex-fade: 18px; }
  }
  .subhead {
    font-family:"Fraunces",serif; font-size: 22px; color: var(--ink);
    margin: 64px 0 18px; padding-bottom: 10px; border-bottom: 1px solid var(--rule);
  }

  /* ---------- FAQ ---------- */
  /* A subtle off-white band sets the FAQ apart as its own chapter while the
     rest of the page stays on the base cream (--paper). */
  #faq { background: var(--faq-bg); padding-bottom: 44px; }
  .faq-group { margin-top: 50px; }
  .faq-group > h3 {
    font-size: 26px; font-weight: 600;
    color: var(--ink); font-family:"Fraunces",serif;
    margin: 0 0 26px;
    display: flex; align-items: center; justify-content: flex-start; gap: 12px;
  }
  /* FAQ group icon — same Apple-tile recipe as the Apps section icons:
     flat-square SVG, CSS supplies the squircle rounding + soft shadow. */
  .faq-icon {
    flex: 0 0 26px; width: 26px; height: 26px; display: block;
    border-radius: 23%;
    box-shadow: 0 1px 1px rgba(0,0,0,.05), 0 2px 6px rgba(0,0,0,.10);
  }
  details.faq {
    border-bottom: 1px solid var(--rule); padding: 6px 0;
  }
  /* The bottom rule separates adjacent rows; on each group's last row it has
     nothing below it but whitespace, so drop the trailing line for cleaner
     group boundaries throughout. */
  .faq-group > details.faq:last-of-type { border-bottom: none; }
  details.faq summary {
    list-style: none; cursor: pointer; padding: 22px 0;
    font-family:"Fraunces",serif; font-size: 21px; color: var(--ink);
    display: flex; justify-content: space-between; gap: 20px; align-items: baseline;
  }
  details.faq summary::-webkit-details-marker { display: none; }
  details.faq summary::after { content: "+"; color: var(--ink-faint); font-family:"Hanken Grotesk",sans-serif; font-size: 22px; transition: transform .2s ease; }
  details.faq[open] summary::after { transform: rotate(45deg); }
  details.faq .ans { padding: 4px 0 26px; color: var(--ink-soft); max-width: 64ch; }
  details.faq .ans p + p { margin-top: 12px; }
  /* JS-driven accordion (smooth height + one-open-at-a-time). When the
     controller is active it adds .faq-js and drives the icon off click
     intent (.is-open) so the "×" rotates back in sync with the shrink
     animation, instead of snapping when [open] flips at the very end. */
  #faq.faq-js details.faq summary::after { transform: none; }
  #faq.faq-js details.faq.is-open summary::after { transform: rotate(45deg); }
  /* Easter egg: the "meaning of life" marker is a hollow star that fills with a
     purple gradient + pops (no spin) + gently shimmers when you open it. Keyed
     off .is-open in JS mode (the accordion sets [open] on closed items too). */
  details.faq.faq-star summary::after {
    content: "☆"; color: var(--ink-faint);
    font-family:"Hanken Grotesk",sans-serif; font-size: 19.5px;
    transition: none; animation: none; filter: none; text-shadow: none;
    background: none; -webkit-text-fill-color: var(--ink-faint);
  }
  details.faq.faq-star[open] summary::after,            /* native (no-JS) open */
  #faq.faq-js details.faq.faq-star.is-open summary::after {   /* JS mode: ignite */
    content: "★"; transform: none;
    background: linear-gradient(135deg, #7c3aed 0%, #c084fc 50%, #a855f7 100%);
    background-size: 200% 200%;
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
    /* glow via text-shadow, NOT filter: drop-shadow — in WebKit a filter on a
       background-clip:text element clips the shadow to the square content box
       (the corners get cut off). text-shadow follows the glyph outline and
       needs no compositor layer. See memory: safari-filter-ghost. */
    text-shadow: 0 0 6.6px rgba(147,51,234,.55);
    animation: faqStarSheen 3.4s ease-in-out infinite;
  }
  #faq.faq-js details.faq.faq-star summary::after {      /* JS mode: reset to hollow */
    content: "☆"; color: var(--ink-faint);
    background: none; -webkit-text-fill-color: var(--ink-faint);
    filter: none; text-shadow: none; animation: none;
  }
  @keyframes faqStarSheen {
    0%, 100% { background-position: 0% 50%; }
    50%      { background-position: 100% 50%; }
  }
  @media (prefers-reduced-motion: reduce) {
    details.faq.faq-star[open] summary::after,
    #faq.faq-js details.faq.faq-star.is-open summary::after { animation: none; }
  }
  /* Last FAQ ("Anything else?") reserves its answer's height so opening it
     fades the text in rather than growing the section + nudging the footer.
     The <details> is held open by JS (space reserved) and the answer's
     visibility is driven by opacity off .is-open. JS-only — without JS it
     falls back to the normal native expand/collapse. */
  /* Reserve only the text's height (not the usual 26px answer tail) so the
     closed state isn't a cavern — the section padding already breathes below. */
  #faq.faq-js details.faq.faq-fade .ans { opacity: 0; padding-bottom: 4px; transition: opacity .3s ease; }
  #faq.faq-js details.faq.faq-fade.is-open .ans { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    #faq.faq-js details.faq.faq-fade .ans { transition: none; }
  }

  /* ---------- Footer ---------- */
  footer { text-align: center; padding: 78px 0 40px; border-top: 1px solid var(--rule); position: relative; isolation: isolate; overflow: hidden; }
  /* ---- Footer ambient wash ----
     Mirrors the top wash as a bookend: after the FAQ's closing line the same
     amber/blue/violet field fades back IN from the top and pools gently
     toward the page's foot. Content rides above it; reduced-motion freezes. */
  footer > .wrap { position: relative; z-index: 1; }
  footer::before {
    content: ""; position: absolute; z-index: 0; pointer-events: none;
    top: -6%; left: -10%; width: 120%; height: 112%;
    background:
      radial-gradient(54% 40% at 26% 76%, rgba(253,193,3,.14),  transparent 72%),
      radial-gradient(56% 42% at 76% 70%, rgba(86,171,250,.12), transparent 72%),
      radial-gradient(74% 40% at 50% 96%, rgba(165,104,205,.06), transparent 76%);
    filter: blur(45px);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 42%, #000 100%);
            mask-image: linear-gradient(to bottom, transparent 0%, #000 42%, #000 100%);
    /* Cache the blur on its own GPU layer (see .topwash::before note) so
       WebKit doesn't re-rasterize blur(45px) on every overlapping repaint. */
    transform: translateZ(0);
    backface-visibility: hidden;
  }
  /* Static — see note on .topwash::before re: animating a blurred layer. */
  /* dark mode — soft warm halo highlighting the footer app icon (bookend to the hero) */
  :root[data-theme="dark"] footer::before,
  :root[data-theme="auto"] footer::before {
    background:
      radial-gradient(37% 18% at 51% 13%, rgba(244,232,215,.32), transparent 80%);
    filter: blur(90px);
    opacity: 1;
  }
  /* dither overlay — breaks up 8-bit gradient banding (not blurred; GPU-cached, static) */
  :root[data-theme="dark"] footer::after,
  :root[data-theme="auto"] footer::after {
    content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size: 200px 200px;
    opacity: .03;
    transform: translateZ(0); backface-visibility: hidden;
  }
  .foot-app-icon { width: 84px; height: 84px; display: block; margin: 0 auto 22px; filter: drop-shadow(0 5px 16px rgba(0,0,0,.11)); }
  footer h2 { font-size: clamp(28px,4.4vw,40px); margin-bottom: 37px; }   /* heading→badge gap matched to the real badge→meta gap (badge's inline-block strut + meta's line-leading add ~7px below), so the larger badge sits optically centered */
  .foot-links { margin-top: 46px; font-size: 15px; }
  .foot-links a { color: var(--ink-soft); }
  .foot-links a:hover { color: var(--accent); text-decoration: none; }
  .foot-links .sep { color: var(--ink-faint); margin: 0 16px; }
  @media (max-width: 520px){ .foot-links a { display: block; margin: 8px 0; } .foot-links .sep { display: none; } }
  /* Footer feature links with characterful dimensional bitmap icons.
     Bottom-aligned so labels line up even when an item has no icon yet. */
  /* Fixed 4-up grid (not a content-sized wrapping flexbox): keeps all four
     on one row in every language. A long localized label (fr/de run longer
     than English) wraps to a second line under its own icon instead of
     bumping the whole tile onto a new row. Icons top-aligned so the row of
     icons stays clean regardless of label height. */
  .foot-features { display: grid; grid-template-columns: repeat(4, 1fr); justify-items: center; align-items: start; gap: 34px 16px; margin-top: 46px; }
  .foot-feature { display: flex; flex-direction: column; align-items: center; gap: 12px; font-size: 15px; color: var(--ink-soft); }
  .foot-feature span { text-align: center; overflow-wrap: anywhere; }
  .foot-feature:hover { color: var(--accent); text-decoration: none; }
  .foot-feature img { width: 52px; height: 52px; border-radius: 23%; box-shadow: 0 2px 3px rgba(0,0,0,.08), 0 5px 13px rgba(0,0,0,.18); }
  /* Below the width where all four fit on one row, the wrapping flexbox
     left the content-sized items mis-aligned. Use an even 2-col grid so the
     icons sit in two clean columns; cap + center it so the cluster stays
     tight rather than splaying to the footer's full width. */
  @media (max-width: 680px){
    .foot-features {
      display: grid; grid-template-columns: repeat(2, 1fr);
      justify-items: center; align-items: start;
      gap: 30px 18px;
      max-width: 340px; margin-left: auto; margin-right: auto;
    }
  }
  .foot-grid { margin-top: 54px; display: grid; grid-template-columns: repeat(3, minmax(150px, auto)); justify-content: center; gap: 30px 76px; text-align: left; }
  .foot-col { min-width: 0; }
  .foot-col-h { font-size: 11px; letter-spacing: .13em; text-transform: uppercase; color: var(--ink-faint); font-weight: 600; margin-bottom: 13px; }
  .foot-col ul { list-style: none; margin: 0; padding: 0; }
  .foot-col li { margin: 7px 0; }
  .foot-col a { color: var(--ink-soft); font-size: 15px; }
  .foot-col a:hover { color: var(--accent); text-decoration: none; }
  .foot-col .tbd { color: var(--ink-faint); font-size: 15px; }
  @media (max-width: 660px){ .foot-grid { grid-template-columns: repeat(2, minmax(150px, auto)); } }
  @media (max-width: 420px){ .foot-grid { grid-template-columns: minmax(150px, auto); } }
  .colophon { margin-top: 44px; font-size: 14px; color: var(--ink-faint); }

  /* ---------- Page-load motion ----------
     Sequence: notepad icon arrives first (paper rises, then the 4
     calculator buttons pop into it), THEN the text content
     cascades from top to bottom (heading → sub → CTA → meta). The
     icon "introduces itself" as a visual landmark before the
     reader's eye starts processing copy. */
  @media (prefers-reduced-motion: no-preference) {
    .rise { opacity: 0; transform: translateY(14px); will-change: transform, opacity; animation: rise .9s cubic-bezier(.2,.7,.2,1) forwards; }
    /* Arriving via the language switcher: skip the intro, land settled. */
    html.no-lang-intro .rise { animation: none; opacity: 1; transform: none; }
    html.no-lang-intro .hero .app-icon-svg .ai-btn { animation: none; opacity: 1; transform: scale(1); }
    .hero .app-icon { animation-delay: .05s; }
    .hero h1      { animation-delay: 1.05s; }
    .hero .sub    { animation-delay: 1.18s; }
    .hero .cta-primary, .hero .cta-appstore { animation-delay: 1.32s; }
    .hero .cta-meta, .hero .cta-alts { animation-delay: 1.44s; }
    @keyframes rise { to { opacity: 1; transform: none; } }

    /* Hero icon — the 4 calculator buttons pop into the notepad
       in reading order, echoing the iOS-tap pulse motif used on
       the feature cards below ($, %, clock, =). One-shot. The
       paper face + yellow tab stay visible throughout (they're
       the "notepad" that the calculator is meeting). The text
       below the icon waits for these to finish before it cascades
       in. */
    .hero .app-icon-svg .ai-btn {
      transform-box: fill-box;
      transform-origin: center;
      transform: scale(0);
      opacity: 0;
      will-change: transform, opacity;
      animation: ai-pop .42s cubic-bezier(.34, 1.56, .64, 1) forwards;
    }
    .hero .app-icon-svg .ai-btn-1 { animation-delay: .35s; }
    .hero .app-icon-svg .ai-btn-2 { animation-delay: .50s; }
    .hero .app-icon-svg .ai-btn-3 { animation-delay: .65s; }
    .hero .app-icon-svg .ai-btn-4 { animation-delay: .80s; }
    @keyframes ai-pop {
      to { opacity: 1; transform: scale(1); }
    }
  }
  /* Reduced-motion: buttons just appear, no scaling. */
  @media (prefers-reduced-motion: reduce) {
    .hero .app-icon-svg .ai-btn { opacity: 1; }
  }

  /* ---------- Trinity tabs: Notepad / Calculator / Platforms ---------- */
  .seg { display: flex; gap: 4px; width: max-content; max-width: 100%; margin: 0 auto;
         padding: 4px; background: var(--rule-soft); border: 1px solid var(--rule);
         border-radius: 13px; }
  .seg-btn { appearance: none; -webkit-appearance: none; border: 0; background: transparent;
             font: inherit; font-weight: 600; font-size: 15px; letter-spacing: -.005em;
             color: var(--ink-soft); padding: 11px 26px; border-radius: 9px; cursor: pointer;
             white-space: nowrap; transition: background .2s ease, color .2s ease, box-shadow .2s ease; }
  .seg-btn:hover { color: var(--ink); }
  .seg-btn[aria-selected="true"] { background: var(--paper-raised); color: var(--ink); box-shadow: var(--shadow); }
  /* In dark mode, --paper-raised is darker than --rule-soft (the strip
     bg), so the selected tab would actually be DARKER than the strip —
     opposite of what we want. Override with a clearly lighter shade so
     selection reads at a glance. */
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .seg-btn[aria-selected="true"] {
      background: #34343D;
      box-shadow: 0 1px 2px rgba(0,0,0,.5), 0 4px 12px rgba(0,0,0,.35);
    }
  }
  :root[data-theme="dark"] .seg-btn[aria-selected="true"] {
    background: #34343D;
    box-shadow: 0 1px 2px rgba(0,0,0,.5), 0 4px 12px rgba(0,0,0,.35);
  }
  .seg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .tpanels { margin-top: 58px; }
  .tpanel { padding: 0; border-top: 0; }
  .tpanel[hidden] { display: none; }
  .tpanel:not([hidden]) { animation: tabfade .3s ease; }
  @keyframes tabfade { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
  @media (max-width: 560px) {
    .seg { width: 100%; }
    .seg-btn { flex: 1; padding: 11px 8px; font-size: 14px; }
  }

  /* ---------- "The Apps" platform sub-tabs ----------
     The trinity's "The Apps" panel hosts its own platform selector:
     Mac / iPad / iPhone / Windows. Smaller, lighter than the trinity
     segmented control — these read as a filter row, not a primary
     navigation. Each sub-panel: hero screenshot (lazy-loaded), short
     intro, then a 4-group grid (Sharing & Export · Automation ·
     Sheets & Sync · Internationalization) with a single glyph per
     group. */
  .appsub-tabs {
    display: flex; justify-content: center; align-items: flex-end; gap: 10px;
    margin: 28px auto 48px;
    width: max-content; max-width: 100%;
  }
  .appsub-tab {
    appearance: none; -webkit-appearance: none; border: 0;
    background: transparent;
    color: var(--ink-soft); font-weight: 500; font-size: 13.5px;
    width: 104px; padding: 12px 12px 10px; border-radius: 16px; cursor: pointer;
    font-family: inherit;
    display: inline-flex; flex-direction: column; align-items: center; gap: 8px;
    transition: color .15s ease, background .15s ease;
  }
  /* Glyphs are real-world scale (a monitor reads larger than a handheld). The
     icon area hugs the glyph height — NO fixed shelf — and the tabs bottom-align,
     so the selection pill keeps a common WIDTH while its HEIGHT hugs each device
     (a short pill for the iPhone, a taller one for the Mac). */
  .appsub-tab-icon {
    display: flex; align-items: flex-end; justify-content: center;
  }
  .appsub-tab-glyph {
    display: block; width: auto; flex: 0 0 auto;
    object-fit: contain;
  }
  #appsub-mac .appsub-tab-glyph     { height: 70px; }
  #appsub-windows .appsub-tab-glyph { height: 60px; }
  #appsub-ipad .appsub-tab-glyph    { height: 46px; }
  #appsub-iphone .appsub-tab-glyph  { height: 34px; }
  /* light/dark glyph swap — the -dark variants lighten the device bezels so they
     don't vanish against the dark page. */
  .glyph-dark { display: none; }
  :root[data-theme="dark"] .glyph-light { display: none; }
  :root[data-theme="dark"] .glyph-dark  { display: block; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .glyph-light { display: none; }
    :root[data-theme="auto"] .glyph-dark  { display: block; }
  }
  .appsub-tab:hover {
    color: var(--ink);
    /* --rule (not --paper-raised) so the hover pill is clearly visible:
       --paper-raised is pure white in light mode and vanishes on the cream page. */
    background: var(--rule);
  }
  /* Selected: faint warm-grey fill from the theme (--rule), dark text — soft and
     on-palette rather than a loud colored pill. */
  .appsub-tab[aria-selected="true"] {
    background: var(--rule); color: var(--ink);
    font-weight: 600;
  }
  .appsub-tab[aria-selected="true"]:hover {
    background: var(--rule);
    color: var(--ink);
  }
  /* dark mode: --rule is near-black (#2C2C33) and barely reads on the dark page;
     use a distinctly lighter fill for the selected pill. */
  :root[data-theme="dark"] .appsub-tab[aria-selected="true"],
  :root[data-theme="dark"] .appsub-tab[aria-selected="true"]:hover { background: #38383f; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .appsub-tab[aria-selected="true"],
    :root[data-theme="auto"] .appsub-tab[aria-selected="true"]:hover { background: #38383f; }
  }
  .appsub-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .appsub-panel[hidden] { display: none; }
  /* Device tabs: fade only, no upward slide (the shared tabfade adds a 7px
     translateY, which reads as the device jumping up a couple px on switch). */
  .appsub-panel:not([hidden]) { animation: appsubfade .3s ease; }
  @keyframes appsubfade { from { opacity: 0; } to { opacity: 1; } }

  /* Hero screenshot slot — reserves layout space so the page doesn't
     jump when the lazy-loaded image arrives. Aspect 16:10 fits Mac /
     iPad landscape / Windows naturally; iPhone slot keeps the same
     outer aspect with the phone floating centered (transparent
     surround) so all 4 platforms share the same layout container. */
  .appsub-shot {
    aspect-ratio: 16 / 10;
    background: var(--paper-raised);
    border: 1px solid var(--rule-soft);
    border-radius: 14px;
    margin: 0 auto 48px;
    max-width: 880px;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    color: var(--ink-faint);
    font-size: 14px;
    font-style: italic;
  }
  .appsub-shot img {
    display: block; width: 100%; height: 100%;
    object-fit: contain;
  }
  /* Hide the <img> entirely until it has successfully loaded — this
     prevents the broken-image icon during the dev placeholder phase
     (image file doesn't exist yet) and during the brief moment between
     `src` being set and the bytes arriving in production. */
  .appsub-shot:not(.is-loaded) img { display: none; }
  .appsub-shot:not(.is-loaded) video { display: none; }
  .appsub-shot.is-loaded .appsub-shot-ph { display: none; }
  /* Loading indicator: a small spinner centered in the shot frame while the
     screenshot/video bytes arrive. The placeholder text is kept for screen
     readers; reduced-motion shows that text instead of a spinning ring. */
  .appsub-shot-ph {
    font-size: 0; color: transparent;
    display: flex; align-items: center; justify-content: center;
  }
  .appsub-shot-ph::before {
    content: ""; box-sizing: border-box;
    width: 30px; height: 30px; border-radius: 50%;
    border: 3px solid var(--rule);
    border-top-color: var(--ink-faint);
    animation: appsub-spin .8s linear infinite;
  }
  @keyframes appsub-spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) {
    .appsub-shot-ph { font-size: 13px; color: var(--ink-faint); font-style: italic; }
    .appsub-shot-ph::before { display: none; }
  }

  /* ---- iPad: real screenshot inside a thin space-black device bezel ----
     The capture is the bare iPad screen (4:3 landscape, incl. status bar);
     the bezel is pure CSS so it scales + theme-swaps. All the knobs to
     experiment with live in the custom properties on .device-ipad. */
  .appsub-shot[data-platform="ipad"] {
    aspect-ratio: auto;            /* let the device set its own height */
    background: transparent;
    border: none;
    border-radius: 0;
    overflow: visible;
    max-width: none;
    margin-bottom: 32px;          /* clearance so the (now tight) drop-shadow doesn't touch the caption */
  }
  /* iPad caption: the platform glyph now lives in the switcher pill (not here),
     and the "Soulver for iPad" heading is gone (redundant) — so the text under
     the device is just the standard centered .appsub-intro. */
  /* The device frame is now a self-contained vector SVG (PommePlate iPad Pro,
     CC0) with the screenshot embedded — so .device-ipad is just a sizing
     wrapper. The drop-shadow filter follows the rounded frame outline. */
  .device-ipad {
    width: 100%;
    max-width: 880px;
    margin: 0 auto;
    /* box-shadow, not filter: drop-shadow — a CSS filter creates a separate
       compositing layer that WebKit strands when the panel goes display:none,
       leaving a wide ghost halo behind the next (narrower) panel until a
       repaint. box-shadow is painted into the element, never a filter layer,
       so nothing can orphan. The iPad SVG frame fills its box edge-to-edge with
       a ~40px outer corner radius (135 SVG units × 880/2924), so a matched
       border-radius makes the shadow hug the frame identically. */
    box-shadow: 0 7px 16px rgba(0,0,0,.13);   /* tight soft lift, hugs the frame */
    border-radius: 40px;
  }
  .device-ipad img {
    display: block;
    width: 100%;
    height: auto;
  }
  /* theme-aware screenshot swap (page themes via data-theme on <html>) */
  .device-ipad .shot-dark { display: none; }
  :root[data-theme="dark"] .device-ipad .shot-light { display: none; }
  :root[data-theme="dark"] .device-ipad .shot-dark  { display: block; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .device-ipad .shot-light { display: none; }
    :root[data-theme="auto"] .device-ipad .shot-dark  { display: block; }
  }

  /* ---- Mac: the bare app window (real macOS rounded corners captured as
     transparent alpha), floating directly on the section — no card. Mirrors
     the iPad treatment: kill the .appsub-shot card chrome, wrap in a
     .device-mac sizing wrapper whose drop-shadow follows the window's
     rounded-corner alpha, and theme-swap light/dark captures. */
  .appsub-shot[data-platform="mac"] {
    aspect-ratio: auto;            /* let the window set its own height */
    background: transparent;
    border: none;
    border-radius: 0;
    overflow: visible;             /* let the shadow spill onto the section */
    max-width: none;
    margin-bottom: 32px;
  }
  .device-mac {
    width: 100%;
    max-width: 880px;
    margin: 0 auto;
    /* box-shadow, not filter: drop-shadow — see .device-ipad note: a filter
       layer ghosts when the panel is hidden; box-shadow can't. The window is
       captured edge-to-edge with macOS's ~10px corner radius (≈11px at this
       render scale), so a matched border-radius hugs the window. */
    box-shadow: 0 12px 30px rgba(0,0,0,.15);   /* soft lift, hugs the window corners */
    border-radius: 11px;
  }
  .device-mac img {
    display: block;
    width: 100%;
    height: auto;
  }
  /* theme-aware screenshot swap (page themes via data-theme on <html>) */
  .device-mac .shot-dark { display: none; }
  :root[data-theme="dark"] .device-mac .shot-light { display: none; }
  :root[data-theme="dark"] .device-mac .shot-dark  { display: block; }
  @media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .device-mac .shot-light { display: none; }
    :root[data-theme="auto"] .device-mac .shot-dark  { display: block; }
  }

  /* ---- iPhone: a short product video, not a still. The capture (matte.app)
     already includes a realistic titanium iPhone frame on a transparent
     background, so .device-iphone is just a sizing wrapper (like .device-ipad).
     The video carries an alpha channel so the phone sits on either page theme;
     it's served as HEVC-alpha (Safari/WebKit) or VP9-alpha WebM (everyone else),
     chosen in JS. It plays when the iPhone tab is opened and loops endlessly,
     with a brief hold on the finished sheet between runs (the footage is a
     narrative — empty sheet → built up — so the hold softens the reset).
     Reduced-motion rests on the final frame instead of looping. */
  .appsub-shot[data-platform="iphone"] {
    aspect-ratio: auto;
    background: transparent;
    border: none;
    border-radius: 0;
    overflow: visible;
    max-width: none;
    margin-bottom: 32px;
  }
  .device-iphone {
    width: 100%;
    max-width: 300px;
    margin: 0 auto;
    filter: drop-shadow(0 9px 22px rgba(0,0,0,.18));   /* soft lift under the phone */
  }
  .device-iphone video {
    display: block;
    width: 100%;
    height: auto;
    cursor: pointer;                                   /* tap to replay */
  }

  /* Short intro under the screenshot */
  .appsub-intro {
    text-align: center;
    max-width: 580px;
    margin: 0 auto 28px;
    color: var(--ink-soft);
  }
  .appsub-intro h3 {
    margin: 0 0 22px;
    font-size: clamp(24px, 3vw, 30px);
    color: var(--ink);
  }
  /* Platform glyph beside the title — small 3D device icon next to "Soulver for X" */
  .appsub-intro-header {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-bottom: 22px;
  }
  .appsub-intro-header h3 { margin: 0; }
  .appsub-platform-glyph {
    width: 56px;
    height: 56px;
    flex-shrink: 0;
    object-fit: contain;
  }
  .appsub-intro p { margin: 0 0 8px; }
  .appsub-intro a { color: var(--ink-soft); border-bottom: 1px solid var(--rule); }
  .appsub-intro a:hover { color: var(--ink); text-decoration: none; }
  /* Tiny "system requirements" line tucked under the platform intro.
     Quieter than the body copy — small, faint, slight letter-spacing
     so it reads as fine print without shouting. */
  .appsub-intro .appsub-req {
    margin-top: 20px;
    font-size: 12.5px;
    color: var(--ink-faint);
    letter-spacing: 0.01em;
  }

  /* 4-group grid — 2×2 on desktop, single column below 760 */
  .appsub-groups {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 44px 56px;
    max-width: 820px;
    margin: 0 auto;
  }
  @media (max-width: 760px) {
    .appsub-groups { grid-template-columns: 1fr; gap: 36px; }
  }
  .appsub-group { display: flex; gap: 18px; }
  .appsub-icon {
    flex: 0 0 32px; width: 32px; height: 32px;
    color: var(--accent);
    margin-top: 2px;
    display: block;
  }
  /* "Apple app-icon" category icons. Quiver renders a flat-square
     gradient + white silhouette glyph; CSS rounds the corner and
     adds the shadow so all four icons share identical squircle
     math and shadow depth. */
  img.appsub-icon, svg.appsub-icon {
    border-radius: 23%;
    box-shadow:
      0 1px 1px rgba(0,0,0,.05),
      0 2px 6px rgba(0,0,0,.10);
  }
  .appsub-group-body { min-width: 0; }
  .appsub-group-body h4 {
    margin: 0 0 10px;
    font-size: 16px; font-weight: 600;
    color: var(--ink);
    font-family: "Hanken Grotesk", sans-serif;
  }
  .appsub-group-body ul {
    list-style: none; padding: 0; margin: 0;
    font-size: 15px; line-height: 1.6;
    color: var(--ink-soft);
  }
  .appsub-group-body li { padding: 0; }
  .appsub-group-body li + li { margin-top: 4px; }
  /* Paragraph variant — same visual weight as the ul/li list,
     but with selective <b> emphasis on key features instead of
     hard bullets. Matches the essence section's voice. */
  .appsub-group-body p {
    margin: 0;
    font-size: 15px; line-height: 1.6;
    color: var(--ink-soft);
  }
  .appsub-group-body p + p { margin-top: 8px; }
  .appsub-group-body p b { color: var(--ink); font-weight: 600; }
  .appsub-group-body li a {
    color: var(--ink-soft);
    border-bottom: 1px dotted var(--rule);
  }
  .appsub-group-body li a:hover {
    color: var(--ink); text-decoration: none;
  }

  /* "And one more thing…" quieter trailing line — Soulver Studio /
     truly-devoted callout per CLAUDE.md. */
  .appsub-quieter {
    margin: 72px auto 0;
    max-width: 820px;
    padding-top: 28px;
    border-top: 1px solid var(--rule-soft);
    text-align: center;
    font-size: 14px;
    color: var(--ink-faint);
  }
  .appsub-quieter a {
    color: var(--ink-soft);
    border-bottom: 1px solid var(--rule);
  }
  .appsub-quieter a:hover { color: var(--ink); text-decoration: none; }
