/* ============================================================================
   The homepage's CINEMATIC layer. This is the 35% of the 65/35 rule, and it is
   deliberately quarantined in its own file that only index.html loads: nothing
   here may leak into a screen somebody uses every day.

   HOW THE MOTION WORKS, because it is unusual and worth stating once.
   Every scroll animation here is a CSS scroll-driven animation running on the
   compositor, with no JavaScript and no scroll listener. The pattern is always:

     .thing { animation-name: x; animation-duration: 1ms; animation-fill-mode: both }
     @supports (animation-timeline: view()) { .thing { animation-timeline: ...;
                                                       animation-duration: auto } }

   The 1ms duration is the FALLBACK for browsers without scroll-driven
   animations: they land on the animation's END state instantly, and that end
   state is the finished, assembled, fully readable page. Nothing is ever
   hidden behind motion and there is no scroll listener to jank.

   REDUCED MOTION NEEDS ITS OWN BLOCK, at the bottom of this file, and the
   reason is worth writing down because it is a trap. The global reduced-motion
   rule in system.css clamps animation-duration to 0.001ms with !important,
   which disables ordinary animations. It does NOT disable these: once an
   animation has a scroll timeline, duration is driven by the timeline and the
   declared duration is ignored, !important or not. Measured before the fix:
   with prefers-reduced-motion set, the six food discs sat at opacity 0 to 0.57
   and stayed there, so the hero was a half-empty plate that never resolved.
   Killing the TIMELINE is what actually stops them.
   ========================================================================= */

/* ---- NAVIGATION (§42) ----------------------------------------------------
   The bar FLOATS over the page rather than sitting above it. That is the
   design intent, and it also fixes a real bug: while the bar occupied 100px
   of layout, the hero's 100vh sticky scene began at y=80 and its buttons
   landed 49px below the fold on a 900px-tall screen.

   It densifies on scroll with a scroll-driven animation rather than a scroll
   listener, so there is no work on the main thread and nothing to throttle.

   STICKY, not fixed. Fixed chrome permanently covers whatever sits under it at
   maximum scroll, which scripts/responsive catches as "topbar rests on ...  at
   the end of the page", and it is a real defect rather than a pedantic one: on
   a floating bar the last section of every page is unreadable. Sticky keeps the
   glass and the float while staying in the layout, so nothing is ever hidden.
   The hero then sizes itself to the viewport MINUS the bar, below. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: rgba(var(--bar-rgb), 0.58);
  border-bottom: 1px solid transparent;
  animation-name: navDensify;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: linear;
}
/* one number for the bar's height, because the hero subtracts it */
:root {
  --nav-h: 76px;
}
@media (max-width: 900px) {
  :root {
    --nav-h: 64px;
  }
}
.topbar-in {
  min-height: var(--nav-h);
  transition: min-height var(--motion-medium) var(--ease-standard);
}
@keyframes navDensify {
  from {
    background: rgba(var(--bar-rgb), 0.5);
    border-bottom-color: transparent;
    box-shadow: none;
  }
  to {
    background: rgba(var(--bar-rgb), 0.86);
    border-bottom-color: var(--border);
    box-shadow: var(--shadow-sm);
  }
}
@supports (animation-timeline: scroll()) {
  .topbar {
    animation-timeline: scroll(root block);
    animation-range: 0 140px;
    animation-duration: auto;
  }
}

/* ---- THE HERO ------------------------------------------------------------
   REBUILT. The first version was a 210vh sticky scene with six circular food
   photographs orbiting a white ellipse, and the owner's verdict was right on
   every count: the foods never belonged to the plate, whole thalis floated
   toward another plate (a nonsense of visual grammar), the plate read as an
   empty platform, the headline clipped under the sticky header, and the CTAs
   floated free of the words they belonged to.

   What replaced it, and why:
     - the visual is a REAL PHOTOGRAPH of a composed meal, so the first frame
       already reads as "a plate of food" with no motion required at all. The
       brief's own instruction: premium 2D photography beats mediocre 3D.
     - the hero is ONE FOLD, height driven by its content. No sticky scene, so
       nothing can clip under the header and there is no unexplained void.
     - headline, supporting line and both CTAs are one column, one group. The
       CTA belongs to the sentence that earned it.
     - motion in this fold is almost nil: a single settle on entry. The story
       is told by the scenes BELOW, where each movement has a destination.
   Nothing here orbits, bobs, drifts or follows the pointer. */
.hero {
  position: relative;
  /* WITHOUT THIS THE WHOLE BACKGROUND LAYER IS INVISIBLE, and it was.
     position: relative with z-index: auto does NOT open a stacking context, so
     .hero-light and .hero-orbs - both at z-index: -1 - were painting behind the
     nearest ancestor that has a background, which is <body> at #fcfbf9. Opaque.
     The glow and the orbs were rendering correctly and being covered by the
     page itself; a screenshot is the only thing that shows it, because every
     measurement of the elements comes back exactly as authored.
     isolation: isolate makes .hero a stacking context, so negative-z children
     land behind the hero's CONTENT and in front of the page ground. */
  isolation: isolate;
  /* ONE CENTRED COLUMN, PLATE UNDERNEATH.
     It was two columns, copy left and plate right, and the composition read
     wrong from the first screenshot: the plate hung at the top of the right
     column with a third of the fold empty beneath it, and because the assembly
     clock was measured from the top of the DOCUMENT the dishes only landed once
     you had scrolled past the thing they were landing on. Stacked and centred,
     the plate is where the eye goes after the sentence rather than beside it,
     and it fills as it comes up the screen. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  text-align: center;
  gap: clamp(26px, 4vh, 46px);
  width: min(1280px, calc(100% - 40px));
  margin: 0 auto;
  padding: clamp(24px, 5vh, 56px) 0 clamp(44px, 7vh, 80px);
}

/* the luminous ground, in CurryPlate's own blues. Two soft pools, well under
   full strength, sitting behind everything and carrying no meaning of their
   own beyond "this surface is lit". */
.hero-light {
  position: absolute;
  /* vertical bleed only. The first version also bled -4% horizontally, which
     pushed the glow 39px past the page container and gave the document a
     19px sideways scroll at iPad landscape. A background glow is never worth
     a horizontal scrollbar. */
  inset: -6% 0 -10%;
  background:
    radial-gradient(
      44% 46% at 74% 46%,
      rgba(var(--accent-rgb), 0.13) 0%,
      rgba(var(--accent-rgb), 0) 68%
    ),
    radial-gradient(
      40% 40% at 18% 26%,
      rgba(var(--emerald-rgb), 0.1) 0%,
      rgba(var(--emerald-rgb), 0) 66%
    );
  pointer-events: none;
  z-index: -1;
}

/* ---- THE ORBS ------------------------------------------------------------
   Three spheres of light drifting behind the fold. What makes this the "clean"
   version rather than the one that was rejected: they are LIGHT and not FOOD.
   The rejected hero had photographs of dishes circling a plate, which is
   conceptually wrong (food belongs on the plate) and visually busy (edges the
   eye tracks). These have no edge at all - a radial gradient fading to zero -
   so peripheral vision registers a lit surface and central vision registers
   nothing to look at, which is the whole trick.

   WHY A GRADIENT AND NOT filter: blur. A blurred circle costs a full-size
   offscreen buffer re-composited every frame while it moves; a radial gradient
   that fades to transparent is the same picture with no buffer at all, and
   this page has a 268KB budget and no WebGL to spend it on.

   THE WRAPPER CLIPS, AND NOTHING MAY EVER REACH THE CLIP. An absolutely
   positioned element that drifts is the classic source of a sideways
   scrollbar, and that has been fixed on this page twice already (the hero
   glow's -4% horizontal bleed, and hero-note losing its centering transform),
   so overflow: hidden stays as the structural guarantee. But the first version
   also ANCHORED the orbs past the hero's edges, and the clip then sliced them
   at an alpha around 0.15: three soft spheres became three hard rectangular
   edges down the sides of the fold. A screenshot showed it; nothing else could.
   Every orb is now positioned so its visible disc plus its drift stays inside
   the box, and the clip is a net that is never touched. */
.hero-orbs {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}
.orb {
  position: absolute;
  display: block;
  border-radius: 50%;
  /* Fully transparent by 70% of the radius, so the outer 30% of every orb box
     is empty and the clip has nothing to cut.
     FIVE STOPS, NOT THREE. Three stops leave a visible slope change partway
     out, and where two orbs overlap that change reads as a rim: at 390px the
     blue and the cyan drew a distinct lens outline. The extra stops are not
     detail work, they are what stops a light having an edge. */
  background: radial-gradient(
    circle at 38% 34%,
    rgba(var(--orb-rgb), 0.24) 0%,
    rgba(var(--orb-rgb), 0.19) 22%,
    rgba(var(--orb-rgb), 0.11) 42%,
    rgba(var(--orb-rgb), 0.04) 58%,
    rgba(var(--orb-rgb), 0) 72%
  );
}
/* HOW STRONG THEY ARE IS A CONTRAST DECISION, not a composition one. The
   headline is #14151a and the sub-line is #5a5e6b on a #fcfbf9 ground, and no
   check measures what an orb does to either: the contrast audit walks ANCESTOR
   backgrounds, and an orb is a sibling. At 0.38 the violet sat visibly on "Eat
   what you love"; 0.24 at the core, and cores placed away from the text, keeps
   the light in the background where it belongs. */
.orb-1 {
  --orb-rgb: var(--accent-rgb);
  width: 44%;
  aspect-ratio: 1;
  top: 4%;
  right: 4%;
}
.orb-2 {
  --orb-rgb: var(--emerald-rgb);
  width: 40%;
  aspect-ratio: 1;
  bottom: 2%;
  left: 2%;
}
/* the cyan is the smallest and the faintest: it is a 2.22:1 colour, a LIGHT
   and never a surface, so it reads as a highlight rather than as a third mass.
   It lives in the gap between the copy and the plate, which is the one part of
   the fold with no text in it. */
.orb-3 {
  --orb-rgb: var(--cyan-rgb);
  width: 20%;
  aspect-ratio: 1;
  top: 6%;
  left: 48%;
  opacity: 0.7;
}

.hero-copy {
  position: relative;
  z-index: 1;
}
/* fluid, and capped so it cannot grow into the two-line monster that clipped.
   max-width in ch rather than px, so the line break lands in the same place
   at every width instead of wherever the viewport happens to put it. */
.hero-copy h1 {
  margin: 14px auto 0;
  /* wider than the old 14ch, because a centred headline no longer has a column
     to fit inside and a 14ch measure broke "Build a plate that / works for /
     you." into three ragged lines */
  font-size: clamp(2.4rem, 5.4vw, 4.4rem);
  line-height: 1.06;
  letter-spacing: var(--track-display);
  max-width: 16ch;
}
.hero-sub {
  margin: 18px auto 0;
  font-size: var(--text-lead);
  line-height: 1.55;
  color: var(--foreground-muted);
  max-width: 38ch;
}
/* attached to the copy, not floating under a picture */
.hero-actions {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: clamp(22px, 3.5vh, 34px);
}

/* The plate figure, everything inside it and its caption live in hero.css:
   the homepage animation is one self-contained module (see that file's
   header). This file owns the fold's LAYOUT and its ambient light only. */

@media (max-width: 900px) {
  .hero {
    padding-top: clamp(20px, 4vh, 40px);
    gap: clamp(22px, 4vh, 36px);
  }
  .hero-copy h1 {
    font-size: clamp(2.2rem, 8.5vw, 3rem);
    max-width: 14ch;
  }
}

/* ---- SCENES --------------------------------------------------------------
   Each scene is a statement, a short line and one piece of evidence. The
   statement carries the scale; the explanation stays small, per §59. */
.scene {
  position: relative;
  width: min(1280px, calc(100% - 40px));
  margin: clamp(80px, 16vh, 200px) auto;
}
.scene-head {
  max-width: 20ch;
}
.scene-head.wide {
  max-width: 26ch;
}
.scene-lead {
  margin-top: 22px;
}
/* .rise is inert now. It was the scroll-fade: statements animated up as they
   entered the viewport, which is the first trait the design brief rejects by
   name. The class stays in the markup (removing it would touch every scene
   for no behaviour) and means nothing; the hero's assembling plate keeps its
   own animation because it is the signature, not a fade. */
.rise {
  opacity: 1;
}

/* The page's typographic moments. Deliberately NOT full-bleed via the
   left:50%/width:100vw trick: 100vw includes the scrollbar, so on every
   desktop viewport that has one the page gained a 10px horizontal overflow
   (caught by scripts/responsive at iPad landscape). A centred max-width
   block reads identically and cannot overflow. */
.statement {
  position: relative;
  width: min(1400px, calc(100% - 24px));
  margin-inline: auto;
  /* was clamp(90px, 18vh, 220px): section heights sized for 144px slogans
     would leave 54px type floating in a field of nothing (self-critique 9.4) */
  padding: clamp(48px, 9vh, 110px) clamp(20px, 6vw, 90px);
  text-align: center;
  overflow: hidden;
}
.statement .t-statement {
  max-width: 16ch;
  margin: 0 auto;
}
/* Was the one dark rectangle in the middle of a light page, and the class name
   said so. It keeps its job - a full-width pause that the eye reads as a
   different kind of moment - but it does it with the band material and a
   hairline instead of with navy. The name is left alone deliberately: renaming
   it would touch index.html for no behaviour, and the comment is here. */
.statement-dark {
  background: var(--sky-100);
  border: 1px solid var(--border);
  color: var(--ink);
  border-radius: var(--radius-lg);
  margin-block: clamp(36px, 7vh, 90px);
}
.statement-dark .t-statement,
.statement-dark .t-section {
  color: var(--ink);
}
.statement-dark .t-lead {
  color: var(--muted);
  margin-inline: auto;
}

/* ---- MASKED IMAGE REVEAL (§46) ------------------------------------------
   The image grows out of a rounded window as it scrolls in. Used twice on the
   page, deliberately: a reveal that happens on every figure is wallpaper. */
.reveal-figure {
  position: relative;
  margin: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--media-backdrop);
  animation-name: maskOpen;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out);
}
.reveal-figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  animation-name: maskPush;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out);
}
/* The figure only ever CLIPS; the scale lives on the img, inside the clipped
   box. The first cut scaled the figure itself to 1.06 in the from-state, and
   because an element sits at its from-state until its scroll range begins,
   every one of these overflowed the page by 6% on load: scripts/responsive
   caught it as a 10px sideways scroll at iPad landscape. */
@keyframes maskOpen {
  from {
    clip-path: inset(14% 18% round var(--radius-lg));
  }
  to {
    clip-path: inset(0% 0% round var(--radius-lg));
  }
}
@keyframes maskPush {
  from {
    transform: scale(1.08);
  }
  to {
    transform: scale(1);
  }
}
@supports (animation-timeline: view()) {
  .reveal-figure,
  .reveal-figure img {
    animation-timeline: view();
    animation-duration: auto;
    animation-range: entry 0% cover 34%;
  }
}

/* ---- REGIONAL STRIP (scene 3) -------------------------------------------
   Asymmetric on purpose: the cards sit at three different heights, which is
   the spatial-composition note applied where it costs nothing in legibility. */
.regions {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(14px, 2vw, 26px);
  margin-top: clamp(34px, 6vh, 70px);
}
.region {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 3 / 4;
  background: var(--media-backdrop);
  text-decoration: none;
  display: block;
}
.regions .region:nth-child(2) {
  transform: translateY(clamp(20px, 5vh, 54px));
}
.regions .region:nth-child(3) {
  transform: translateY(clamp(-16px, -3vh, -8px));
}
.region img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--motion-slow) var(--ease-out);
}
.region:hover img {
  transform: scale(1.05);
}
.region figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 20px;
  color: var(--on-accent);
  font-weight: 700;
  font-size: var(--text-body);
  background: linear-gradient(
    180deg,
    rgba(var(--photo-scrim), 0) 0%,
    rgba(var(--photo-scrim), 0.82) 100%
  );
}
.region figcaption small {
  display: block;
  font-weight: 500;
  font-size: var(--text-caption);
  color: var(--on-accent-soft);
  margin-top: 3px;
}

/* ---- PORTION DEMO (scene 5) ---------------------------------------------
   Three bars that grow to their real proportions on scroll. Numbers support
   the picture rather than replacing it, per §38. */
.portions {
  display: grid;
  gap: 18px;
  margin-top: clamp(30px, 5vh, 56px);
  max-width: 640px;
}
.portion {
  display: grid;
  grid-template-columns: 9ch 1fr auto;
  align-items: center;
  gap: 16px;
  font-size: var(--text-ui);
  font-weight: 650;
}
.portion-track {
  height: 12px;
  border-radius: var(--radius-pill);
  background: var(--background-secondary);
  overflow: hidden;
}
/* display:block is load-bearing, not tidiness: this is a <span>, and width and
   height do not apply to an inline box. The bars measured 0x0 and rendered as
   three empty grey tracks while the animation itself reported progress=1. */
.portion-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  animation-name: growBar;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out);
}
/* FROM one width TO another, not zero to full. A bar growing from nothing
   says "loading"; a bar moving from 78% to 52% says "this portion changed",
   which is the only thing this scene exists to say. */
@keyframes growBar {
  from {
    width: var(--from, 0%);
  }
  to {
    width: var(--to, 100%);
  }
}
@supports (animation-timeline: view()) {
  .portion-fill {
    animation-timeline: view();
    animation-duration: auto;
    animation-range: entry 10% cover 30%;
  }
}
.portion-num {
  color: var(--foreground-muted);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* ---- THE PRODUCT HAND-OFF (scene 8) -------------------------------------
   The page stops performing here. This block is styled like the product, on
   purpose: it is the visual promise that the app is calm. */
.handoff {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(28px, 5vw, 72px);
  align-items: center;
  padding: clamp(28px, 4vw, 56px);
  border-radius: var(--radius-xl);
}
.capabilities {
  display: grid;
  gap: 2px;
}
.cap {
  border-top: 1px solid var(--border);
}
.cap summary {
  list-style: none;
  cursor: pointer;
  padding: 15px 2px;
  font-weight: 650;
  font-size: var(--text-body);
  display: flex;
  align-items: center;
  gap: 10px;
}
.cap summary::-webkit-details-marker {
  display: none;
}
.cap summary::before {
  content: '›';
  color: var(--accent-text);
  font-weight: 900;
  font-size: 18px;
  transition: transform var(--motion-fast) var(--ease-standard);
}
.cap[open] summary::before {
  transform: rotate(90deg);
}
.cap p {
  margin: 0 0 15px 28px;
  color: var(--foreground-muted);
  font-size: var(--text-ui);
  line-height: var(--leading-body);
}

/* ---- RESPONSIVE ----------------------------------------------------------
   WHAT WAS DELETED HERE, because it is the reason the phone hero had a 317px
   hole in it. This block still carried the rules of the 210vh sticky scene
   that was removed two rewrites ago: `.hero { min-height: 170vh }` and
   `150vh`, plus `.hero-sticky`, `.scroll-hint` and `.stage`, none of which
   have existed since. The min-heights were not inert - they made .hero 1266px
   tall at 390x844 for 738px of content, and `align-content: normal` on a grid
   is `stretch`, so the two auto rows absorbed 528px of nothing and dropped the
   plate off the bottom of the screen.
   `.food:nth-child(5), .food:nth-child(6) { display: none }` was worse than
   inert: restoring the original animation brought the `.food` class back, and
   that rule silently hid two dishes at 640px and below. A dead CSS selector
   is only dead until somebody reuses the name, and nothing in the gate reads
   stylesheets for selectors that match nothing. */
@media (max-width: 1024px) {
  .handoff {
    grid-template-columns: 1fr;
  }
}
@media (max-width: 900px) {
  .regions {
    grid-template-columns: 1fr;
  }
  .regions .region {
    aspect-ratio: 16 / 10;
    transform: none;
  }
  .regions .region:nth-child(2),
  .regions .region:nth-child(3) {
    transform: none;
  }
}
@media (max-width: 640px) {
  .portion {
    grid-template-columns: 7.5ch 1fr auto;
    gap: 10px;
    font-size: var(--text-caption);
  }
}

/* A short viewport cannot hold the fold's full height AND its headline, so
   the hero gives up its minimum rather than clipping the type. Landscape
   phones are the case this exists for. */
@media (max-height: 560px) {
  .hero-copy {
    position: static;
    animation: none;
  }
}

/* ---- REDUCED MOTION ------------------------------------------------------
   See the header of this file. system.css clamps animation-duration globally,
   which is enough for time-driven animations and does NOTHING for these: a
   scroll-driven animation takes its progress from the timeline, so the only
   way to stop one is to take the timeline away. Detaching it returns each
   element to the 1ms document-timeline animation declared above, which lands
   it on its END state: the plate arrives assembled, the bars arrive full, the
   figures arrive open. The page is complete and still, which is the promise. */
@media (prefers-reduced-motion: reduce) {
  /* the plate, the food and the halo are NOT listed here. They are hero.css's,
     and they were listed here too until the duplicate was spotted: one rule
     written twice is the failure this codebase keeps repeating, and the copy
     that lives beside the animation is the one that can be kept true. */
  .hero-copy,
  .rise,
  .reveal-figure,
  .reveal-figure img,
  .portion-fill,
  .topbar {
    animation-timeline: none !important;
    animation-range: normal !important;
  }
  .hero-copy {
    opacity: 1;
  }
}

/* ---- SCENE 2: THE CHOICE -------------------------------------------------
   A MENU with four dishes taken out of it, and the four picked ones land one
   after another as the section scrolls in.

   WHY IT IS NO LONGER FOUR ROWS. The previous cut was four full-width rows
   that all ended ticked, and it failed the scene's own heading three ways.
   Nothing was ever shown UNPICKED, so the selected state carried no
   information and the group read as a completed form. The second column held
   four different taxonomies - "Tamil Nadu" (a region), "Lentils & vegetables"
   (ingredients), "Side" (a role) and "Probiotic" (a nutrition tag) - so the
   eye could not learn what that column was. And four dishes is the worst
   available evidence for a sentence that says "over 200"; fourteen reads as a
   menu, four reads as four.

   WHY CHIPS AND NOT A LIST. Chips size to their own text, so the dead 250px
   gap between the dish name and its label in a 560px row is gone, and they
   wrap into rows on their own at every width without a horizontal scrollbar.

   THE PICKED ONES ARE THE ONLY THINGS THAT MOVE. The rest of the menu is
   static, which is what makes the four landings read as choosing rather than
   as a page finishing loading. */
.dish-picks {
  list-style: none;
  margin: clamp(26px, 4vh, 44px) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  max-width: 760px;
}
.dish-pick {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 16px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font-size: var(--text-ui);
  font-weight: 650;
  color: var(--foreground-muted);
  /* the menu must not reflow as the picked ones gain their tick: the badge is
     laid out from the first frame and only its opacity and scale animate */
  white-space: nowrap;
}
.dish-pick.is-picked {
  color: var(--foreground);
  animation-name: pickOn;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out);
}
.dp-tick {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 900;
  color: var(--on-accent);
  background: var(--accent);
}
/* PICKED IS NOT SAID IN COLOUR ALONE. The tick is a shape, which answers it
   for anyone looking, and it carries role="img" aria-label="Picked", which
   answers it for anyone listening.
   IT IS NOT A VISUALLY-HIDDEN SPAN, and that is not a style preference. The
   first cut used the usual 1px-box-plus-clip-path idiom and responsive.test
   failed 32 times with "span is clipped by 47px". The check was right: a box
   that clips its own text is exactly what it exists to find, and it cannot
   tell a deliberate screen-reader-only clip from an accidental one. Naming the
   badge instead adds no text node for it to measure. */
@keyframes pickOn {
  from {
    border-color: var(--border);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
  }
  to {
    border-color: var(--accent);
    background: rgba(var(--accent-rgb), 0.07);
    box-shadow: var(--shadow-sm);
  }
}
/* the tick arrives with the chip rather than sitting there from the start */
.dish-pick .dp-tick {
  animation-name: tickIn;
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-spring);
}
@keyframes tickIn {
  from {
    opacity: 0;
    transform: scale(0.4);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@supports (animation-timeline: view()) {
  .dish-picks {
    view-timeline-name: --picks;
    view-timeline-axis: block;
  }
  .dish-pick.is-picked,
  .dish-pick .dp-tick {
    animation-timeline: --picks;
    animation-duration: auto;
  }
  /* one after another, so it reads as choosing rather than as a page loading.
     Named classes rather than :nth-child, because the picked ones are spread
     THROUGH the menu and their positions are a composition decision that
     should not be re-derived from a selector every time a dish is added. */
  .dish-pick.p1,
  .dish-pick.p1 .dp-tick {
    animation-range: entry 18% entry 34%;
  }
  .dish-pick.p2,
  .dish-pick.p2 .dp-tick {
    animation-range: entry 30% entry 46%;
  }
  .dish-pick.p3,
  .dish-pick.p3 .dp-tick {
    animation-range: entry 42% entry 58%;
  }
  .dish-pick.p4,
  .dish-pick.p4 .dp-tick {
    animation-range: entry 54% entry 70%;
  }
}
@media (prefers-reduced-motion: reduce) {
  .dish-pick.is-picked,
  .dish-pick .dp-tick {
    animation-timeline: none !important;
    animation-range: normal !important;
  }
}

/* only the portions that MOVED are marked, so the eye goes to the change
   rather than to a table of three equal rows */
.portion.is-changed .portion-fill {
  background: var(--accent);
  box-shadow: var(--shadow-sm);
}
.portion:not(.is-changed) .portion-fill {
  background: var(--sky-400);
}
.portion-num s {
  color: var(--foreground-muted);
  text-decoration-thickness: 1px;
  margin-right: 6px;
  font-weight: 500;
}
.portion-note {
  margin: 18px 0 0;
  font-size: var(--text-caption);
  font-weight: 650;
  color: var(--accent-text);
}

/* ============================================================================
   THE HERO'S MOTION. Added after the static rebuild, because "correct but
   inert" is its own failure: the fold was right and it felt like nothing.

   The rule I am following, so this does not slide back into the orbiting
   version: seamlessness does not come from MORE moving objects. It comes from
   FEWER objects moving for longer. There are exactly three things in motion
   here, and they run once:

     1. the headline arrives, one line at a time, from behind its own baseline
     2. the plate arrives and a ring of light travels around it, once
     3. the plate then stays in motion with the SCROLL, continuously, across
        the fold boundary into the next scene

   (3) is the seamless part. The plate is not a hero decoration that ends where
   the section ends: it keeps scaling and rising as the page moves, so the eye
   never registers a cut between "hero" and "next section". Nothing loops,
   nothing bobs, nothing follows the pointer.
   ========================================================================= */

/* ---- 1. THE HEADLINE ------------------------------------------------------
   Each line is a mask; the inner span rises out of it. This is why the effect
   reads as typography rather than as a fade: the letters are revealed by their
   own line box, so nothing ever overlaps the line above. */
.hero-h1 .line {
  display: block;
  overflow: hidden;
  /* the mask must clear the descenders or the tail of "p" is shaved off */
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}
.hero-h1 .line-in {
  display: block;
  animation: lineRise 1s var(--ease-out) both;
}
.hero-h1 .line:nth-child(1) .line-in {
  animation-delay: 120ms;
}
.hero-h1 .line:nth-child(2) .line-in {
  animation-delay: 260ms;
}
@keyframes lineRise {
  from {
    transform: translateY(105%);
  }
  to {
    transform: none;
  }
}
/* the supporting elements follow the headline rather than arriving with it */
.hero-rise {
  animation: heroRise 800ms var(--ease-out) both;
  animation-delay: var(--d, 0ms);
}
@keyframes heroRise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* 2 and 3, the plate's arrival and its scroll hand-off, moved to hero.css
   with the rest of the assembling plate. */

/* ---- 4. THE LIGHT BREATHES -----------------------------------------------
   The only looping thing on the page, and it is a background glow with no
   edges: 18 seconds, opacity and scale only, nothing a person can consciously
   track. This is what stops the fold feeling frozen once the entrance is done. */
.hero-light {
  animation: lightBreathe 18s ease-in-out infinite;
}
@keyframes lightBreathe {
  0%,
  100% {
    opacity: 0.9;
    transform: scale(1);
  }
  50% {
    opacity: 1;
    transform: scale(1.06);
  }
}

/* ---- 4b. THE ORBS DRIFT ---------------------------------------------------
   Three different periods (34s, 44s, 27s), deliberately not multiples of each
   other, so the arrangement never repeats within the time anyone looks at the
   fold. That is the difference between "drifting" and "looping": a common
   period is visible within about three cycles and instantly reads as a GIF.

   transform only. No top/left, no width, no opacity keyframes on the two large
   orbs: those are layout and paint, and this runs behind text that must stay
   crisp while it scrolls. */
.orb-1 {
  animation: orbDrift1 34s ease-in-out infinite;
}
.orb-2 {
  animation: orbDrift2 44s ease-in-out infinite;
}
.orb-3 {
  animation: orbDrift3 27s ease-in-out infinite;
}
@keyframes orbDrift1 {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(-5%, 6%, 0) scale(1.06);
  }
}
@keyframes orbDrift2 {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(8%, -6%, 0) scale(0.93);
  }
}
@keyframes orbDrift3 {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(0.96);
  }
  50% {
    transform: translate3d(-9%, -11%, 0) scale(1.12);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-h1 .line-in,
  .hero-rise,
  .hero-light {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    translate: none !important;
    scale: none !important;
  }
  /* the orbs get their own clause rather than joining the one above, because
     that one forces opacity: 1 and orb-3 is deliberately held at 0.62. Stillness
     is what was asked for, not a brighter page. */
  .orb {
    animation: none !important;
  }
}

/* ============================ v3 §2.4 ACTS ============================== */
/* ACT 1 lives on the hero gradient: the one deliberate dark room on the
   site (documented exception in scripts/no-dark-bands.mjs). The copy flips
   to the on-dark set; the plate stays a lit object on it.

   FULL BLEED. Deployed, the room rendered as an inset rectangle floating in
   white gutters under a white bar - the owner's review: "the entire page
   should be that background color.. not just that portion. so odd." The
   room now runs edge to edge and the bar sits inside it (below); the copy
   keeps its 1280px measure through padding rather than a narrowed section. */
.hero {
  background: var(--grad-hero);
  width: 100%;
  margin: 0;
  padding-inline: max(20px, calc((100% - 1280px) / 2));
  /* clip, not hidden: the assembling dishes wait OUTSIDE the frame, and at
     full bleed there is no gutter left to absorb them - a waiting dish must
     not scroll the document sideways (hero-assembly pins this at 360px).
     clip cannot become a scroll container the way hidden can. */
  overflow-x: clip;
}
/* THE BAR BELONGS TO THE ROOM. A white glass bar over the dark hero was the
   seam the owner saw, and a TRANSLUCENT navy one only traded it for a grey
   band: the sticky bar sits over the white page body, not over the hero, so
   any transparency lets white bleed through. Solid navy instead - the exact
   colour of the hero gradient's top stop (both are var(--foreground)) - so
   bar and room meet with no visible edge and the whole first screen is one
   surface. Scrolled, only a hairline and a shadow arrive; the colour never
   changes, and the on-dark text is legible over the light acts too. */
.topbar {
  background: rgb(var(--ink-rgb));
}
@keyframes navDensify {
  from {
    background: rgb(var(--ink-rgb));
    border-bottom-color: transparent;
    box-shadow: none;
  }
  to {
    background: rgb(var(--ink-rgb));
    border-bottom-color: rgba(var(--white-rgb), 0.14);
    box-shadow: var(--shadow-sm);
  }
}
.topbar .brand,
.topbar .nav-links a,
.topbar .nav-signin,
.topbar .nav-burger {
  color: var(--on-accent);
}
.topbar .brand small {
  color: rgba(var(--white-rgb), 0.72);
}
.topbar .nav-links a:hover,
.topbar .nav-links a.here,
.topbar .nav-signin:hover {
  color: var(--accent-cyan);
}
.topbar .nav-signin.is-in {
  color: var(--accent-mint);
}
.hero .eyebrow {
  color: var(--accent-cyan);
}
.hero-h1,
.hero-h1 .line-in {
  color: var(--on-accent);
}
.hero-sub {
  color: var(--link-on-dark);
}
.hero-note {
  color: var(--link-on-dark);
}
.hero .btn.secondary {
  /* §2.2's secondary: glass over teal - translucent white, hairline, blur */
  background: rgba(var(--white-rgb), 0.12);
  color: var(--on-accent);
  border-color: rgba(var(--white-rgb), 0.35);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* ACT 2: three real intake questions as cards */
.act-qs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
  gap: clamp(12px, 2vw, 22px);
  margin-top: clamp(26px, 4vh, 52px);
}
.act-q {
  display: grid;
  gap: 10px;
  align-content: start;
  padding: clamp(16px, 2.2vw, 26px);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-raised);
  text-align: left;
}
.act-q b {
  font-size: var(--text-feature);
  letter-spacing: var(--track-tight);
}
.act-q small {
  color: var(--foreground-muted);
  line-height: 1.5;
}
.act-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.act-chip {
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--foreground-muted);
}
.act-chip.on {
  background: var(--sky-400);
  border-color: var(--accent);
  color: var(--accent-text);
}
/* ACT 3: the two doors, shown as themselves */
.act-doors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
  gap: clamp(12px, 2vw, 22px);
  margin: clamp(24px, 4vh, 44px) auto 0;
  max-width: 640px;
}
.act-door {
  display: grid;
  gap: 6px;
  padding: clamp(16px, 2.2vw, 24px);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* the same tinted blue-into-green glass the app's real doors wear, so the
     landing's preview and the product are one object (color + image set
     separately for the same grounded-guard reason as .door) */
  background-color: var(--blue-150);
  background-image: var(--grad-wash);
  box-shadow: var(--bevel-raised), var(--shadow-raised);
  text-align: left;
}
.act-door small {
  color: var(--foreground-muted);
}
.act-typed {
  margin: clamp(20px, 3vh, 36px) auto 0;
  font-size: var(--text-lead);
  color: var(--foreground-muted);
  max-width: 44ch;
}
.act-typed span {
  color: var(--foreground);
  font-weight: 700;
  border-bottom: 2px solid var(--accent-cyan);
}
/* ACT 4: the phone the plan arrives on */
.act-phone {
  margin: clamp(26px, 4vh, 52px) auto 0;
  max-width: 420px;
  padding: clamp(16px, 2.4vw, 26px);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-md);
}
.act-phone-hd {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 12px;
}
.act-phone-hd small {
  color: var(--foreground-muted);
}
.act-phone .tk-week {
  grid-template-columns: repeat(auto-fit, minmax(44px, 1fr));
  gap: 6px;
}
.act-phone .tk-thali,
.act-phone .tk-thali svg {
  /* the wrapper carries a full-size width in system.css; unconstrained it
     kept 151px inside a 44px grid cell and handed the page a sideways
     scrollbar at 360px - the exact bug the hero's comment warns about */
  width: 44px;
  height: 44px;
  margin: 0;
  display: block;
}
/* ACT 5: one button */
.act-final .btn-lg {
  font-size: var(--text-feature);
  padding: 15px 30px;
}

/* ---- THE POCKET: store badges, and the gloss the owner asked for ---------
   ("use glossy effect, clicking button effect wherever possible"). The press
   already lives in system.css's :active rules; what was missing is the LIGHT
   sitting on the pill. The sheen is a ::before because [data-tactile] owns
   ::after for its ripple, and the two must never fight over one slot. */
.store-row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}
.store-badge {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  padding: 11px 20px 11px 15px;
  border-radius: var(--radius-md);
  color: var(--ink);
  text-align: left;
  background: linear-gradient(165deg, var(--frost-a), var(--frost-b));
  border: 1px solid color-mix(in srgb, var(--ink) 16%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--ink) 14%, transparent),
    var(--shadow-sm);
  /* stated, not clickable: the stores are coming, and a badge that LOOKS
     pressable but goes nowhere is the broken promise the aria-label avoids */
  opacity: 0.85;
}
.store-badge svg {
  width: 26px;
  height: 26px;
}
.store-badge small {
  display: block;
  font-size: var(--text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.store-badge b {
  display: block;
  font-size: var(--text-ui);
  line-height: 1.15;
}
/* GLOSS: a fixed top-light plus a slow sheen that sweeps the pill and rests.
   White on the gold raises its lightness, so the navy ink only gains. */
.glossy {
  position: relative;
  overflow: hidden;
}
.glossy::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(var(--white-rgb), 0.34), rgba(var(--white-rgb), 0) 46%),
    linear-gradient(105deg, transparent 40%, rgba(var(--white-rgb), 0.3) 50%, transparent 60%);
  background-size:
    100% 100%,
    240% 100%;
  background-position:
    0 0,
    -140% 0;
  background-repeat: no-repeat;
  animation: ctaSheen 5.6s ease-in-out infinite;
}
@keyframes ctaSheen {
  0%,
  62% {
    background-position:
      0 0,
      -140% 0;
  }
  86%,
  100% {
    background-position:
      0 0,
      240% 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .glossy::before {
    animation: none;
  }
}
