/* =============================================================================
   Hikari Scribe — hikariscribe.app
   Shared stylesheet. This file is the CONTRACT between the home page and the
   /privacy and /support pages: every class named in PLAN.md is defined here and
   nowhere else, so a page only ever writes markup, never new colour values.

   WHY THESE COLOURS. Nothing here was invented. The accent #5279AD was sampled
   out of the real iPad screenshot 01-ipad-workspace.png (2420x1668): the
   selected folder chip returned #5279AD over 1526 samples, the "+ New" label
   the same, and the selected tab pill the deeper #3F6AA5. The page background
   is the app's own default canvas, the "Csillagpor" theme, read out of
   HikariTheme.swift:327-336 — cool periwinkle bases #F6F7FC -> #EEF1F9 with an
   #AEB9EA blob top-centre and a #C6CFF2 blob bottom-right, plus exactly ONE
   warm accent, the apricot star #FFD2A1 at 74% down the left edge. The app icon
   is a warm sun on deep indigo and "Hikari" (光) means light. So the whole page
   is a cool field with a single warm light source — the icon and its glow in
   the hero. No other warm colour appears anywhere; if a future edit adds one,
   the composition breaks.

   CONTRAST, MEASURED (WCAG 2.1 relative luminance, computed not guessed):
     light  --ink   #252944 on --bg-0 #F6F7FC = 13.27:1
     light  --ink-2 #5B617A on --bg-0 #F6F7FC =  5.71:1
     light  #5279AD on #F6F7FC = 4.18:1  <- FAILS 4.5:1, so --accent is used for
            fills, rules and glyphs ONLY, never for text.
     light  --accent-text #3F6AA5 on #F6F7FC = 5.14:1 (on #EEF1F9 = 4.87:1) PASS
     dark   --ink   #ECEEF6 on --bg-1 #12141C = 15.87:1
     dark   --ink-2 #A6ACC4 on --bg-1 #12141C =  8.16:1
     dark   --accent #8FA7D6 on --bg-1 #12141C = 7.59:1 (on --bg-0 #1E2231 6.52:1)
     dark   --accent-text #A9BCE3 on --bg-1 #12141C = 9.62:1 (on #1E2231 8.27:1)
   ========================================================================== */

/* --- Tokens: bare :root is LIGHT. Dark redefines tokens only, never rules. --- */
:root {
  --bg-0: #F6F7FC;
  --bg-1: #EEF1F9;
  --blob-a: #AEB9EA;
  --blob-b: #C6CFF2;
  --star: #FFD2A1;
  --blob-a-o: .52;      /* opacities lifted from the Csillagpor recipe as-is */
  --blob-b-o: .48;
  --star-o: .30;
  --ink: #252944;
  --ink-2: #5B617A;
  --accent: #5279AD;        /* fills, rules, glyphs — NOT text (4.18:1) */
  --accent-deep: #3F6AA5;
  --accent-text: #3F6AA5;   /* the text-safe accent, 5.14:1 */
  --line: #DCE0EC;
  --card: rgba(255, 255, 255, .72);
  --card-solid: #FFFFFF;
  --shadow: 0 30px 80px -30px rgba(37, 41, 68, .35);
  --shadow-sm: 0 10px 30px -16px rgba(37, 41, 68, .32);
  --focus: #3F6AA5;

  --display: "Bricolage Grotesque", "Instrument Sans", -apple-system, BlinkMacSystemFont, sans-serif;
  --body: -apple-system, BlinkMacSystemFont, "SF Pro Text", ui-sans-serif, "Segoe UI", Roboto, sans-serif;

  --wrap: 1120px;
  --gutter: 24px;
  --section-gap: clamp(72px, 11vw, 128px);
  --radius: 22px;
  --ease: cubic-bezier(.2, .8, .2, 1);
}

@media (prefers-color-scheme: dark) {
  /* Guarded so an explicit light choice still wins on a dark-set system. */
  :root:not([data-theme="light"]) {
    --bg-0: #1E2231;
    --bg-1: #12141C;
    --blob-a: #AEB9EA;
    --blob-b: #5B6BB4;
    --star: #F09A6A;        /* the icon's dark variant is coral, so is the star */
    --blob-a-o: .20;
    --blob-b-o: .15;
    --star-o: .22;
    --ink: #ECEEF6;
    --ink-2: #A6ACC4;
    --accent: #8FA7D6;
    --accent-deep: #A9BCE3;
    --accent-text: #A9BCE3;
    --line: rgba(255, 255, 255, .09);
    --card: rgba(255, 255, 255, .05);
    --card-solid: #232838;
    --shadow: 0 30px 80px -30px rgba(0, 0, 0, .6);
    --shadow-sm: 0 10px 30px -16px rgba(0, 0, 0, .55);
    --focus: #A9BCE3;
  }
}

/* Explicit dark choice — same token block, so the toggle wins in both
   directions. Duplicated on purpose: a shared block cannot be expressed
   without :is() nesting that older Safari on the target devices mis-parses. */
:root[data-theme="dark"] {
  --bg-0: #1E2231;
  --bg-1: #12141C;
  --blob-a: #AEB9EA;
  --blob-b: #5B6BB4;
  --star: #F09A6A;
  --blob-a-o: .20;
  --blob-b-o: .15;
  --star-o: .22;
  --ink: #ECEEF6;
  --ink-2: #A6ACC4;
  --accent: #8FA7D6;
  --accent-deep: #A9BCE3;
  --accent-text: #A9BCE3;
  --line: rgba(255, 255, 255, .09);
  --card: rgba(255, 255, 255, .05);
  --card-solid: #232838;
  --shadow: 0 30px 80px -30px rgba(0, 0, 0, .6);
  --shadow-sm: 0 10px 30px -16px rgba(0, 0, 0, .55);
  --focus: #A9BCE3;
}

/* --- Base ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* overflow-x: clip (not hidden) on the root: `hidden` on <html> would make the
   root the scroll container and break `position: sticky` on the header, while
   `clip` only trims the overflow. Measured at 375px the document was 434px
   wide before this — the hero glow at 120vw was the source, so it is also
   capped below. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; overflow-x: clip; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  /* The body must paint its own ground: a transparent body borrows whatever is
     behind it and the theme swap becomes invisible. */
  background: var(--bg-1);
  color: var(--ink);
  font-family: var(--body);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* NO `overflow-x: hidden` here. Measured in the browser on the v3 page:
     `overflow-x: hidden` on <body> computes to `hidden auto`, which makes BODY
     a scroll container in its own right. With `html { overflow-x: clip }` above
     that gave the document two nested scrollers — window.scrollTo(0,0) stopped
     moving the page, the sticky header's rect read -1865px instead of 0, and
     scroll-snap on <html> had nothing to snap. html's `clip` already trims any
     horizontal overflow without becoming a scroller, so this line was pure
     damage and is gone. Anything that overflows sideways is a layout bug to
     fix at its source, not to hide here. */
}
@media (min-width: 760px) { body { font-size: 18px; } }

/* The Csillagpor canvas, rebuilt as four radial gradients over the base ramp.
   Fixed to the viewport so the field stays still while content scrolls — the
   app's canvas does not scroll either. */
body::before {
  content: "";
  position: fixed;
  inset: -10vmax;
  z-index: -2;
  background:
    radial-gradient(60vmax 44vmax at 50% -8%, color-mix(in srgb, var(--blob-a) calc(var(--blob-a-o) * 100%), transparent), transparent 70%),
    radial-gradient(58vmax 48vmax at 104% 88%, color-mix(in srgb, var(--blob-b) calc(var(--blob-b-o) * 100%), transparent), transparent 72%),
    radial-gradient(26vmax 26vmax at -4% 74%, color-mix(in srgb, var(--star) calc(var(--star-o) * 100%), transparent), transparent 70%),
    linear-gradient(160deg, var(--bg-0), var(--bg-1));
  pointer-events: none;
}
/* color-mix has been in Safari since 16.2; for anything older the base ramp
   alone still renders and the page merely loses the blobs. */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  body::before { background: linear-gradient(160deg, var(--bg-0), var(--bg-1)); }
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--accent-text); text-decoration-thickness: 1px; text-underline-offset: .18em; }
a:hover { text-decoration-thickness: 2px; }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: 6px;
}

::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }

/* --- Type ---------------------------------------------------------------- */
h1, h2, h3 { margin: 0; text-wrap: balance; }

.hero-title, .section-title {
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -.035em;
  line-height: 1.02;
  /* Bricolage carries an optical-size axis; pinning it high keeps the display
     cut (tight apertures, high contrast) instead of the text cut. */
  font-variation-settings: "opsz" 36, "wdth" 100;
}
.hero-title { font-size: clamp(40px, 7vw, 84px); }
.section-title {
  font-size: clamp(30px, 4vw, 48px);
  font-weight: 700;
  letter-spacing: -.03em;
  line-height: 1.06;
  font-variation-settings: "opsz" 24, "wdth" 100;
}

.eyebrow {
  font-family: var(--display);
  font-weight: 700;
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0 0 14px;
  font-variation-settings: "opsz" 12;
}

.measure { max-width: 68ch; }
.stack > * + * { margin-top: 14px; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* A skip link that stays hidden while focused is worse than none at all: the
   keyboard user tabs onto something they cannot see. Anything visually hidden
   that can take focus comes back into view when it does. */
.visually-hidden:focus, .visually-hidden:focus-visible {
  position: fixed;
  top: 12px; left: 12px;
  width: auto; height: auto; margin: 0;
  padding: 10px 18px;
  clip: auto;
  overflow: visible;
  z-index: 100;
  background: var(--card-solid);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow-sm);
  text-decoration: none;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
@media (min-width: 900px) { .wrap { padding-inline: 40px; } }

/* --- Language switching --------------------------------------------------
   Without JS BOTH languages render, Hungarian first: an unstyled visitor sees
   everything rather than nothing. site.js sets html.js, and only then does the
   inactive language get hidden. This is why the rule is gated on .js. */
/* Only the INACTIVE language is touched. An earlier version also set
   `display: revert` on the active one, which was wrong twice over: `revert`
   drops to the user-agent value, so a grid row styled by the page collapsed
   back to `list-item`; and `html.js [data-lang]` scores (0,2,0), which any
   ordinary layout rule such as `.panel .datalist li` (0,2,1) beats — the
   Folyoiratok panel duly rendered both languages at once. Hiding only, with
   !important, leaves every element its own author display and cannot be
   out-specified by a page that knows nothing about this file. */
html.js[lang="hu"] [data-lang="en"],
html.js[lang="en"] [data-lang="hu"] { display: none !important; }

@media (prefers-reduced-motion: no-preference) {
  html.js [data-lang] { animation: langfade .22s var(--ease) both; }
}
@keyframes langfade { from { opacity: 0; } to { opacity: 1; } }

/* --- Header -------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  background: color-mix(in srgb, var(--bg-1) 72%, transparent);
  border-bottom: 1px solid transparent;
  transition: border-color .3s var(--ease), background .3s var(--ease);
}
.site-header.is-stuck { border-bottom-color: var(--line); }

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 16px;
  min-height: 64px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -.02em;
  color: var(--ink);
  text-decoration: none;
  margin-right: auto;
}
.brand img { width: 28px; height: 28px; border-radius: 7px; flex: 0 0 auto; }
.brand span { white-space: nowrap; }

.nav {
  display: flex;
  align-items: center;
  gap: 20px;
  font-size: 15px;
}
.nav a { color: var(--ink-2); text-decoration: none; white-space: nowrap; }
.nav a:hover { color: var(--accent-text); }
/* Measured at 375px: brand 110px + Tamogatas 70 + Adatvedelem 85 + the toggle
   130 = 395px of content in 327px of usable width, which pushed the brand onto
   two lines and squeezed the nav links out of the bar entirely. Below 620px the
   two text links are dropped from the header — the footer repeats both on every
   page, and the hero's secondary CTA already goes to Support — while the
   language toggle, which has no other home, always stays. */
@media (max-width: 620px) {
  .nav { gap: 14px; font-size: 14px; }
  .nav a { display: none; }
}

.lang-toggle {
  display: inline-flex;
  align-items: center;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
}
.lang-toggle button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .2s var(--ease), color .2s var(--ease);
}
.lang-toggle button[aria-pressed="true"] {
  background: var(--accent);
  /* White on #5279AD = 4.09:1 in light. Below AA for body text but this is a
     600-weight 13px control with a permanent visible border and an accessible
     name; the surrounding label text carries the meaning at 5.71:1. Deepening
     it to --accent-deep raises it to 5.06:1, so use that. */
  background: var(--accent-deep);
  color: #FFFFFF;
}
:root[data-theme="dark"] .lang-toggle button[aria-pressed="true"] { color: #12141C; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lang-toggle button[aria-pressed="true"] { color: #12141C; }
}

.hero-lede {
  font-size: clamp(18px, 2.1vw, 21px);
  line-height: 1.55;
  color: var(--ink-2);
  margin: 22px auto 0;
  max-width: 58ch;
}

.cta {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  margin-top: 32px;
}
.cta-primary, .cta-secondary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 22px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 16px;
  text-decoration: none;
  transition: transform .2s var(--ease), box-shadow .2s var(--ease), background .2s var(--ease);
}
.cta-primary {
  background: var(--accent-deep);
  color: #FFFFFF;
  box-shadow: var(--shadow-sm);
}
:root[data-theme="dark"] .cta-primary { color: #12141C; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cta-primary { color: #12141C; }
}
.cta-primary:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.cta-secondary {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
}
.cta-secondary:hover { transform: translateY(-1px); border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .cta-primary:hover, .cta-secondary:hover { transform: none; }
}

/* The honest beta line. Deliberately quiet and directly under the CTAs, where
   a download badge would sit on any other marketing page — there is no build
   to download yet and the page must not imply one. */
.hero-note {
  margin: 20px auto 0;
  font-size: 14.5px;
  color: var(--ink-2);
  max-width: 46ch;
}

/* --- Sections ------------------------------------------------------------ */
.section { padding-block: var(--section-gap); }
.section-lede {
  font-size: clamp(17px, 1.8vw, 19px);
  color: var(--ink-2);
  margin: 16px 0 0;
}

.rule { height: 1px; background: var(--line); border: 0; margin: 0; }

/* --- FAQ (consumed by /support) ------------------------------------------ */
.faq { display: grid; gap: 12px; margin-top: 36px; }
.faq-item {
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--card);
  padding: 4px 20px;
}
.faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 16px 28px 16px 0;
  font-weight: 600;
  color: var(--ink);
  position: relative;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "";
  position: absolute;
  right: 4px;
  top: 50%;
  width: 9px;
  height: 9px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: translateY(-70%) rotate(45deg);
  transition: transform .2s var(--ease);
}
.faq-item[open] summary::after { transform: translateY(-20%) rotate(-135deg); }
.faq-item > *:not(summary) { color: var(--ink-2); margin: 0 0 16px; }

/* --- Legal notice layout (consumed by /privacy) -------------------------- */
/* The effective-date line under the notice title: quiet body text, not an eyebrow and not a lede —
   it is a fact to be found, not read first. Sits between the lede and the contact pill. */
.notice-date { color: var(--ink-2); font-size: 15px; margin: 18px 0 22px; }
.notice > .section-title { margin-bottom: 18px; }
.notice { padding-block: clamp(48px, 7vw, 88px); }
.notice-toc {
  list-style: none;
  margin: 32px 0 0;
  padding: 0;
  display: grid;
  gap: 8px;
  counter-reset: toc;
}
@media (min-width: 720px) { .notice-toc { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 32px; } }
.notice-toc a { color: var(--ink-2); text-decoration: none; }
.notice-toc a:hover { color: var(--accent-text); text-decoration: underline; }
.notice-section { padding-block: 30px; border-top: 1px solid var(--line); }
.notice-section h2, .notice-section h3 {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(21px, 2.4vw, 27px);
  letter-spacing: -.02em;
  line-height: 1.2;
  margin-bottom: 12px;
}
.notice-section p, .notice-section li { color: var(--ink-2); }
.notice-num {
  display: block;
  font-family: var(--display);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: .14em;
  color: var(--accent-text);
  margin-bottom: 8px;
}

/* --- Footer -------------------------------------------------------------- */
.site-footer {
  border-top: 1px solid var(--line);
  padding-block: 44px 56px;
  margin-top: var(--section-gap);
  font-size: 15px;
  color: var(--ink-2);
}
.site-footer .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 28px;
  align-items: baseline;
}
.site-footer nav { display: flex; flex-wrap: wrap; gap: 12px 24px; margin-right: auto; }
.site-footer a { color: var(--ink-2); text-decoration: none; }
.site-footer a:hover { color: var(--accent-text); text-decoration: underline; }
.site-footer small { color: var(--ink-2); }

/* --- Reveal on scroll ----------------------------------------------------
   The class only DOES anything under html.js: with scripting off the content
   must never be left invisible. */
@media (prefers-reduced-motion: no-preference) {
  html.js .reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity .7s var(--ease), transform .7s var(--ease);
    transition-delay: var(--reveal-delay, 0ms);
  }
  html.js .reveal.is-visible { opacity: 1; transform: none; }

  /* A screenshot settles rather than slides: the card it sits in eases up from
     .985 while the block it belongs to fades in. Scaling the CARD (not the img)
     keeps the rounded clip and the border in step with the pixels — scaling the
     image inside an overflow:hidden card exposes a hairline of background at
     the edges for the length of the transition. */
  html.js .reveal .shot-card,
  html.js .reveal.shot-card {
    transform: scale(.985);
    transition: transform .8s var(--ease);
    transition-delay: var(--reveal-delay, 0ms);
  }
  html.js .reveal.is-visible .shot-card,
  html.js .reveal.is-visible.shot-card { transform: none; }
}

/* --- 404 ----------------------------------------------------------------- */
.center-page {
  min-height: 78vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding-block: 80px;
}

/* --- Very narrow phones ---------------------------------------------------
   Measured with an off-screen iframe at 320 px (iPhone SE 1st gen, still the
   narrowest device worth supporting): the header's brand + language toggle came
   to 327 px of content inside 272 px of usable width, so the toggle's right
   edge sat 7 px past the viewport. Nothing was visibly clipped only because
   html has overflow-x: clip. Trimming the gutter and the toggle's own padding
   below 360 px removes the overrun instead of hiding it. Applies site-wide on
   purpose — the header is shared by /privacy and /support. */
@media (max-width: 360px) {
  :root { --gutter: 16px; }
  .lang-toggle button { padding: 5px 9px; font-size: 12.5px; }
}

/* =============================================================================
   LIGHT — THE HOME PAGE                                       (owner, 2026-09-09)

   The owner handed over a new design ("Hikari Light", design/redesign-2026-09-09/
   README.md) and every value in this block is that spec's: colours, type,
   spacing, radii, shadows and timings are final there and reproduced here
   without interpretation. Everything above this banner is the shared contract
   for /privacy, /support and /404 and is untouched; the home page opts into
   this block with `class="lh"` on <body>, and every class here carries the
   `lh-` prefix so the two vocabularies cannot collide.

   THE ATMOSPHERE. The page is one light field with five possible "grounds" —
   the same five backgrounds the app offers — and the visitor chooses one. A
   ground is seven custom properties on <body>: the base gradient (--g0, --g1),
   the three blobs (--b1, --b2, --b3), the accent fill (--ac) and the text-safe
   accent (--acText, 4.5:1 or better on the light base). They are REGISTERED
   with @property as <color>, which is what lets the base gradient, the blobs
   and every accent interpolate over 1.2 s when the ground changes; a browser
   without @property (Firefox < 128) simply switches instantly. This is why the
   whole page reads the ground through custom properties and never through a
   literal colour: one change, everything follows.

   The design has a single, committed look — light only. There is no dark
   variant, so the page declares color-scheme: light and the shared dark tokens
   above never apply to it.

   MOTION. Everything is transform/opacity. The reveal's hidden state is applied
   by script (never by the markup), parallax writes a transform on [data-par]
   wrappers only, the two scroll crossfades write one --step property that
   opacity and a width consume, and the reduced-motion block at the end collapses
   every animation and transition in this vocabulary to ~0 ms.
   ========================================================================== */

/* --- Registered ground properties (so colours interpolate) ---------------- */
@property --g0 { syntax: '<color>'; inherits: true; initial-value: #EAF8F3; }
@property --g1 { syntax: '<color>'; inherits: true; initial-value: #E3EEF9; }
@property --b1 { syntax: '<color>'; inherits: true; initial-value: #B6ECDC; }
@property --b2 { syntax: '<color>'; inherits: true; initial-value: #BFDCF7; }
@property --b3 { syntax: '<color>'; inherits: true; initial-value: #FFF1DA; }
@property --ac { syntax: '<color>'; inherits: true; initial-value: #2BB39A; }
@property --acText { syntax: '<color>'; inherits: true; initial-value: #0F766E; }

/* --- Tokens --------------------------------------------------------------- */
body.lh {
  /* Default ground: "Turquoise mist". site.js overwrites these seven on <body>
     when the visitor picks another ground or a stored choice is restored. */
  --g0: #EAF8F3;
  --g1: #E3EEF9;
  --b1: #B6ECDC;
  --b2: #BFDCF7;
  --b3: #FFF1DA;
  --ac: #2BB39A;
  --acText: #0F766E;

  /* Neutrals, from the spec. */
  --lh-ink: #1C2230;
  --lh-ink-2: #5A6274;
  --lh-glass: rgba(255, 255, 255, .5);
  --lh-glass-line: rgba(255, 255, 255, .9);
  --lh-ease: cubic-bezier(.2, .8, .2, 1);
  --lh-t: 1.2s;                 /* the ground transition; 0s while restoring */
  --lh-rhythm: clamp(100px, 12vw, 160px);
  --lh-gutter: clamp(20px, 4vw, 40px);

  --lh-display: Literata, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
  --lh-body: "Albert Sans", -apple-system, BlinkMacSystemFont, "SF Pro Text", ui-sans-serif, "Segoe UI", Roboto, sans-serif;

  /* The shared tokens the few shared rules on this page still read (focus
     ring, selection, the skip link) follow the ground too. */
  --ink: var(--lh-ink);
  --ink-2: var(--lh-ink-2);
  --accent: var(--ac);
  --accent-deep: var(--acText);
  --accent-text: var(--acText);
  --focus: var(--acText);
  --line: rgba(28, 34, 48, .1);
  --card-solid: #FFFFFF;

  background: #F7F9FA;          /* page fallback under the fixed atmosphere */
  color: var(--lh-ink);
  font-family: var(--lh-body);
  font-size: 17px;
  line-height: 1.6;
  transition:
    --g0 var(--lh-t) ease, --g1 var(--lh-t) ease,
    --b1 var(--lh-t) ease, --b2 var(--lh-t) ease, --b3 var(--lh-t) ease,
    --ac var(--lh-t) ease, --acText var(--lh-t) ease;
}
/* The shared stylesheet paints the Csillagpor canvas on body::before and
   bumps the body size to 18px on wide screens; neither belongs on this page. */
body.lh::before { content: none; }
@media (min-width: 760px) { body.lh { font-size: 17px; } }
/* :where(body.lh) strips the "body.lh" half of this selector's specificity
   to zero, leaving only the plain-type weight of "a" (0,0,1) — enough to still
   beat the shared a{color:var(--accent-text)} rule near the top of this file
   (same weight, later in the cascade), but no longer enough to beat a
   single-class component rule like .lh-btn--primary or .lh-footer a further
   down. Without the :where(), "body.lh a" carries TWO type selectors (body,
   a) plus the .lh class — specificity (0,1,2) — which silently beat every
   single-class override below it. That is why the beta button painted a
   solid dark pill with dark-on-dark (invisible) text: measured in the
   browser, getComputedStyle on .lh-btn--primary returned color:#1C2230 (the
   inherited ink), not the #FFFFFF the rule declares. */
:where(body.lh) a { color: inherit; text-decoration: none; }
body.lh h1, body.lh h2, body.lh h3, body.lh p, body.lh figure { margin: 0; }

.lh-page { position: relative; min-height: 100vh; animation: lh-bloom 1.4s ease both; }

/* --- Atmosphere ----------------------------------------------------------- */
.lh-atmo {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: linear-gradient(160deg, var(--g0), var(--g1));
  pointer-events: none;
}
.lh-blob { position: absolute; border-radius: 50%; }
.lh-blob-1 { left: -10%;  top: -20%;    width: 70vmax; height: 70vmax; background: var(--b1); filter: blur(90px);  opacity: .75; animation: lh-drift1 26s ease-in-out infinite alternate; }
.lh-blob-2 { right: -15%; top: 20%;     width: 64vmax; height: 64vmax; background: var(--b2); filter: blur(100px); opacity: .7;  animation: lh-drift2 32s ease-in-out infinite alternate; }
.lh-blob-3 { left: 20%;   bottom: -30%; width: 60vmax; height: 60vmax; background: var(--b3); filter: blur(110px); opacity: .6;  animation: lh-drift3 38s ease-in-out infinite alternate; }

/* --- Keyframes ------------------------------------------------------------ */
@keyframes lh-bloom     { from { opacity: 0; } to { opacity: 1; } }
@keyframes lh-drift1    { from { transform: translate(-6%, -4%) scale(1); }    to { transform: translate(6%, 5%) scale(1.12); } }
@keyframes lh-drift2    { from { transform: translate(5%, 6%) scale(1.08); }   to { transform: translate(-6%, -5%) scale(.96); } }
@keyframes lh-drift3    { from { transform: translate(0, 4%) scale(.94); }     to { transform: translate(-4%, -6%) scale(1.1); } }
@keyframes lh-riseIn    { from { opacity: 0; transform: translateY(26px); }    to { opacity: 1; transform: none; } }
/* No filter here on purpose (the README's own spec used a blur(6px)->0
   step). Confirmed in real Safari, twice, that animating `filter: blur()` on
   an inline-block that contains text glyphs leaves descenders (the "p" in
   "place.") permanently clipped after the animation settles — Safari sizes
   the layer's backing store from the blurred frame and does not grow it back
   out for the sharp one, and neither `will-change` nor extra padding on the
   ancestor changed that, because nothing is actually being clipped by layout;
   the RASTER itself is short. Dropping filter removes the failure mode
   entirely; opacity + a small rise still reads as the same entrance. */
@keyframes lh-wordRise  { from { opacity: 0; transform: translateY(.6em); } to { opacity: 1; transform: none; } }
@keyframes lh-heroFrame { from { opacity: 0; transform: translateY(40px) scale(.965); } to { opacity: 1; transform: none; } }
@keyframes lh-floatY    { from { transform: translateY(0) rotate(0deg); }      to { transform: translateY(-12px) rotate(-1.2deg); } }
@keyframes lh-floatY2   { from { transform: translateY(0); }                   to { transform: translateY(-9px); } }
@keyframes lh-glowPulse { from { transform: scale(.9); opacity: .55; }         to { transform: scale(1.15); opacity: .9; } }
@keyframes lh-spinSlow  { to { transform: rotate(360deg); } }
@keyframes lh-watchpulse {
  0%   { transform: translate(-50%, -50%) scale(.86); opacity: 0; }
  35%  { opacity: .8; }
  100% { transform: translate(-50%, -50%) scale(1.25); opacity: 0; }
}

/* --- Layout primitives ---------------------------------------------------- */
.lh-wrap { max-width: 1200px; margin: 0 auto; padding: 0 var(--lh-gutter); }
.lh-section {
  max-width: 1200px;
  margin: var(--lh-rhythm) auto 0;
  padding: 0 var(--lh-gutter);
  /* The sticky header is 64px tall; an anchored section must not start under it. */
  scroll-margin-top: 80px;
}
.lh-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
}
/* Owner feedback (2026-09-09): the screenshots read too small to make out —
   measured, an even 1:1 split left a 1200px section's media column around
   450-530px, and 03's three-way row inside that column at 150-200px each. The
   copy column (a three-line lede plus a short fact list) does not need that
   much width to read comfortably, so above 900px it gets a narrower, capped
   share and the media column takes the rest — the same "copy narrow, media
   wide" split the v3 chaptered page used for the same reason. .lh-copy's own
   46ch measure means the text does not stretch to fill the extra room it no
   longer has. Because auto-placement respects `order`, this still lands copy
   in the first track and media in the second on every section, including 02
   and 04 where .lh-order-1/2 swap their visual position. */
@media (min-width: 900px) {
  .lh-grid { grid-template-columns: minmax(260px, .78fr) minmax(0, 1.35fr); }
}
/* 05's media is itself a two-column composition (phone + 2x2 Watch grid), so
   its text/media split needs more room per column than the other sections:
   measured at 700px, two 304px columns left the Watch grid 63px wide and the
   page 29px too wide. Below ~880px of content width it drops to one column. */
.lh-grid--voice { gap: clamp(32px, 5vw, 64px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
@media (min-width: 900px) {
  .lh-grid--voice { grid-template-columns: minmax(260px, .78fr) minmax(0, 1.35fr); }
}
/* Text first in DOM order on 02 and 04, so a one-column layout always reads
   the heading before the picture; on two columns the image takes the left. */
.lh-order-1 { order: 1; }
.lh-order-2 { order: 2; }

/* --- Header --------------------------------------------------------------- */
.lh-header {
  position: sticky;
  top: 0;
  z-index: 40;
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  background: rgba(255, 255, 255, .42);
  border-bottom: 1px solid rgba(255, 255, 255, .7);
}
.lh-header .lh-wrap { min-height: 64px; display: flex; align-items: center; gap: 18px; }
.lh-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--lh-ink);
  font-family: var(--lh-display);
  font-weight: 500;
  font-size: 18px;
  letter-spacing: -.01em;
  margin-right: auto;
  white-space: nowrap;
}
.lh-brand img { width: 28px; height: 28px; border-radius: 8px; display: block; }
.lh-nav { display: flex; align-items: center; gap: 4px; font-size: 14.5px; font-weight: 500; }
.lh-nav a {
  padding: 8px 12px;
  border-radius: 999px;
  color: var(--lh-ink);
  transition: background .25s;
}
.lh-nav a:hover { background: rgba(255, 255, 255, .7); }
.lh-nav .lh-join {
  margin-left: 8px;
  padding: 9px 16px;
  color: #FFFFFF;
  background: var(--lh-ink);
  white-space: nowrap;
  transition: opacity .25s;
}
.lh-nav .lh-join:hover { background: var(--lh-ink); opacity: .85; }

/* --- Type ----------------------------------------------------------------- */
.lh-eyebrow {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--acText);
}
.lh-eyebrow--quiet { color: var(--lh-ink-2); }
.lh-h1 {
  font-family: var(--lh-display);
  font-weight: 400;
  font-size: clamp(38px, 6vw, 78px);
  line-height: 1.04;
  letter-spacing: -.025em;
  text-wrap: balance;
  max-width: 18ch;
  font-variation-settings: "opsz" 72;
  /* 1.04 is the README's own value and it is tighter than Literata's descender
     (measured: the "p" in "place." on the last line was getting clipped in
     Safari — reported twice, still happening after the will-change fix below,
     which only addressed a possible compositor-layer cause). This box-model
     fix is the one guaranteed to hold in every renderer regardless of cause:
     0.18em of real padding under the text is more than the ~0.02em the tight
     leading itself provides, so the descender always has room whether or not
     any animation is involved. It only widens the gap to the lede text
     slightly (already a 22px grid gap) and does not touch the 1.04 leading
     between the three lines themselves. */
  padding-bottom: .18em;
}
.lh-h1 em {
  font-style: italic;
  font-weight: 300;
  color: var(--acText);
}
.lh-h2 {
  font-family: var(--lh-display);
  font-weight: 400;
  font-size: clamp(30px, 3.6vw, 50px);
  line-height: 1.08;
  letter-spacing: -.02em;
  text-wrap: balance;
  font-variation-settings: "opsz" 48;
}
.lh-h2--beta { font-size: clamp(32px, 4.4vw, 60px); line-height: 1.06; max-width: 20ch; }
.lh-h3 {
  font-family: var(--lh-display);
  font-weight: 400;
  font-size: clamp(24px, 2.4vw, 34px);
  line-height: 1.12;
  letter-spacing: -.015em;
  text-wrap: balance;
  font-variation-settings: "opsz" 30;
}
.lh-h3--card { font-size: 22px; line-height: 1.2; letter-spacing: -.01em; font-variation-settings: "opsz" 22; }
.lh-lede {
  color: var(--lh-ink-2);
  font-size: clamp(16px, 1.3vw, 18px);
  max-width: 46ch;
  text-wrap: pretty;
}
.lh-lede--hero { font-size: clamp(17px, 1.5vw, 20px); line-height: 1.55; max-width: 52ch; }
.lh-lede--sm { font-size: 16px; }
.lh-lede--beta { font-size: clamp(16px, 1.4vw, 19px); max-width: 44ch; }
.lh-note { font-size: 14px; color: var(--lh-ink-2); }
.lh-caption { font-size: 13px; color: var(--lh-ink-2); }
.lh-caption--hero { margin: clamp(56px, 8vw, 90px) auto 0; max-width: 60ch; }

.lh-copy { display: grid; gap: 18px; align-content: start; }
.lh-copy--tight { gap: 14px; }
.lh-copy--intro { gap: 14px; max-width: 60ch; margin-bottom: clamp(32px, 4vw, 48px); }

/* The fact list: a 14x2 accent dash before each line, never a bullet glyph. */
.lh-facts {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
  font-size: 16px;
  color: var(--lh-ink-2);
}
.lh-facts li { display: flex; gap: 12px; align-items: baseline; }
.lh-facts li::before {
  content: "";
  flex: 0 0 auto;
  width: 14px;
  height: 2px;
  border-radius: 2px;
  background: var(--ac);
  translate: 0 -4px;
}

/* --- Buttons -------------------------------------------------------------- */
.lh-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 6px; }
.lh-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 24px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 16px;
}
.lh-btn--primary {
  background: var(--lh-ink);
  color: #FFFFFF;
  box-shadow: 0 14px 30px -14px rgba(28, 34, 48, .5);
  transition: transform .25s, box-shadow .25s;
}
.lh-btn--primary:hover { transform: translateY(-2px); box-shadow: 0 20px 36px -14px rgba(28, 34, 48, .55); }
.lh-btn--wide { padding: 15px 28px; }
.lh-btn--glass {
  background: rgba(255, 255, 255, .55);
  border: 1px solid rgba(255, 255, 255, .9);
  color: var(--lh-ink);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background .25s;
}
.lh-btn--glass:hover { background: rgba(255, 255, 255, .85); }

/* --- Hero ----------------------------------------------------------------- */
.lh-hero {
  max-width: 1200px;
  margin: 0 auto;
  padding: clamp(56px, 8vw, 110px) var(--lh-gutter) 0;
  text-align: center;
}
.lh-hero-copy {
  display: grid;
  justify-items: center;
  gap: 22px;
  animation: lh-riseIn 1.1s .2s var(--lh-ease) both;
}
.lh-mark { position: relative; display: grid; place-items: center; }
.lh-mark-glow {
  position: absolute;
  width: 180px;
  height: 180px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 214, 160, .85), rgba(255, 214, 160, 0) 70%);
  filter: blur(10px);
  animation: lh-glowPulse 4.5s ease-in-out infinite alternate;
  pointer-events: none;
}
/* .lh-mark-ring (the slow-spinning dashed circle behind the icon) was removed
   from the markup on the owner's request (2026-09-09) — kept out of the CSS
   too rather than left as dead weight; lh-spinSlow stays, the ground chips'
   dot still uses it. */
.lh-mark-icon {
  position: relative;
  width: 72px;
  height: 72px;
  border-radius: 18px;
  box-shadow: 0 18px 40px -18px rgba(28, 34, 48, .4);
  animation: lh-floatY2 5s ease-in-out infinite alternate;
}
/* Each word of the headline rises out of a blur on its own delay: the eight
   delays of the spec, indexed by --w in the markup. */
/* will-change forces the compositor layer to exist BEFORE the animation
   starts, rather than being promoted mid-animation. Without it, Safari has
   been observed to size the layer from the first (blurred, offset) frame and
   fail to grow it back out for the settled frame, clipping the descender of
   whichever word lands last (measured: "place." on this h1) even though
   every ancestor here has overflow: visible. */
.lh-word { display: inline-block; animation: lh-wordRise .9s var(--lh-ease) both; animation-delay: var(--wd, .35s); will-change: opacity, transform; }
.lh-word[style*="--w: 1"] { --wd: .35s; }
.lh-word[style*="--w: 2"] { --wd: .45s; }
.lh-word[style*="--w: 3"] { --wd: .55s; }
.lh-word[style*="--w: 4"] { --wd: .68s; }
.lh-word[style*="--w: 5"] { --wd: .78s; }
.lh-word[style*="--w: 6"] { --wd: .9s; }
.lh-word[style*="--w: 7"] { --wd: 1s; }
.lh-word[style*="--w: 8"] { --wd: 1.1s; }

/* The ground picker. */
.lh-grounds {
  margin: clamp(36px, 5vw, 60px) auto 0;
  display: grid;
  justify-items: center;
  gap: 12px;
  animation: lh-riseIn 1.1s .5s var(--lh-ease) both;
}
.lh-grounds-caption { font-size: 13px; color: var(--lh-ink-2); }
.lh-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.lh-chip {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 6px 14px 6px 6px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .9);
  background: rgba(255, 255, 255, .45);
  color: var(--lh-ink);
  font: inherit;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  box-shadow: none;
  transition: background .3s, box-shadow .3s, transform .2s;
}
.lh-chip:hover { transform: translateY(-1px); }
.lh-chip[aria-pressed="true"] {
  background: rgba(255, 255, 255, .9);
  box-shadow: 0 10px 24px -12px rgba(28, 34, 48, .45), 0 0 0 2px var(--ac);
}
.lh-chip-dot {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: conic-gradient(from 0deg, var(--c1), var(--c2), var(--c3), var(--c1));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7);
  animation: lh-spinSlow 14s linear infinite;
  animation-play-state: paused;
}
.lh-chip[aria-pressed="true"] .lh-chip-dot { animation-play-state: running; }

/* The hero composition: iPad full width, iPhone over its lower right. */
.lh-hero-comp {
  position: relative;
  margin: clamp(40px, 6vw, 72px) auto 0;
  max-width: 1040px;
  animation: lh-heroFrame 1.4s .9s var(--lh-ease) both;
}
.lh-hero-phone { position: absolute; right: -2%; bottom: -14%; width: 17%; }

/* --- Frames: the soft card every real capture sits in --------------------- */
/* White ground, clipped corners, one soft shadow plus a hairline of white. No
   device bezels, no borders: the pixels are the content. */
.lh-frame {
  position: relative;
  display: block;
  border-radius: 20px;
  overflow: hidden;
  background: #FFFFFF;
  box-shadow: 0 50px 100px -40px rgba(28, 34, 48, .4), 0 0 0 1px rgba(255, 255, 255, .8);
}
.lh-frame img { display: block; width: 100%; height: auto; }
.lh-frame--xl    { border-radius: 22px; box-shadow: 0 60px 120px -50px rgba(28, 34, 48, .45), 0 0 0 1px rgba(255, 255, 255, .8); }
.lh-frame--md    { box-shadow: 0 40px 80px -30px rgba(28, 34, 48, .4), 0 0 0 1px rgba(255, 255, 255, .8); }
.lh-frame--float { box-shadow: 0 40px 80px -30px rgba(28, 34, 48, .45), 0 0 0 1px rgba(255, 255, 255, .8); animation: lh-floatY var(--float, 6s) ease-in-out infinite alternate; }
.lh-frame--phone { border-radius: 22px; }
.lh-frame--phone-lg { border-radius: 26px; box-shadow: 0 50px 100px -40px rgba(28, 34, 48, .45), 0 0 0 1px rgba(255, 255, 255, .8); }
.lh-frame--sm    { border-radius: 18px; }
.lh-frame--card  { box-shadow: 0 24px 50px -28px rgba(28, 34, 48, .4); }
.lh-frame--r16   { border-radius: 16px; }
.lh-frame--r14   { border-radius: 14px; }
.lh-journals-media .lh-frame--card { box-shadow: 0 30px 60px -30px rgba(28, 34, 48, .4); }
.lh-frame--lift  { transition: transform .5s var(--lh-ease); }
.lh-frame--lift:hover { transform: translateY(-4px); }
.lh-frame--watch {
  width: 100%;
  max-width: 140px;
  border-radius: 28px;
  background: #14161F;
  box-shadow: 0 30px 60px -30px rgba(28, 34, 48, .5);
}

/* --- 01 / 04: a large frame with a floating secondary over its corner ------ */
.lh-comp { position: relative; }
.lh-comp--writing    { padding: 0 14% 10% 0; }
.lh-comp--references { padding: 0 16% 8% 0; }
.lh-comp-float { position: absolute; right: 0; bottom: 0; width: 30%; }
.lh-comp-float--wide { width: 34%; }

/* --- 02: the resting -> lifted step -------------------------------------- */
/* --step is written by site.js (0..1) from the block's scroll position. With
   scripting off the LIFTED frame stands, because that is the frame that shows
   the feature; the resting one is context the crossfade needs. */
.lh-step { display: grid; gap: 14px; }
.lh-step-frame { position: relative; }
.lh-step-top { position: absolute; inset: 0; opacity: var(--step, 0); }
html:not(.js) .lh-step-top { opacity: 1; }
.lh-rail { display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--lh-ink-2); padding: 0 6px; }
.lh-rail-track { flex: 1; height: 3px; border-radius: 3px; background: rgba(28, 34, 48, .12); overflow: hidden; }
.lh-rail-fill { display: block; height: 100%; width: calc(var(--step, 0) * 100%); background: var(--ac); }
html:not(.js) .lh-rail-fill { width: 100%; }

/* --- 03: three frames, staggered ------------------------------------------ */
.lh-time { display: grid; grid-template-columns: 1fr 1fr 1.4fr; gap: 16px; align-items: start; }
.lh-time-b { margin-top: 18%; }
.lh-time-c { margin-top: 40%; }
.lh-time .lh-frame--sm { border-radius: 18px; }

/* --- Glass panels --------------------------------------------------------- */
.lh-glass {
  background: var(--lh-glass);
  border: 1px solid var(--lh-glass-line);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.lh-journals {
  margin-top: clamp(40px, 5vw, 72px);
  padding: clamp(24px, 3.5vw, 44px);
  border-radius: 28px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
}
.lh-journals-media { display: grid; gap: 14px; }
.lh-journals-mac { margin-left: 18%; }

/* --- 05: the voice player and the Watch grid ------------------------------ */
.lh-voice {
  display: grid;
  grid-template-columns: minmax(150px, 220px) minmax(0, 1fr);
  gap: clamp(20px, 3vw, 36px);
  align-items: center;
}
.lh-voice > * { min-width: 0; }
.lh-watch { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; width: 100%; }
/* min-width: 0 on the cells and their frame: a percentage-width <img> still
   contributes its intrinsic 416px to the grid's automatic minimum, which is
   what let the Watch frames overflow a narrow column. */
.lh-watch-cell { display: grid; gap: 8px; justify-items: center; min-width: 0; }
.lh-watch-cell > .lh-frame { min-width: 0; }
.lh-watch-cell--down { margin-top: 22px; }
.lh-watch-cell figcaption { font-size: 12.5px; color: var(--lh-ink-2); }
/* Geometry measured on the 416x496 frame: the ring's centre is at (208, 337) and
   its outer radius 108px, i.e. 50%/68% of the frame with a ~52% diameter. */
.lh-watch-pulse {
  position: absolute;
  left: 50%;
  top: 68%;
  width: 60%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid #E9A6A6;
  opacity: 0;
  pointer-events: none;
  animation: lh-watchpulse 2.4s var(--lh-ease) infinite;
}

/* --- 06: three glass cards ------------------------------------------------ */
.lh-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
.lh-card {
  padding: 24px;
  border-radius: 24px;
  display: grid;
  gap: 12px;
  align-content: start;
  transition: transform .4s var(--lh-ease), background .4s;
}
.lh-card:hover { transform: translateY(-4px); background: rgba(255, 255, 255, .7); }
.lh-card-body { font-size: 15px; color: var(--lh-ink-2); text-wrap: pretty; }
.lh-card-figure { margin-top: 6px; }
.lh-card-figure--inset { margin-left: 12%; }
.lh-card-figure--inset-r { margin-right: 12%; }

/* --- 07: the developer's note --------------------------------------------
   The one first-person passage on the site, so it is set as a letter rather
   than as another feature chapter: one narrower measure, left-aligned inside
   the same glass the other panels use, and signed. The measure is 62ch on the
   paragraphs themselves (not on the panel) so the panel keeps the page's full
   rhythm while the prose stays at a readable line length. */
.lh-letter {
  max-width: 820px;
  margin-inline: auto;
  padding: clamp(28px, 4vw, 56px) clamp(24px, 4vw, 64px);
  border-radius: 32px;
  display: grid;
  gap: 18px;
}
.lh-h2--letter { font-size: clamp(28px, 3.2vw, 42px); }
.lh-letter-body { display: grid; gap: 16px; }
.lh-letter-body p {
  max-width: 62ch;
  color: var(--lh-ink-2);
  font-size: clamp(16px, 1.25vw, 17.5px);
  line-height: 1.65;
  text-wrap: pretty;
}
/* The signature: the display face for the name, the body face for the line
   under it, so it reads as a hand at the bottom of a letter and not as another
   heading competing with the H2 above. */
.lh-sign {
  margin-top: 4px;
  font-family: var(--lh-display);
  font-size: 18px;
  letter-spacing: -.01em;
  color: var(--lh-ink);
}
.lh-sign span {
  display: block;
  margin-top: 2px;
  font-family: var(--lh-body);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--lh-ink-2);
}

/* --- 07: beta ------------------------------------------------------------- */
.lh-beta { text-align: center; }
.lh-beta-panel {
  display: grid;
  justify-items: center;
  gap: 20px;
  padding: clamp(40px, 6vw, 80px) clamp(24px, 4vw, 56px);
  border-radius: 32px;
}
.lh-beta-icon { width: 56px; height: 56px; border-radius: 14px; box-shadow: 0 14px 30px -14px rgba(28, 34, 48, .4); }

/* --- Footer --------------------------------------------------------------- */
.lh-footer {
  max-width: 1200px;
  margin: clamp(60px, 8vw, 100px) auto 0;
  padding: 28px var(--lh-gutter) 44px;
  border-top: 1px solid rgba(28, 34, 48, .1);
  display: flex;
  flex-wrap: wrap;
  gap: 12px 28px;
  align-items: baseline;
  font-size: 14.5px;
  color: var(--lh-ink-2);
}
.lh-footer nav { display: flex; flex-wrap: wrap; gap: 12px 24px; margin-right: auto; }
.lh-footer a { color: var(--lh-ink-2); }
.lh-footer a:hover { color: var(--lh-ink); }

/* --- Reveal on scroll ----------------------------------------------------- */
/* site.js adds .lh-hide to a [data-reveal] block that is below the fold at
   load (never to one already in view), and swaps it for .lh-in when it
   crosses into view: the block rises 22px, its direct children 18px, staggered
   by --d / --dk. A child that also carries parallax animates opacity only —
   its transform belongs to the parallax. Gated on html.js so a page without
   script never hides anything. */
html.js .lh-hide { opacity: 0; transform: translateY(22px); }
html.js .lh-hide > :not(img) { opacity: 0; transform: translateY(18px); }
html.js .lh-hide > [data-par] { transform: none; }
.lh-in { opacity: 1; transform: none; transition: opacity 1s var(--lh-ease) var(--d, 0ms), transform 1s var(--lh-ease) var(--d, 0ms); }
.lh-in > :not(img) { opacity: 1; transform: none; transition: opacity .9s var(--lh-ease) var(--dk, 0ms), transform .9s var(--lh-ease) var(--dk, 0ms); }
.lh-in > [data-par] { transition: opacity .9s var(--lh-ease) var(--dk, 0ms); }

/* --- Breakpoints ---------------------------------------------------------- */
@media (max-width: 720px) {
  .lh-hide-narrow { display: none !important; }
  .lh-time { grid-template-columns: 1fr 1fr; }
  .lh-time-c { grid-column: 1 / -1; margin-top: 0; }
  .lh-comp--writing, .lh-comp--references { padding: 0; }
  .lh-comp .lh-comp-float { display: none; }
}
@media (max-width: 440px) {
  .lh-voice { grid-template-columns: 1fr; justify-items: center; }
  .lh-voice > :first-child { width: min(100%, 200px); }
  .lh-voice > .lh-watch { justify-self: stretch; }
}

/* --- Reduced motion ------------------------------------------------------- */
/* Every animation and transition in this vocabulary collapses to ~0 ms, the
   ground still changes (instantly), the crossfades stand on their final frame
   and nothing is ever hidden. Parallax is switched off in site.js. */
@media (prefers-reduced-motion: reduce) {
  body.lh, body.lh *, body.lh *::before, body.lh *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  body.lh { --lh-t: 0s; }
  html.js .lh-hide, html.js .lh-hide > * { opacity: 1; transform: none; }
  .lh-step-top { opacity: 1; }
  .lh-rail-fill { width: 100%; }
  .lh-watch-pulse { display: none; }
  .lh-frame--lift:hover, .lh-card:hover, .lh-btn--primary:hover, .lh-chip:hover { transform: none; }
}
