/* ============================================================================
   hetki — idealism.co/hetki
   ----------------------------------------------------------------------------
   A single long page: full-bleed photographic hero, near-white ground, a giant
   display headline over a narrow label column and two body columns, an
   annotated interface with pressable marks, moment cards with inline audio,
   spec accordions, and a persistent pill.

   Two typefaces, one weight each. One accent — ochre — and it only ever marks
   something active, live or hovered. Never pure white, never pure black.

   CONTRAST: every text colour here clears WCAG AA (4.5:1 normal, 3:1 large)
   against the ground it sits on. The ratios are noted where they are tight.
   ========================================================================== */

@font-face {
  font-family: "Canela";
  src: url("../../assets/fonts/Canela-Regular-Web.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("../../assets/fonts/IBMPlexSans-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}

:root {
  --paper:      #f4f2ee;
  --paper-deep: #eae6df;
  --dark:       #16120e;

  --ink:        #1d1712;                    /* body ink. 14.9:1 on paper */
  --ink-60:     rgba(29, 23, 18, 0.66);     /* secondary type. 5.44:1 — was 0.60,
                                               which measured 4.478:1 and missed
                                               AA by 0.02 */
  --ink-mute:   #6b6660;                    /* the SMALL type colour. 5.08:1.
                                               --ink-40 measured 2.49:1 and was
                                               being used for real words. */
  --ink-40:     rgba(29, 23, 18, 0.40);     /* DECORATIVE ONLY — rules, tracks,
                                               fills. Never set type in it. */
  --ink-14:     rgba(29, 23, 18, 0.14);     /* hairlines */

  --cream:      #e6d9c8;                    /* type on dark. 11.8:1 */
  --cream-60:   rgba(230, 217, 200, 0.60);  /* secondary on dark. 5.45:1 */
  --cream-16:   rgba(230, 217, 200, 0.16);  /* hairlines on dark */

  --ochre:      #c99e63;                    /* the accent. Blocks, rules, live
                                               states, and type ON DARK (7.6:1) */
  --ochre-ink:  #8a6532;                    /* the accent as TYPE ON PAPER.
                                               Raw ochre on paper is 1.9:1 and
                                               unreadable; this is 4.7:1 */
  --on-ochre:   #16120e;                    /* type on an ochre block. 7.6:1 */
  --on-ochre-2: #382b1c;                    /* the recessive step there. 5.6:1 */

  --display: "Canela", Georgia, serif;
  --text: "IBM Plex Sans", system-ui, sans-serif;

  /* The type ladder. These clamps are checked against their COMPUTED value at
     1280 / 1440 / 1920 — an unreachable ceiling is a bug, not a scale. */
  --t-hero:    clamp(2.75rem, 6.4vw, 5.5rem);
  --t-display: clamp(2.25rem, 5vw, 4.5rem);
  --t-label:   clamp(1.05rem, 0.9rem + 0.45vw, 1.3rem);      /* ~19-20.8px */
  --t-body:    clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem);    /* ~17-19px */
  --t-small:   0.9375rem;

  --gutter: clamp(1.5rem, 4.5vw, 4rem);
  --band:   clamp(4.5rem, 9vw, 7.5rem);
  --measure: 34rem;

  --header-h: 7rem;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; scroll-padding-top: var(--header-h); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--text);
  font-size: var(--t-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* height:auto matters: the width/height ATTRIBUTES on every img are
   presentational hints that map to CSS width/height. Author CSS overriding
   only the width leaves the attribute height standing, and the picture
   squeezes to fit both — which is exactly what happened to the interface
   render (measured 809x959 against a 1.67 native aspect). */
img { max-width: 100%; height: auto; display: block; }

.wrap { max-width: 90rem; margin-inline: auto; padding-inline: var(--gutter); }

/* Focus is an active state, so it spends the accent. The ring has to change
   colour with the ground under it or it disappears on the ochre block. */
:focus-visible { outline: 2px solid var(--ochre-ink); outline-offset: 3px; }
.section--dark :focus-visible,
.foot :focus-visible,
.hero :focus-visible,
.header :focus-visible { outline-color: var(--ochre); }        /* 7.6:1 on dark */
.band__block :focus-visible,
.pill:focus-visible { outline-color: var(--on-ochre); }        /* 7.6:1 on ochre */

::selection { background: rgba(201, 158, 99, 0.35); }

/* the skip link: invisible until a keyboard asks for it */
.skip {
  position: fixed;
  top: 1rem; left: 50%;
  translate: -50% -400%;
  z-index: 90;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink-14);
  border-radius: 999px;
  padding: 0.7rem 1.3rem;
  font-size: var(--t-small);
  letter-spacing: 0.08em;
  text-decoration: none;
  transition: translate 200ms ease-out;
}
.skip:focus-visible { translate: -50% 0; }

.sr { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }


/* ── type ────────────────────────────────────────────────────────────────── */

.display {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--t-display);
  line-height: 1.08;
  letter-spacing: -0.005em;
  margin: 0;
  max-width: 18ch;
}

.label {
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  color: var(--ink-60);
  margin: 0;
}

/* every label carries a short hairline that draws itself in when its section
   arrives — the ink redraws itself, which is the plugin's signature reveal */
.label::before {
  content: "";
  display: block;
  width: 2.75rem;
  height: 1px;
  background: currentColor;
  opacity: 0.45;
  margin-bottom: 0.8rem;
  transform-origin: left;
}
.js [data-reveal].reveal .label::before {
  transform: scaleX(0);
  transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1) 120ms;
}
.js [data-reveal].reveal.is-in .label::before { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal].reveal .label::before { transform: none; transition: none; }
}

.lede { max-width: var(--measure); margin: 0; }
.muted { color: var(--ink-60); }
.tiny { font-size: var(--t-small); color: var(--ink-mute); }

.ulink {
  --line: currentColor;
  position: relative;
  display: inline-block;
  font-size: var(--t-label);
  letter-spacing: 0.1em;
  color: inherit;
  text-decoration: none;
  padding-bottom: 4px;
  transition: color 140ms ease-out;
}

/* the underline is drawn, not set: on hover it runs a few pixels past the
   word, the way a pen crosses a corner. A line that stops exactly on the
   word was drawn by a machine — the plugin's own hover vocabulary. */
.ulink::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--line);
  transition: left 140ms ease-out, right 140ms ease-out;
}
.ulink:hover { color: var(--ochre-ink); --line: var(--ochre); }
.ulink:hover::after { left: -1px; right: -4px; }
.section--dark .ulink:hover,
.hero .ulink:hover { color: var(--ochre); --line: var(--ochre); }

/* an arrow in a link leans the way it points */
.ulink .arr { display: inline-block; transition: translate 140ms ease-out; }
.ulink:hover .arr { translate: 3px 0; }
.ulink:hover .arr--down { translate: 0 3px; }


/* ── header ──────────────────────────────────────────────────────────────── */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.5rem var(--gutter);
  color: var(--cream);
  transition: background-color 200ms ease-out, color 200ms ease-out;
}

/* The header's own scrim. It cannot depend on the hero photograph being dark
   in the top-right corner — that is a photograph nobody has taken yet. */
.header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(rgba(12, 9, 7, 0.72), rgba(12, 9, 7, 0));
  transition: opacity 200ms ease-out;
}
.header.is-stuck {
  background: var(--paper);
  color: var(--ink);
  border-bottom: 1px solid var(--ink-14);
}
.header.is-stuck::before { opacity: 0; }

/* reading down, the header steps aside; the first upward gesture brings it
   back. It never hides while anything inside it holds focus. */
.header { transition: background-color 200ms ease-out, color 200ms ease-out,
                      transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.header.is-away:not(:focus-within) { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) {
  .header { transition: background-color 200ms ease-out, color 200ms ease-out; }
  .header.is-away:not(:focus-within) { transform: none; }
}

/* the mark is tinted with currentColor — the <a> around it must not fall back
   to the browser's default link blue, or the wordmark paints blue */
.header a { color: inherit; }

.header__mark {
  display: block;                          /* an inline span has no size */
  width: 92px;
  aspect-ratio: 842 / 454;
  background-color: currentColor;
  -webkit-mask: url("../../assets/ink/wordmark.png") no-repeat center / contain;
  mask: url("../../assets/ink/wordmark.png") no-repeat center / contain;
}

.header__nav {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.4vw, 2.25rem);
  font-size: var(--t-small);
  letter-spacing: 0.06em;
}
.header__nav a { color: var(--cream-60); text-decoration: none; transition: color 140ms ease-out; }
.header__nav a:hover { color: var(--cream); }
.header.is-stuck .header__nav a { color: var(--ink-60); }
.header.is-stuck .header__nav a:hover { color: var(--ink); }

@media (max-width: 52rem) { .header__nav a { display: none; } }

.sound {
  display: inline-flex;
  align-items: center;
  background: none;
  border: 0;
  border-radius: 999px;
  padding: 0.5rem 0.4rem;
  color: inherit;
  font: inherit;
  cursor: pointer;
  opacity: 0.7;
  transition: color 140ms ease-out, opacity 140ms ease-out;
}
.sound:hover { opacity: 1; }
.sound[aria-pressed="true"] { color: var(--ochre); opacity: 1; }
.header.is-stuck .sound[aria-pressed="true"] { color: var(--ochre-ink); }
.sound[aria-disabled="true"] { opacity: 0.45; cursor: default; }

/* three bars. They move only while the sound is ON — a live state, not decor. */
.sound__bars { display: flex; align-items: flex-end; gap: 2px; height: 11px; }
.sound__bars i { width: 2px; height: 3px; background: currentColor; display: block; }
.sound[aria-pressed="true"] .sound__bars i { animation: eq 900ms ease-in-out infinite alternate; }
.sound[aria-pressed="true"] .sound__bars i:nth-child(2) { animation-delay: 150ms; }
.sound[aria-pressed="true"] .sound__bars i:nth-child(3) { animation-delay: 300ms; }
@keyframes eq { from { height: 3px; } to { height: 11px; } }
@media (prefers-reduced-motion: reduce) {
  .sound[aria-pressed="true"] .sound__bars i { animation: none; height: 7px; }
}


/* ── hero ────────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;              /* the headline sits low-left */
  background: var(--dark);
  color: var(--cream);
  overflow: hidden;
}

.hero__media { position: absolute; inset: 0; z-index: 0; }
.hero__media img,
.hero__media video { width: 100%; height: 100%; object-fit: cover; }

/* the photograph does not exist yet, so the scrim must guarantee legibility
   over WHATEVER gets shot — including a bright highlight top-right, where the
   nav sits. The right-hand stop never goes below 0.45. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    rgba(12, 9, 7, 0.80) 0%,
    rgba(12, 9, 7, 0.58) 45%,
    rgba(12, 9, 7, 0.45) 100%);
}

/* flush to the gutter at every width — a centred max-width would detach the
   headline from the left edge on a wide screen while the photo stays full-bleed */
.hero__inner {
  position: relative;
  z-index: 1;
  width: 100%;
  padding: 10rem var(--gutter) clamp(3.5rem, 8vh, 6rem);
}

.hero__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--t-hero);
  line-height: 1.06;
  margin: 0 0 1.5rem;
  max-width: 16ch;
}
.hero__sub { max-width: 40ch; margin: 0 0 2rem; color: var(--cream-60); }
.hero__scroll { color: var(--cream); --line: var(--cream-60); }


/* ── photographs ─────────────────────────────────────────────────────────── */

/* the band's picture fills its half of the grid, whatever its native ratio */
.band > img { width: 100%; height: 100%; object-fit: cover; }

/* story photographs sit between the paragraphs, full column width */
.story__plate { width: 100%; height: auto; margin-block: 2.5rem; }

/* the full-bleed breaths between sections. Each is a <figure> holding one
   photograph. Where the browser supports scroll-driven animations, the image
   drifts slowly inside its frame as the page moves past it — depth, scrubbed
   by the visitor's own scrolling, so it never moves on its own. Everywhere
   else (and under prefers-reduced-motion) it is simply a picture. */
.breath { display: block; width: 100%; margin: 0; overflow: hidden; }
.breath img { display: block; width: 100%; height: auto; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .breath img {
      scale: 1.12;                       /* head-room for the drift */
      animation: breath-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes breath-drift {
      from { translate: 0 -4.5%; }
      to   { translate: 0  4.5%; }
    }
  }
}


/* ── photo placeholders — delete the .shot rules once the photographs land ── */

.shot {
  position: relative;
  display: grid;
  place-items: center;
  background: var(--paper-deep);
  border: 1px dashed var(--ink-14);
  color: var(--ink-mute);
  text-align: center;
  padding: 2rem;
  min-height: 12rem;
}
.shot--dark { background: #1b1712; border-color: var(--cream-16); color: var(--cream-60); }
.shot__id { font-size: var(--t-small); letter-spacing: 0.14em; display: block; margin-bottom: 0.5rem; }
.shot__what { font-size: var(--t-small); max-width: 30ch; line-height: 1.5; margin: 0; }

.hero__media .shot { align-items: end; justify-items: end; text-align: right; padding: 3rem; }
.hero__media .shot__what { margin-left: auto; }


/* ── sections ────────────────────────────────────────────────────────────── */

.section { padding-block: var(--band); }
.section--tight { padding-block: calc(var(--band) * 0.55); }

/* The dark sections carry the plugin's own ground: a whisper of warm grain,
   and a candle wash you cannot quite see. Both are background paint — they sit
   under every word. The grain plate is the one tuned against the plugin's
   audit target (sigma 2.1–2.9 measured luminance; flat was rejected as "dead").

   ⚠️ CONTRAST BUDGET, measured: on the lightest possible pixel (a full speck
   under the candle's peak) cream-60 text clears AA at 4.59:1 — a 2% margin.
   Do not raise the speck alpha (the 0.10 in the feColorMatrix), the feFuncA
   slope, or the candle's 0.055 without re-computing that ratio. */
.section--dark,
.foot {
  background-color: var(--dark);
  background-image:
    /* the candle — one soft warm pool, off-centre, like the plugin face */
    radial-gradient(75% 90% at 30% 18%,
      rgba(169, 131, 92, 0.055) 0%,
      rgba(169, 131, 92, 0.030) 40%,
      rgba(169, 131, 92, 0) 72%),
    /* the grain — sparse cream specks, tiled */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='6' intercept='-4'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix values='0 0 0 0 0.89 0 0 0 0 0.81 0 0 0 0 0.66 0 0 0 0.10 0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: no-repeat, repeat;
  background-size: 100% 100%, 300px 300px;
}

.section--dark { color: var(--cream); }
.section--dark .label,
.section--dark .muted,
.section--dark .tiny { color: var(--cream-60); }

.statement__head { margin-bottom: clamp(2.5rem, 5vw, 4.5rem); max-width: 20ch; }

/* a narrow label column beside two columns of body copy */
.overview {
  display: grid;
  grid-template-columns: minmax(0, 10rem) repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: start;
}
@media (max-width: 60rem) {
  .overview { grid-template-columns: 1fr; }
  .overview .label { margin-bottom: 0.5rem; }
}
.overview p { margin: 0; }


/* the band — a picture beside a solid block of the accent */
.band {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
  min-height: 26rem;
}
@media (max-width: 60rem) { .band { grid-template-columns: 1fr; } }

.band__block {
  background: var(--ochre);
  color: var(--on-ochre);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.25rem;
  padding: clamp(2rem, 5vw, 4rem);
}
.band__block .ulink { color: var(--on-ochre); --line: rgba(22, 18, 14, 0.5); }
.band__block .ulink:hover { color: var(--on-ochre); --line: var(--on-ochre); }


/* ── the story ───────────────────────────────────────────────────────────── */

.story { display: grid; grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); gap: clamp(1.5rem, 3vw, 3rem); }
@media (max-width: 60rem) { .story { grid-template-columns: 1fr; } }
.story__body { max-width: 40rem; }
.story__body p { margin: 0 0 1.35em; }
.story__body > .shot { margin-block: 2.5rem; }


/* ── the interface, annotated ────────────────────────────────────────────── */

.iface__stage { position: relative; line-height: 0; }
.iface__stage img { width: 100%; height: auto; border: 1px solid var(--cream-16); }

/* the blank panel sits underneath; the real face is stacked exactly on top of
   it. Without JS the real face is simply visible and the blank one is not. */
.iface__blank { position: absolute; inset: 0; }
.iface__face { position: relative; z-index: 1; }
.js .iface__stage[data-stage] .iface__face { opacity: 0; }
.js .iface__stage[data-stage].is-revealed .iface__face {
  opacity: 1;
  transition: opacity 900ms ease-out;
}

.hot {
  position: absolute;
  width: 34px; height: 34px;
  margin: -17px 0 0 -17px;
  display: grid; place-items: center;
  border-radius: 999px;
  border: 1px solid var(--cream-60);
  background: rgba(22, 18, 14, 0.55);
  backdrop-filter: blur(2px);
  color: var(--cream);
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  transition: background-color 140ms ease-out, border-color 140ms ease-out, color 140ms ease-out;
}
.js #interface.reveal .hot { opacity: 0; transition: opacity 420ms ease-out; }
.js #interface.reveal.is-in .hot { opacity: 1; }
.js #interface.reveal.is-in .hot:nth-of-type(2) { transition-delay: 70ms; }
.js #interface.reveal.is-in .hot:nth-of-type(3) { transition-delay: 140ms; }
.js #interface.reveal.is-in .hot:nth-of-type(4) { transition-delay: 210ms; }
.js #interface.reveal.is-in .hot:nth-of-type(5) { transition-delay: 280ms; }
.js #interface.reveal.is-in .hot:nth-of-type(6) { transition-delay: 350ms; }
.js #interface.reveal.is-in .hot:nth-of-type(7) { transition-delay: 420ms; }
.js #interface.reveal.is-in .hot:nth-of-type(8) { transition-delay: 490ms; }
@media (prefers-reduced-motion: reduce) {
  .js #interface.reveal .hot { opacity: 1; transition: none; }
}

.hot:hover { border-color: var(--ochre); color: var(--ochre); }
.hot[aria-pressed="true"] { background: var(--ochre); border-color: var(--ochre); color: var(--on-ochre); }

/* the caption. Height is reserved for the longest one, so pressing a mark
   swaps the words without shoving the rest of the page down. */
.iface__readout {
  margin-top: 1.5rem;
  min-height: 5.5em;
  max-width: 40rem;
}
.iface__readout .k {
  display: block;
  font-size: var(--t-small);
  letter-spacing: 0.14em;
  color: var(--ochre);
  margin-bottom: 0.35rem;
}

/* BASE FIRST, then the media query. A media query adds no specificity — put
   the base rule after it and it wins at every width, which is exactly how the
   phone captions went missing. */
.iface__list { display: none; margin-top: 1.5rem; padding-left: 0; list-style: none; }
.iface__list li { margin-bottom: 0.9rem; }
.iface__list .k { color: var(--ochre); letter-spacing: 0.1em; font-size: var(--t-small); display: block; }

@media (max-width: 46rem) {
  /* 34px dots on a 1600px image are unusable — the caption list replaces them */
  .hot { display: none; }
  .iface__list { display: block; }
  .iface__prompt { display: none; }   /* "press a mark" — there are no marks */
}


/* ── the instrument, drawing itself ──────────────────────────────────────── */
/* The plugin's signature move: when you change moments, its face redraws in
   ink. This is the same gesture on the web, and it is genuinely Lassi's hand —
   his ink plate traced to 26 centreline strokes, not a fade of a screenshot.

   Each <path> carries pathLength="1", so one dash unit == the whole stroke and
   the maths is length-independent. --s is when that stroke starts (0..1 through
   the sequence) and --d is how long it takes, both scaled by --draw-dur. The
   result is one pen moving at a constant speed across the drawing.

   Plays ONCE, on arrival, and stops. Nothing loops. Under reduced-motion the
   drawing is simply present, fully inked. */

/* the drawing is positioned exactly over the piano inside the face render, so
   the strokes land on the instrument that is already there. When the sequence
   finishes the overlay fades out and what remains is the real face — the seam
   is invisible because the two drawings are the same drawing. */
.pianodraw {
  --draw-dur: 2600ms;
  position: absolute;
  /* Measured, not derived: rendering the strokes at candidate offsets and
     maximising overlap with the printed piano gives 92.8% coincidence here.
     The token-derived guess (4.233 / 21.196 / 49.383) was visibly off. */
  left: 5.00%;
  top: 21.65%;
  width: 47.85%;
  height: 30.17%;
  color: var(--cream);
  pointer-events: none;
  z-index: 2;
}
.pianodraw__svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* resting state: nothing to see, because the face underneath already shows the
   piano. Only the .js path ever hides or animates anything. */
.pianodraw__svg path { stroke-dasharray: 1; stroke-dashoffset: 0; }
.pianodraw { opacity: 0; }

.js .pianodraw[data-draw] .pianodraw__svg path { stroke-dashoffset: 1; }
.js .pianodraw[data-draw] { opacity: 1; }

.js .pianodraw.is-drawn .pianodraw__svg path {
  animation: ink-in calc(var(--draw-dur) * var(--d, 0.1)) linear forwards;
  animation-delay: calc(var(--draw-dur) * var(--s, 0));
}
@keyframes ink-in { to { stroke-dashoffset: 0; } }

/* once the last stroke lands, the overlay retires and leaves the real face */
.js .pianodraw.is-drawn {
  animation: ink-retire calc(var(--draw-dur) + 700ms) ease-out forwards;
}
@keyframes ink-retire {
  0%   { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .js .pianodraw[data-draw],
  .js .pianodraw.is-drawn { opacity: 0; animation: none; }
  .js .iface__stage[data-stage] .iface__face { opacity: 1; transition: none; }
}


/* ── the moments ─────────────────────────────────────────────────────────── */

.moments__head { display: grid; grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); gap: clamp(1.5rem, 3vw, 3rem); margin-bottom: clamp(2.5rem, 5vw, 4rem); }
@media (max-width: 60rem) { .moments__head { grid-template-columns: 1fr; } }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: clamp(2rem, 3vw, 3rem);
}
/* four cards must not land 3 + 1 with an orphan on its own row */
.cards--four { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
@media (min-width: 60rem) and (max-width: 82rem) {
  .cards--four { grid-template-columns: repeat(2, 1fr); }
}

.card__name {
  font-family: var(--display);
  font-size: clamp(1.75rem, 2.4vw, 2.25rem);
  line-height: 1.15;
  margin: 0 0 0.35rem;
}
/* the recipe whispers in italic — the plugin's own convention, and the one
   synthesised slant the two-typeface system permits (Plex ships one weight,
   one style; the browser obliques it) */
.card__recipe { font-size: var(--t-small); font-style: italic; color: var(--ochre-ink); margin: 0 0 0.9rem; }
.card__body { margin: 0 0 1.25rem; color: var(--ink-60); }
.section--dark .card__body { color: var(--cream-60); }
.section--dark .card__recipe { color: var(--ochre); }

.card__sigil {
  display: block;                       /* an inline span has no size */
  width: 34px; height: 34px;
  background-color: var(--ink-60);      /* was --ink-40: measured too faint
                                           against the paper ground */
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  margin-bottom: 1rem;
  transition: background-color 140ms ease-out;
}
.section--dark .card__sigil { background-color: var(--cream-60); }

/* hover is an active state — the sigil may take the accent while a hand is on
   the card, and only then */
.card:hover .card__sigil { background-color: var(--ochre-ink); }
.section--dark .card:hover .card__sigil { background-color: var(--ochre); }
.card__sigil--none { visibility: hidden; }   /* jouset has no drawn plate */


/* ── the audio player ────────────────────────────────────────────────────── */
/* Each .player wraps a REAL <audio controls>. With JS off that is what you
   get: the browser's own player. JS replaces it with the one below. */

.player {
  display: flex;
  align-items: center;
  gap: 1rem;
  border-top: 1px solid var(--ink-14);
  padding-top: 1rem;
}
.section--dark .player { border-color: var(--cream-16); }
.player > audio { width: 100%; }

.player__btn {
  flex: 0 0 auto;
  width: 44px; height: 44px;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: none;
  color: inherit;
  cursor: pointer;
  display: grid;
  place-items: center;
  padding: 0;
  transition: background-color 140ms ease-out, color 140ms ease-out, border-color 140ms ease-out;
}
.player__btn:hover:not(:disabled) { color: var(--ochre-ink); border-color: var(--ochre); }
.section--dark .player__btn:hover:not(:disabled) { color: var(--ochre); }
.player__btn[aria-pressed="true"] { background: var(--ochre); border-color: var(--ochre); color: var(--on-ochre); }
.player__btn svg { width: 13px; height: 15px; fill: currentColor; }
.player__btn .pause { display: none; }
.player__btn[aria-pressed="true"] .play { display: none; }
.player__btn[aria-pressed="true"] .pause { display: block; }

/* a real range input, so the scrubber is keyboard-operable for free */
.player__track {
  flex: 1 1 auto;
  -webkit-appearance: none;
  appearance: none;
  background: none;
  height: 18px;
  margin: 0;
  cursor: pointer;
}
.player__track::-webkit-slider-runnable-track { height: 2px; background: var(--ink-40); }
.player__track::-moz-range-track { height: 2px; background: var(--ink-40); }
.section--dark .player__track::-webkit-slider-runnable-track { background: var(--cream-16); }
.section--dark .player__track::-moz-range-track { background: var(--cream-16); }
.player__track::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 10px; height: 10px; border-radius: 999px;
  background: var(--ochre-ink); border: 0; margin-top: -4px;
}
.player__track::-moz-range-thumb { width: 10px; height: 10px; border-radius: 999px; background: var(--ochre-ink); border: 0; }
.section--dark .player__track::-webkit-slider-thumb { background: var(--ochre); }
.section--dark .player__track::-moz-range-thumb { background: var(--ochre); }
.player__track::-moz-range-progress { height: 2px; background: var(--ochre-ink); }

.player__time { flex: 0 0 auto; font-size: var(--t-small); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.section--dark .player__time { color: var(--cream-60); }

/* the dead state — there is no recording for this moment yet. It is conveyed by
   a disabled control and the word "soon", NOT by dimming, which drove the label
   to 1.6:1 and made the honest message the least readable text on the page. */
.player__btn:disabled { cursor: not-allowed; border-color: var(--ink-40); color: var(--ink-mute); }
.section--dark .player__btn:disabled { border-color: var(--cream-16); color: var(--cream-60); }
.player.is-empty .player__track { visibility: hidden; }


/* ── the buy ─────────────────────────────────────────────────────────────── */

.buy { display: grid; grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); gap: clamp(1.5rem, 3vw, 3rem); }
@media (max-width: 60rem) { .buy { grid-template-columns: 1fr; } }
.buy__price { font-family: var(--display); font-size: clamp(2.5rem, 5vw, 4rem); margin: 0 0 1rem; }

.form { display: flex; gap: 0.75rem; max-width: 30rem; margin-top: 1.75rem; flex-wrap: wrap; }
.form input {
  flex: 1 1 14rem;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--ink-mute);     /* 5.1:1 — a visible boundary */
  padding: 0.7rem 0.15rem;
  font: inherit;
  color: inherit;
}
.form input::placeholder { color: var(--ink-mute); }
.form input:focus { border-color: var(--ochre-ink); }   /* NO outline:none — the
                                                           focus ring is the only
                                                           thing a keyboard user
                                                           has */
.foot .form input { border-color: var(--cream-60); color: var(--cream); }
.foot .form input::placeholder { color: var(--cream-60); }
.foot .form input:focus { border-color: var(--ochre); }

.btn {
  border: 1px solid currentColor;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: 0.08em;
  padding: 0.7rem 1.5rem;
  border-radius: 999px;
  cursor: pointer;
  transition: background-color 140ms ease-out, color 140ms ease-out, border-color 140ms ease-out;
}
.btn:hover { background: var(--ochre); border-color: var(--ochre); color: var(--on-ochre); }


/* ── specs — a row of small accordions ───────────────────────────────────── */

.specs {
  border-top: 1px solid var(--ink-14);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  column-gap: clamp(1.5rem, 3vw, 3rem);
}
.spec { border-bottom: 1px solid var(--ink-14); align-self: start; }
.spec > summary {
  list-style: none;
  cursor: pointer;
  padding: 1.35rem 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--t-small);
  letter-spacing: 0.12em;
  transition: color 140ms ease-out;
}
.spec > summary::-webkit-details-marker { display: none; }
.spec > summary:hover { color: var(--ochre-ink); }
.spec > summary::after { content: "+"; font-size: 1.1rem; color: var(--ink-mute); }
.spec[open] > summary::after { content: "–"; color: var(--ochre-ink); }
.spec__body { padding-bottom: 1.75rem; color: var(--ink-60); font-size: var(--t-small); }
.spec__body p { margin: 0 0 0.85em; }
.spec__body p:last-child { margin-bottom: 0; }
.spec__body ul { margin: 0; padding-left: 1.1rem; }
.spec__body li { margin-bottom: 0.4em; }


/* ── footer ──────────────────────────────────────────────────────────────── */

.foot { background: var(--dark); color: var(--cream); padding-block: var(--band); }
.foot__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: clamp(2rem, 4vw, 4rem); }

/* the ladder inverts on the dark ground */
.foot .muted, .foot .label, .foot .tiny { color: var(--cream-60); }

.foot__mark {
  display: block;
  width: 110px;
  aspect-ratio: 842 / 454;
  background: var(--cream);
  -webkit-mask: url("../../assets/ink/wordmark.png") no-repeat center / contain;
  mask: url("../../assets/ink/wordmark.png") no-repeat center / contain;
  margin-bottom: 1.5rem;
}
.foot a { color: var(--cream); }
/* ── elsewhere: the social row ────────────────────────────────────────────
   Official brand marks (simple-icons), inlined as single-path SVGs so they
   inherit currentColor and cost no extra request. They sit at the cream-60
   step and take the accent on hover, like every other live thing here. */
.social { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--cream-16); }
.social__row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.social__link {
  display: grid;
  place-items: center;
  width: 44px;                       /* a real touch target, not a 16px glyph */
  height: 44px;
  border-radius: 999px;
  color: var(--cream-60);
  transition: color 140ms ease-out, background-color 140ms ease-out;
}
.social__link svg { width: 20px; height: 20px; fill: currentColor; display: block; }
.social__link:hover { color: var(--ochre); background: rgba(230, 217, 200, 0.06); }

.foot__credit { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--cream-16); font-size: var(--t-small); color: var(--cream-60); }
.foot__credit p { margin: 0 0 0.35em; }

.totop { display: block; text-align: center; padding: 1.5rem; font-size: var(--t-small); letter-spacing: 0.14em; color: var(--ink-60); text-decoration: none; }
.totop:hover { color: var(--ochre-ink); }


/* ── the reveal ──────────────────────────────────────────────────────────── */
/* Sections arrive as you reach them: a short fade and a few pixels of rise,
   once, and then they stop. Nothing loops, nothing bounces, nothing re-plays
   on the way back up.

   The class is added by JS. If JS never runs, `.reveal` is never applied and
   everything is simply visible — the content is not hidden by default, which
   is the mistake that leaves no-JS visitors staring at a blank page. */

.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 640ms cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 640ms cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: opacity, transform;
}
.js .reveal.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1; transform: none; transition: none; }
}

/* staggered arrival — inside a marked section, the cards and accordions land
   one after another, ~70ms apart, capped so a long row does not dawdle.
   Scoped to `.js` like everything else that hides: no script, no hiding. */
.js [data-stagger].reveal .card,
.js [data-stagger].reveal .spec {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 560ms cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 560ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.js [data-stagger].reveal.is-in .card,
.js [data-stagger].reveal.is-in .spec {
  opacity: 1;
  transform: none;
}
.js [data-stagger].reveal.is-in :is(.card, .spec):nth-child(2) { transition-delay: 70ms; }
.js [data-stagger].reveal.is-in :is(.card, .spec):nth-child(3) { transition-delay: 140ms; }
.js [data-stagger].reveal.is-in :is(.card, .spec):nth-child(4) { transition-delay: 210ms; }
.js [data-stagger].reveal.is-in :is(.card, .spec):nth-child(5) { transition-delay: 280ms; }
.js [data-stagger].reveal.is-in :is(.card, .spec):nth-child(n+6) { transition-delay: 350ms; }

@media (prefers-reduced-motion: reduce) {
  .js [data-stagger].reveal .card,
  .js [data-stagger].reveal .spec { opacity: 1; transform: none; transition: none; }
}


/* ── the scrubbed statements ─────────────────────────────────────────────── */
/* The two giant claims ink in word by word as the visitor scrolls through
   them — from the ladder's faintest step up to full ink, like a line being
   written. Driven by scroll position only: it moves when the reader moves,
   both directions, and holds still when they do. With no JS (or reduced
   motion) the words are simply ink. */

.js [data-scrub] .w {
  opacity: 0;                           /* the line does not exist until the
                                           reader moves — then it writes itself
                                           in, word by word. Deliberate (the
                                           owner's call): unrevealed words are
                                           ABSENT, not low-contrast. Covered
                                           for everyone else: aria-label holds
                                           the full sentence, reduced-motion
                                           shows full ink, no-JS never hides,
                                           and the failsafe lights everything
                                           if scroll events never arrive. */
  transition: opacity 160ms ease-out;
}
.js [data-scrub] .w.on { opacity: 1; }

/* "…in a room with a piano" — the letters of the last word are keys. When the
   word arrives, each dips down and comes back up in turn, once: five notes
   played left to right. Then stillness, like everything else here. */
[data-scrub] .kb { display: inline-block; }
.js [data-scrub] .w.on .kb {
  animation: key-press 520ms cubic-bezier(0.36, 0, 0.34, 1) both;
  animation-delay: calc(var(--k, 0) * 110ms + 120ms);
}
@keyframes key-press {
  0%   { translate: 0 0; }
  35%  { translate: 0 0.085em; }   /* the key goes down          */
  55%  { translate: 0 0.085em; }   /* it is held, just a breath  */
  100% { translate: 0 0; }         /* and released               */
}
@media (prefers-reduced-motion: reduce) {
  .js [data-scrub] .w.on .kb { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js [data-scrub] .w { opacity: 1; transition: none; }
}


/* ── hero depth ──────────────────────────────────────────────────────────── */
/* As the hero scrolls away, the headline drifts up a touch faster than the
   photograph and quietly hands itself over. Scroll-scrubbed; set via custom
   properties from the one scroll handler. */

@media (prefers-reduced-motion: no-preference) {
  .js .hero__inner {
    transform: translateY(calc(var(--hero-p, 0) * -6vh));
    opacity: calc(1 - var(--hero-p, 0) * 1.1);
    will-change: transform, opacity;
  }
  /* the photograph settles on arrival: one slow ease from 1.10 to 1.04, then
     it stops. The 1.04 stays as head-room for the scroll parallax, which uses
     the separate `scale` channel so the two never fight. */
  .js .hero__media img {
    transform: translateY(calc(var(--hero-p, 0) * 2vh));
    animation: hero-settle 1600ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    will-change: transform, scale;
  }
  @keyframes hero-settle {
    from { scale: 1.06; }
    to   { scale: 1.02; }
  }
  /* the words arrive a breath apart, once, and stop */
  .js .hero__title  { animation: hero-in 800ms cubic-bezier(0.22, 0.61, 0.36, 1) 120ms both; }
  .js .hero__sub    { animation: hero-in 800ms cubic-bezier(0.22, 0.61, 0.36, 1) 280ms both; }
  .js .hero__scroll { animation: hero-in 800ms cubic-bezier(0.22, 0.61, 0.36, 1) 440ms both; }
  @keyframes hero-in {
    from { opacity: 0; translate: 0 16px; }
  }
}


/* ── the lightbox — for the trailer, when there is one ───────────────────── */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 5vw, 4rem);
  background: rgba(12, 9, 7, 0.92);
  border: 0;
}
.lightbox::backdrop { background: rgba(12, 9, 7, 0.92); }
.lightbox[hidden] { display: none; }

.lightbox__frame { width: min(100%, 68rem); aspect-ratio: 16 / 9; background: #000; }
.lightbox__frame video,
.lightbox__frame iframe { width: 100%; height: 100%; border: 0; display: block; }

.lightbox__close {
  position: absolute;
  top: clamp(1rem, 3vw, 2rem);
  right: clamp(1rem, 3vw, 2rem);
  background: none;
  border: 1px solid var(--cream-60);
  border-radius: 999px;
  color: var(--cream);
  font: inherit;
  font-size: var(--t-small);
  letter-spacing: 0.08em;
  padding: 0.5rem 1rem;
  cursor: pointer;
}
.lightbox__close:hover { border-color: var(--ochre); color: var(--ochre); }


/* ── the persistent pill ─────────────────────────────────────────────────── */

.pill {
  position: fixed;
  right: clamp(1rem, 2.5vw, 2rem);
  bottom: clamp(1rem, 2.5vw, 2rem);
  z-index: 70;
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  background: var(--ochre);
  color: var(--on-ochre);
  text-decoration: none;
  border-radius: 999px;
  padding: 0.9rem 1.5rem;
  letter-spacing: 0.06em;
  box-shadow: 0 8px 30px rgba(22, 18, 14, 0.22);
  transition: transform 140ms ease-out;
}
.pill:hover { transform: translateY(-2px); }
.pill small { font-size: 0.8em; color: var(--on-ochre-2); }   /* 5.6:1, not an
                                                                 opacity, which
                                                                 measured 4.3:1 */
@media (prefers-reduced-motion: reduce) { .pill:hover { transform: none; } }
