/* website/style.css, Verdano marketing + legal pages.
   Tokens mirror app/theme/tokens.ts by convention (website/ is outside
   scripts/token-audit.js scope). If the app palette changes, update here.

   Direction: DESIGN.md "Verdano" / "The Night Market Stall" (chosen 2026-08-06).
   Two full-bleed Night Ink fields bracket a warm Paper middle. Lime is a
   dark-field voice only, never text or a mark on a light band. Flat by
   default; the only shadow lifts a phone bezel off a Night Ink field.
   Light-only, zero JS, system font stack only, see PRODUCT.md constraints. */

:root {
  --bg: #F4F3ED;
  --surface: #FFFFFF;
  --text: #211F1B;
  --muted: #76746C;
  --faint: #9C9A91;
  --ink: #1E3D2E;
  --ink-deep: #13271C;
  --lime: #C7DC7E;
  --lime-deep: #A9C65C;
  --lime-soft: #EAF2D7;
  --hairline: rgba(30,40,30,0.08);
  --text-on-dark: rgba(255,255,255,0.82);
  --maxw: 68rem;
  --maxw-prose: 44rem;
  /* The band gutter: content sits in one centred --maxw column while the
     field colour runs edge to edge. Every element that must share that
     column edge, skip link, nav, section, footer, references this, so
     the four cannot drift apart. */
  --gutter: max(1.5rem, calc((100% - var(--maxw)) / 2));
  color-scheme: light;
}

/* --muted / --faint are defined for parity with the app token set but are
   NEVER assigned to `color:` anywhere below, DESIGN.md's Verified-Pair
   Rule: both fail WCAG AA against --bg and --lime-soft. Where the design
   wants "quieter", a smaller size of --text is used instead. */

* { box-sizing: border-box; }

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

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: 1.125rem;
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
}

/* ---- Typography (DESIGN.md Typography > Hierarchy) ---- */

h1 {
  /* Fluid in BOTH axes. 8.2vw alone sizes the headline off width, which is
     right on a tall window and wrong on a short one — a 14" laptop is wide
     enough to ask for 88px of display type but not tall enough to hold it
     under the subline and the CTA. min() lets whichever dimension is scarcer
     do the limiting, continuously, so the headline is always as large as the
     viewport can actually carry. A height *breakpoint* cannot express this:
     it holds one size until it crosses a threshold and then jumps. */
  font-size: clamp(2.75rem, min(8.2vw, 10.5vh), 5.5rem);
  font-weight: 800;
  line-height: 0.96;
  letter-spacing: -0.042em;
  color: var(--lime);
  max-width: 12ch;
  margin: 0 0 16px;
}

h2 {
  font-size: clamp(2rem, 5.4vw, 3.5rem);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.038em;
  color: var(--ink);
  max-width: 12ch;
  margin: 40px 0 16px;
}

/* The 40px above an h2 is for a heading that follows content inside a band.
   When the h2 opens its band, the band's own padding already owns that space
   (DESIGN.md Layout > Spacing rhythm) and the two must not stack. */
.section > h2:first-child,
.feature h2:first-child { margin-top: 0; }

p {
  margin: 0 0 16px;
  max-width: 65ch;
}

a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
a:hover { color: var(--ink-deep); }

::selection {
  background: var(--lime);
  color: var(--ink-deep);
}

a:focus-visible,
summary:focus-visible,
.wrap:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
.section-dark :focus-visible {
  outline-color: var(--lime);
}

/* ---- Skip link ----
   The one element on the site that is invisible until focused. With no
   JavaScript, native focus is the whole keyboard story (DESIGN.md Buttons >
   Focus), and the <h1> + hero CTA sit in the opening field ahead of the
   feature content, so a keyboard user needs a way past the nav.
   It carries .btn-cta, so it inherits the verified pairs already in the
   system and needs no new colour: Night Ink on Stall Lime (10.46:1) on
   index.html, and white on Deep Ink (9.6:1) under .page-legal. Aligned to the
   nav's 24px inline padding, not --gutter: the nav is edge-aligned chrome, so
   a skip link on the content column would appear 152px away from the lockup it
   covers at 1440px.

   The selectors are `a.skip`, not `.skip`: the element also carries .btn-cta,
   which is declared later in this file, so at equal specificity .btn-cta's
   padding would win and the hidden link would keep a 48x28 box. The element
   qualifier settles it by specificity instead of by source order, this file
   must not be reordered (see the note on h2:first-child). */

a.skip {
  position: absolute;
  z-index: 10;
  top: 24px;
  left: 24px;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
a.skip:focus {
  width: auto;
  height: auto;
  padding: 14px 24px;
  overflow: visible;
  clip-path: none;
}

/* ---- Nav (DESIGN.md Components > Navigation) ----
   Sits inside the Night Ink hero field and shares its ground, no bar, no
   border, no sticky behaviour. On .page-legal it inverts to Deep Ink on
   Paper with a 1px hairline beneath. Single markup, context styles it. */

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Chrome is edge-aligned while content stays on the --maxw column
     (DESIGN.md Layout). Deliberate: the nav frames the page rather than
     sitting in it. */
  padding: 24px;
  /* Block padding only — the 24px inline padding is what edge-aligns the
     lockup and must not move. Scales with height so the chrome gives back a
     little room on a short window, continuously rather than at a threshold. */
  padding-block: clamp(12px, 2.4vh, 24px);
}

.nav .wordmark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text-on-dark);
  text-decoration: none;
  /* Tap target: 8px block padding keeps the anchor comfortably past the
     WCAG 2.5.8 24px minimum and stays on the 4px grid. */
  padding-block: 8px;
}

/* The lockup is inlined rather than linked so it takes its colour from the
   anchor around it: --text-on-dark on the Night Ink field, --ink on Paper.
   An <img> could not invert, and a second colour variant would be a second
   asset to keep in sync. */
.nav .lockup {
  width: auto;
  height: 40px;
  flex: none;
}

/* Visually hidden, still in the accessibility tree. The lockup is aria-hidden,
   so this text is the link's accessible name — without it the only link to the
   homepage would be an unnamed graphic. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.page-legal .nav {
  border-bottom: 1px solid var(--hairline);
}
.page-legal .nav .wordmark {
  color: var(--ink);
}

/* ---- Buttons (DESIGN.md Components > Buttons) ----
   One class. Default = primary-on-dark (the nav/hero CTA on Night Ink).
   .page-legal context = primary-on-light, since the legal nav inverts. */

.btn-cta {
  display: inline-block;
  padding: 14px 24px;
  border-radius: 6px;
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-decoration: none;
  background: var(--lime);
  color: var(--ink-deep);
  transition: background-color 150ms ease-out;
}
.btn-cta:hover { background: var(--lime-deep); }
.btn-cta:focus-visible { outline-color: var(--lime); }

.page-legal .btn-cta {
  background: var(--ink);
  color: var(--surface);
}
.page-legal .btn-cta:hover { background: var(--ink-deep); }
.page-legal .btn-cta:focus-visible { outline-color: var(--ink); }

/* ---- Section bands (DESIGN.md Overview + The Two-Field Rule) ----
   .section is a full-bleed field; its content is centred at --maxw via
   inline padding so the background can still run edge to edge without a
   wrapper element. Paper by default. .section-dark is a Night Ink field -
   used for the hero (and, at most, one further open/close field per the
   Two-Field Rule). --surface (white) is reserved, not a band colour. */

.section {
  position: relative;
  padding-block: 80px;
  padding-inline: var(--gutter);
  background: var(--bg);
}

.section-dark {
  background: var(--ink-deep);
  color: var(--text-on-dark);
}

/* The opening band is the only one whose height competes with the fold, so it
   is the only one whose padding tracks the viewport. */
main > .section-dark { padding-block: clamp(40px, 7.5vh, 80px); }

/* ---- Feature rows (text + phone, DESIGN.md Layout) ----
   Not a required interface class, but the alternating text/phone pattern
   needs a mechanism; Task 3 may refine or replace it. Collapses at 820px,
   phone last, per DESIGN.md's Responsive breakpoints.
   Markup contract: alternation is an explicit opt-in via `.feature-reverse`
   on every other row, NOT `:nth-of-type(even)`, that selector counts
   siblings within the immediate parent, so it silently no-ops if a future
   markup wraps each `.feature` in its own `.section` (each row then becomes
   its parent's only child, always "1st of type"). Apply `.feature-reverse`
   by hand to the 2nd, 4th, ... row regardless of wrapper structure. */

.feature {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 40px;
  /* How far the device mock is pulled in off the column edge, toward the
     centre. space-between would otherwise pin it to the outer edge and leave
     the row reading as two things at opposite ends of a wide band rather than
     one composition. Scales with width because the slack it eats into does
     too — at the 900px breakpoint there is little to give. */
  --phone-inset: clamp(0px, 5vw, 80px);
}
.feature-reverse { flex-direction: row-reverse; }

/* Physical, not flow-relative: `margin-inline-end` is the right margin in
   both rows, but row-reverse puts the mock on the left, so the reversed row
   needs the mirror side. The copy column stays on the column edge either way
   — only the mock moves. */
.feature > .phone { margin-inline-end: var(--phone-inset); }
.feature-reverse > .phone {
  margin-inline-end: 0;
  margin-inline-start: var(--phone-inset);
}

/* Columns are measured, not stretched. A `flex: 1 1 20rem` basis here would
   override .phone's clamp() width (and, through aspect-ratio, its height),
   and would stretch the text column past its measure so justify-content had
   no free space left to work with, the copy then hugs the phone and leaves
   a void at the far edge of the band. Shrink-wrapping both columns lets
   space-between anchor text and phone to opposite edges, which is what makes
   the alternation read. Text absorbs all the shrink; the phone is an object. */
.feature > * { flex: 0 1 auto; }
.feature > .phone { flex: 0 0 auto; }

/* DESIGN.md Typography > Body: "Measure 44ch beside a phone, 65-75ch at full
   width." The 65ch default belongs to the full-width bands. */
.feature p { max-width: 44ch; }

/* The CTA is its own group under the copy, not the tail of the paragraph:
   16px (p margin) + 24px = 40px, the 2.5x separation DESIGN.md asks for
   between a tight heading+paragraph group and what follows it. */
.feature .btn-cta { margin-top: 24px; }

/* ---- Feature grid (DESIGN.md Components > Cards / Containers) ----
   Not cards: cells on Paper, separated by 1px hairlines, no background,
   no radius, no shadow. */

.grid {
  display: grid;
  /* DESIGN.md's track is `minmax(14rem, 1fr)`. The min() guard is a text-zoom
     fix, not a design change: a bare 14rem minimum is a floor the track cannot
     go under, so at 200% text zoom 14rem resolves to 448px inside a 342px
     column and the grid pushed the page into horizontal scroll (WCAG 1.4.4).
     Wherever 14rem fits, every viewport at normal zoom, min(14rem, 100%)
     resolves to 14rem and the layout is byte-identical to the spec. */
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: 0 40px;
  margin-top: 40px;
  /* The closing rule belongs to the track, not to the last cell. Scoping it to
     `.grid > *:last-child` was tried and reverted: with five cells the auto-fit
     track wraps 3+2 across roughly 800-1064px, so the rule landed under the
     MIDDLE column of the final row with nothing under the cell to its left -
     an asymmetric hairline that reads as a rendering bug. Measured at 900px
     (clientWidth 885, 3 columns): rule spanned x=316.33..568.66 while the cell
     at x=24..276.33 in the same row closed with nothing. A full-width rule
     instead runs under the final row's empty columns, which reads as the
     bottom edge of a bordered list, rendered and compared at 700/900/1440px,
     it is the lesser artifact and it is symmetric at every column count. */
  border-bottom: 1px solid var(--hairline);
}
.grid > * {
  border-top: 1px solid var(--hairline);
  padding: 24px 0;
}
/* DESIGN.md Typography > Title: one style, two elements, feature-grid card
   titles and FAQ summaries. Grouped so the scale cannot drift between them. */
.grid h3,
summary {
  font-size: clamp(1.125rem, 2.4vw, 1.5rem);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.022em;
}
.grid h3 {
  color: var(--ink);
  margin: 0 0 8px;
}
.grid p {
  margin: 0;
  max-width: none;
}

/* ---- FAQ disclosure (DESIGN.md Components > FAQ Disclosure) ----
   Native <details>/<summary>. Rows separated by a 2px Deep Ink rule above.
   Marker is Deep Ink, never lime (1.73:1 would be invisible). */

/* The 21px band is DESIGN.md's faq-row padding, but it lives on <summary>,
   not on <details>: as a <details> padding it left the actual click/tap
   target only 23px tall (the summary line box), failing WCAG 2.5.8's 24px
   minimum on the site's primary interaction. Moved onto <summary> the whole
   row becomes the target (65px) and the closed-row geometry is unchanged -
   21 + 23 + 21 either way. details[open] restores the 21px below the answer
   that <details>'s own padding-bottom used to provide. */
details {
  border-top: 2px solid var(--ink);
  padding: 0;
}
details[open] { padding-bottom: 21px; }
details:last-of-type { border-bottom: 2px solid var(--ink); }

summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 21px 0;
  cursor: pointer;
  list-style: none;
}
summary::-webkit-details-marker { display: none; }
summary::after {
  content: "+";
  color: var(--ink);
  font-weight: 700;
  flex-shrink: 0;
}
details[open] summary::after { content: "\2013"; }

/* DESIGN.md > FAQ Disclosure: "Answers are Body on Paper, max 60ch, 24px below
   the summary." Once the 21px band moved onto <summary>, the open-state
   distance from the summary text to the answer became 21 (summary padding) +
   24 (p margin) = 45px, off the 4/8/16/24/40 spacing scale and nearly double
   the spec. Dropping the summary's bottom padding only while open restores
   exactly 24px; the summary is still a 43.5px target at 390px (21px top
   padding + a 22.5px line box), well clear of WCAG 2.5.8's 24px floor, and
   details[open]'s own padding-bottom still supplies the 21px under the answer.
   Closed rows are untouched. */
details[open] summary { padding-bottom: 0; }

details p {
  max-width: 60ch;
  margin: 24px 0 0;
}

/* ---- Footer (hairline top rule; "quieter" via size, not grey text) ---- */

.footer {
  border-top: 1px solid var(--hairline);
  padding-block: 40px;
  padding-inline: var(--gutter);
}
.footer a,
.footer p {
  font-size: 0.9375rem;
  line-height: 1.2;
  margin: 0;
}
/* <p> takes its colour by inheritance, from body on a Paper footer, from
   .section-dark on the closing ink field, so neither needs an override.
   Only <a> does: the global `a { color: var(--ink) }` matches the element
   directly and would otherwise beat inheritance in both contexts. */
.footer a { color: var(--text); text-decoration: none; }
.footer.section-dark a { color: var(--text-on-dark); }

/* Brand sign-off, last thing on the page. `width: 100%` with a max-width means
   it spans the band on a phone — where the mark reading edge to edge is the
   point — and then stops growing, so it is a fixed 26rem on anything wider.
   No breakpoint: min(100%, 26rem) is what width + max-width already express.

   Inlined, aria-hidden, and not a link: the nav lockup is the named route home
   and a second link to the same place would be a duplicate stop for a keyboard
   or screen-reader user. Being inline SVG rather than an <img> is what lets one
   asset invert — --text-on-dark on the home page's closing ink field, --ink on
   the Paper footers the legal pages use. --ink, not --text: on Paper this is a
   mark, and DESIGN.md's Colors section reserves Deep Ink for marks. */
.footer-lockup {
  display: block;
  width: 100%;
  max-width: 26rem;
  height: auto;
  margin-top: 40px;
  /* Centred on the band. A no-op on a phone, where the mark already spans the
     full width — it only does visible work once max-width caps the growth. */
  margin-inline: auto;
  color: var(--ink);
}
.footer.section-dark .footer-lockup { color: var(--text-on-dark); }
.footer a:hover { text-decoration: underline; }
/* Tap target: 15px/1.2 footer links were 20px tall, under WCAG 2.5.8's 24px
   minimum. 8px block padding takes them to 36px; the list's row gap drops to
   0 because that padding now supplies the separation between wrapped rows. */
.footer ul a {
  display: inline-block;
  padding-block: 8px;
}
.footer ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 16px;
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
}

/* ---- Phone bezel (DESIGN.md Components > Phone Bezel) ---- */

.phone {
  /* Height-aware for the same reason as h1: a 19.5:9 bezel is the tallest
     thing in a feature row, and since the row centres its two columns, every
     pixel of bezel above the copy's height pushes the copy — and the hero's
     CTA — half that far down. Letting the mock shrink on a short window is
     what keeps the CTA above the fold without a breakpoint. */
  width: clamp(196px, min(46vw, 30vh), 256px);
  aspect-ratio: 9 / 19.5;
  background: var(--ink-deep);
  border-radius: 32px;
  padding: 8px;
}
.section-dark .phone {
  box-shadow: 0 24px 60px rgba(0,0,0,0.45);
}

.placeholder {
  width: 100%;
  height: 100%;
  border-radius: 25px;
  background: var(--lime-soft);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 18px;
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* ---- Legal / prose pages (DESIGN.md governs chrome; content is frozen) ----
   .page-legal is the body-level context; .prose is the text column,
   carrying over table/code/.box/.todo/.updated from privacy.html and
   impressum.html's inline <style> blocks, minus their dark-mode blocks. */


.prose {
  max-width: var(--maxw-prose);
  margin: 0 auto;
  padding: 40px 24px 80px;
  /* Long email addresses and bare URLs in the legal text are single unbreakable
     words; at 200% text zoom they exceeded the column and scrolled the page
     (WCAG 1.4.4). Only takes effect when a word genuinely cannot fit, so normal
     rendering is unchanged. */
  overflow-wrap: break-word;
}

.prose h1 {
  font-size: 1.75rem;
  line-height: 1.25;
  letter-spacing: normal;
  color: var(--ink);
  max-width: none;
  margin: 0 0 4px;
}

.prose h2 {
  font-size: 1.2rem;
  line-height: 1.3;
  letter-spacing: normal;
  max-width: none;
  margin: 40px 0 16px;
}

.prose .updated {
  /* Ported as-is from privacy.html/impressum.html's original `color: #666`
     (5.16:1 on Paper, passes AA at 0.9rem). Not a Verdano palette colour;
     kept literal rather than "corrected" to a token. */
  color: #666;
  font-size: 0.9rem;
  margin-top: 0;
}

.prose table {
  border-collapse: collapse;
  width: 100%;
  margin: 16px 0;
  font-size: 0.92rem;
}
.prose th,
.prose td {
  /* Ported as-is from the original `border: 1px solid #ddd`. */
  border: 1px solid #ddd;
  padding: 8px 10px;
  text-align: left;
  vertical-align: top;
}
.prose th { font-weight: 600; }
.prose .wrap { overflow-x: auto; }

/* Ported legal chrome. Both keep their original literals rather than tokens:
   these are frozen artifacts, and binding them to a live palette value would
   let a future palette change silently redraw a legal document. */
.prose .box,
.prose .todo {
  padding: 8px 0 8px 16px;
  margin: 24px 0;
}
.prose .box { border-left: 3px solid #999; }
.prose .todo { border-left: 3px solid #c00; }

/* ---- Decorative kitchen assets (DESIGN.md Decorative Imagery) ----
   Empty divs with background images, never <img>: display:none does NOT stop a
   browser downloading an image, so the only way a phone pays nothing for
   decoration is a background declared inside a media query the phone never
   matches. Backgrounds are also absent from the accessibility tree, which is
   where decoration belongs — no alt text to invent for a tomato. */

.float { display: none; }

/* 1136px, not 900px: the cutouts sit in the channel between the copy column
   and the device mock, and that channel only exists once the 68rem column has
   stopped growing. At 900px the band is 852px wide and the copy, gap, mock and
   its inset already claim ~709px of it — less open space than a single cutout
   is wide. Shrinking them to fit would leave decoration too small to read as
   deliberate, so the layer waits for a viewport that can hold it. The
   Free-On-Phones Rule extends unchanged: below this width nothing is fetched. */
@media (min-width: 1136px) {
  .float {
    display: block;
    position: absolute;
    /* z-index: 2 — the top of the three-level stack; see the rationale in
       the "Glyph watermark on the opening ink field" comment below. */
    z-index: 2;
    top: var(--f-top);
    width: var(--f-w);
    aspect-ratio: var(--f-ar);
    background-image: var(--f-img);
    background-size: contain;
    background-repeat: no-repeat;
    /* translateY() listed before rotate() so the animated translateY below
       composes in the SAME order (see float-drift): translateY is then the
       outer/last-applied function, moving the already-rotated box along the
       page's Y axis. If drift were the inner function instead, its
       displacement vector would itself get rotated by --f-rot, picking up a
       page-space X component (-drift * sin(rot)) that eats into the
       .float-left/.float-right clamp below. translateY(0) is a no-op at rest
       but keeps the base and animated transform lists the same shape, so the
       browser interpolates per-function instead of falling back to matrix
       interpolation (a different, unverified animation path). */
    transform: translateY(0) rotate(var(--f-rot, 0deg));
    pointer-events: none;
  }
  /* Anchored to the page's centre line: --f-x is "how far right of centre
     this placement sits," negative for left of it.

     The cutouts occupy the channel between the copy column and the device
     mock, and that channel is what moves when the viewport narrows. Above
     ~1136px the content column is a fixed 68rem and centred, so anchoring to
     the column edge and anchoring to the centre are the same thing — the
     algebra cancels the viewport width out. Below that the column starts
     shrinking, the mock walks toward the copy, and an edge-anchored cutout
     tracks the edge instead of the channel, drifting into whichever column
     reaches it first. Centre-anchoring holds the composition still while the
     columns close in around it.

     This also retires the 28px overflow clamp the edge-anchored version
     needed. Overflow now requires |--f-x| + width/2 + the rotated box's
     horizontal overhang to exceed half the viewport; the widest case is
     f-salad-cook at 78 + 150.5 + 13 = 242px against 568px at the 1136px
     breakpoint, so the geometry cannot reach an edge to overflow it.
     (The overhang term is (w·cos + h·sin − w)/2 for a box rotated by --f-rot.)

     The horizontal shift rides the `translate` property, not `transform`:
     individual transform properties apply outside `transform`, so this stays
     in page space and is never rotated by --f-rot — the same trap that put a
     drift component on the X axis and opened an h-scrollbar. */
  .float { left: 50%; right: auto; translate: calc(-50% + var(--f-x)) 0; }

  /* Drift is scroll-linked, not time-linked, and needs no JavaScript. Where
     animation-timeline is unsupported the @supports block never applies and the
     assets simply hold their static positions — the composition still reads.

     view(), not scroll(root). scroll(root) maps the WHOLE document to the
     animation, so an authored 150px of drift is spread over ~4000px of page:
     a 3% speed differential, about 27px per screenful, which is correct,
     invisible, and reads as broken. It also means a cutout deep in the page
     has spent most of its drift before it is ever on screen. view() gives each
     cutout its own timeline, running over its transit through the viewport —
     the same 150px now plays out over ~1100px, which is the parallax the
     composition was designed around. */
  @supports (animation-timeline: view()) {
    .float {
      animation: float-drift linear both;
      animation-timeline: view();
      /* Promote to its own compositor layer. Without it the browser may
         repaint the scaled background on each frame instead of just moving an
         existing texture, which is where the scroll jank came from. Four
         elements, desktop only, so the memory cost is bounded. */
      will-change: transform;
    }

    /* The hero cutout is the one placement already on screen before any
       scrolling happens, so its view() timeline begins in the past and that
       part is unreachable: it played 71% of its drift on a 850px-tall window
       and only 45% on a 1440px one, and its resting position drifted with
       monitor size. Anchoring it to document scroll over the first viewport
       gives it a range that is fully reachable at every size — and that span
       is exactly while the hero is on screen. It also starts at rest rather
       than mid-drift, which is what the composition was tuned against. */
    .f-tomato-hero {
      animation-name: float-drift-from-rest;
      animation-timeline: scroll(root);
      animation-range: 0 100vh;
    }
  }

  /* Symmetric about the tuned position: the cutout arrives half a drift below
     where it was placed and leaves half a drift above it, so the composition
     as tuned — and as measured for text clearance — is what you see when the
     cutout is centred in the viewport, not an edge case at scroll-top. */
  @keyframes float-drift {
    from { transform: translateY(calc(var(--f-drift) * -0.5)) rotate(var(--f-rot, 0deg)); }
    to   { transform: translateY(calc(var(--f-drift) * 0.5)) rotate(var(--f-rot, 0deg)); }
  }

  /* For a cutout you meet already on screen, the tuned position belongs at the
     start, not the middle — there is no "arriving from below" phase to spend
     half the drift on. */
  @keyframes float-drift-from-rest {
    from { transform: translateY(0) rotate(var(--f-rot, 0deg)); }
    to   { transform: translateY(var(--f-drift)) rotate(var(--f-rot, 0deg)); }
  }
}

/* Scroll-linked motion is a vestibular trigger. This is not optional. */
@media (prefers-reduced-motion: reduce) {
  .float { animation: none; }
}

/* Placements. --f-drift differs per placement on purpose: uniform drift reads
   as one sliding sheet, not depth. --f-top is measured from the top of the
   section the div sits in, so copy edits do not move the assets. */
.f-tomato-hero {
  --f-img: url(/img/tomato.webp);
  --f-ar: 480/343;
  --f-w: 215px;
  --f-top: 463px;
  --f-x: 10px;
  --f-rot: -14deg;
  --f-drift: -120px;
}
.f-grater-import {
  --f-img: url(/img/cheesegrater.webp);
  --f-ar: 480/810;
  --f-w: 205px;
  --f-top: 551px;
  --f-x: 29.5px;
  --f-rot: 8deg;
  --f-drift: -180px;
}
.f-onion-healthify {
  --f-img: url(/img/onion.webp);
  --f-ar: 480/333;
  --f-w: 225px;
  --f-top: 605px;
  --f-x: -54.5px;
  --f-rot: -24deg;
  --f-drift: -90px;
}
.f-salad-cook {
  --f-img: url(/img/salad.webp);
  --f-ar: 480/325;
  --f-w: 301px;
  --f-top: 527px;
  --f-x: 78px;
  --f-rot: -8deg;
  --f-drift: -150px;
}

/* Glyph watermark on the opening ink field: a few percent lighter than the
   field behind it. Sits left of the content column and behind everything, so
   it changes no measured contrast pair. Decorative, so it is a background on a
   pseudo-element and never enters the accessibility tree.

   The selector is `main > .section-dark`, not `.section-dark`: three elements
   carry that class — the header nav, the hero band, and the closing footer
   field. Only the hero is a direct child of <main>, so this marks the opening
   field and nothing else.

   z-index: 0 alone does not put the glyph behind the hero's text: CSS paint
   order puts any positioned descendant (even at z-index: 0) above in-flow
   non-positioned content, regardless of tree order. The hero's .feature
   column is given its own explicit stacking level below to sit above the
   glyph, and .float is bumped to the next level up so the decorative assets
   keep painting above hero content exactly as before. */
@media (min-width: 900px) {
  main > .section-dark::before {
    content: "";
    position: absolute;
    left: -5rem;
    top: 50%;
    translate: 0 -50%;
    width: 44rem;
    aspect-ratio: 417/426;
    background: url(/img/glyph-watermark.svg) no-repeat center/contain;
    opacity: 0.05;
    pointer-events: none;
    z-index: 0;
  }

  main > .section-dark > .feature {
    position: relative;
    z-index: 1;
  }
}

/* ---- Responsive (DESIGN.md Layout > Responsive) ----
   1136px: the decorative float layer gates on this (see Decorative kitchen
   assets above for why it is not 900px).
   900px: the glyph watermark and the hero's stacking rule.
   820px: feature rows collapse to a stacked column, phone last.
   640px: band padding drops from 80px to 48px. 390px is the design floor.
   There is no height query in this file; short windows are handled by fluid
   sizing instead, see the note at the end of the file. */

@media (max-width: 820px) {
  .feature {
    flex-direction: column;
    align-items: flex-start;
  }
  .feature .phone { order: 2; align-self: center; }
}

@media (max-width: 640px) {
  .section { padding-block: 48px; }
  .footer { padding-block: 48px; }
}

/* Keeping "Get beta access" above the fold on a short window is handled by
   fluid sizing, not by a height breakpoint — see the min(vw, vh) notes on h1
   and .phone, and the vh-scaled padding on the nav and the opening band.
   A breakpoint was tried here first and rejected: it holds one size until the
   threshold and then jumps, which on a laptop reads as the headline snapping
   to a smaller size for no visible reason. Continuous sizing has no threshold
   to cross, and gives a bigger headline than the breakpoint did at every
   height above the floor. */
