/* ============================================================================
   THE LAW OF THIS FILE

   Picture geometry is PHYSICAL. Document chrome is LOGICAL.

   Everything inside #arc and #ring is a photograph: left, top, transform,
   transform-origin, and GSAP x/y. Those must come out pixel-identical in he
   and en, and tools/checks/09-rtl.mjs asserts exactly that.

   Everything outside them is chrome: inset-inline-start, padding-inline,
   margin-inline. Those flip with direction, which is correct.

   The trap this prevents: a chip positioned with inset-inline-start:0 becomes
   right:0 in Hebrew, every computed x mirrors, and the ring appears reflected
   ONLY in the Hebrew build.
   ========================================================================= */

/* ----------  shared type  ------------------------------------------------ */
.kicker {
  font-family: var(--f-body);
  font-weight: 600;
  font-size: var(--t-kicker);
  letter-spacing: var(--track-kicker);
  color: var(--olive);
  margin: 0 0 1rem;
}
.h, .h2 {
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: var(--track-h);
  line-height: var(--lead-h);
  margin: 0;
  color: var(--ink);
  text-wrap: balance;
}
.h  { font-size: var(--t-h); }
.h2 { font-size: var(--t-h2); }
.lede {
  font-family: var(--f-air);
  font-weight: 300;
  font-size: var(--t-lede);
  line-height: 1.6;
  color: var(--ink-2);
  margin: 1.2rem auto 0;
  max-width: 42ch;
}

/* ----------  hero stage  ------------------------------------------------- */
#hero { position: relative; }

.stage {
  position: sticky;
  top: 0;
  /* svh, never vh. On iOS vh is the LARGE viewport, so a 100vh sticky stage
     is taller than what is visible while the address bar is showing and the
     bottom of the plate is cropped. */
  height: 100svh;
  /* clip, never hidden. hidden would make this element a scroll container
     for its own descendants and is the classic way to kill sticky. */
  overflow: clip;
  background: var(--cream);
}

.tint {
  position: absolute; inset: 0; z-index: 0;
  background: var(--cream);
}

.scroll-hint {
  position: absolute; z-index: 5;
  inset-block-end: .7rem; inset-inline: 0;       /* chrome -> logical */
  margin: 0; text-align: center;
  font-size: .68rem;
  letter-spacing: var(--track-kicker);
  color: var(--stone);
}

/* ----------  act 1: the arc  --------------------------------------------- */
/* The stage holds six plates on a circle whose centre is far above it. All
   six live in one box; js/sections/wheel.js gives each a transform from
   js/arc.js. Nothing here positions a plate - CSS owns the SIZE, js owns the
   SEAT, and the two never negotiate.

   No marker, no veils, no clip-paths. The plate at the bottom of the arc is
   the one being read, and that is the whole mechanic. What this replaced was
   one photograph of a six-wedge plate with five cream sectors laid over it,
   which required a CSS circle to land exactly on a photographed rim - three
   measured numbers that had to agree and never quite did. */
.arc {
  position: absolute; inset: 0; z-index: 1;
}
/* A <picture> is display:inline by default, so it carries a baseline gap and
   the <img> inside it comes out a few pixels short of the box it is meant to
   fill. Every picture wrapper on this page is a layout-transparent box. */
picture { display: contents; }

.plate {
  position: absolute;
  /* Seated from the CENTRE of the stage. js adds x/y on top of this, so the
     translate has to come first in the matrix - which it does, because GSAP
     composes its own transforms after the ones written here. */
  left: 50%; top: 46%;
  width: min(62vw, 62svh);
  aspect-ratio: 1;
  margin-left: calc(min(62vw, 62svh) / -2);
  margin-top: calc(min(62vw, 62svh) / -2);
}
.plate picture { display: block; position: absolute; inset: 0; }
/* The plates ship as cutouts cropped to their own silhouette and centred in a
   square by tools/images.py, so the frame IS the plate: contain, and nothing
   to re-centre or fade. */
.plate-img { width: 100%; height: 100%; object-fit: contain; }
/* The cut took the shoot's shadow with it, and a plate with no shadow is a
   sticker. One soft ellipse under the rim puts it back on a table. */
.plate-cast {
  position: absolute; z-index: -1;
  left: 50%; bottom: 2%;
  width: 82%; height: 8%;
  transform: translateX(-50%);
  background: radial-gradient(50% 50% at 50% 50%, rgba(26,23,18,.22), transparent 72%);
  pointer-events: none;
}

/* ----------  act 1: copy  ------------------------------------------------ */
.wheel-copy {
  position: absolute; z-index: 3;
  left: 50%; top: 84%;
  transform: translate(-50%, -50%);
  width: min(100%, 46rem);
  padding-inline: var(--pad-x);
  text-align: center;
}
/* Six blocks stacked in one place. Only opacity and transform animate, so a
   change costs no layout at all. */
.wc { grid-area: 1 / 1; }
.wheel-copy { display: grid; }
.wc-origin {
  font-family: var(--f-body); font-weight: 600;
  font-size: var(--t-kicker); letter-spacing: var(--track-kicker);
  color: var(--olive); margin: 0 0 .7rem;
}
.wc-name {
  font-family: var(--f-display); font-weight: 800;
  font-size: var(--t-name); line-height: var(--lead-h);
  letter-spacing: var(--track-h); margin: 0; color: var(--ink);
}
.wc-note {
  font-family: var(--f-air); font-weight: 300;
  font-size: var(--t-lede); line-height: 1.55; color: var(--ink-2);
  margin: .9rem auto 0; max-width: 38ch;
}
.wc-chips {
  list-style: none; margin: 1.1rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .45rem; justify-content: center;
}
.wc-chips li {
  font-size: var(--t-kicker); color: var(--stone);
  border: 1px solid var(--hair); border-radius: 100px;
  padding: .28rem .8rem;
}

/* ----------  act 2: the exploded ring  ----------------------------------- */
#ring {
  position: absolute; inset: 0; z-index: 4;
  opacity: 0; visibility: hidden;
  /* This is picture space, not text space. Bidi reordering has no business
     shifting a chip's inline boxes relative to its own image. */
  direction: ltr;
}
.ring-probe {
  position: absolute;
  left: var(--bowl-x); top: var(--bowl-y);
  width: var(--ring-r); height: 0;
  visibility: hidden; pointer-events: none;
}

.ing {
  position: absolute; left: 0; top: 0;            /* NEVER inset-inline-start */
  display: flex; align-items: center;
}
/* Every chip occupies the same square, whatever shape its subject is.
   The cutouts are cropped to their own content, so their aspect ratios run
   from 0.43 (a tahini ribbon) to 1.04 (a heap of chickpeas). Sizing on width
   alone made the tahini two and a half times taller than the chickpeas and it
   took over the ring. A square box with contain keeps the ring even and lets
   each subject sit at its own natural proportion inside it. */
/* Act 2 draws the ring at full size around a plate that never leaves, so the
   chips are read as objects rather than as a legend. */
.ing .cut {
  width: clamp(56px, 7.6vw, 128px);
  aspect-ratio: 1;
  height: auto;
  object-fit: contain;
}

/* The chip's own soft shadow is a sibling element, not filter: drop-shadow().
   A large-radius drop-shadow on eight simultaneously animating layers forces
   a full recomposite every frame. */
html:not(.has-photos) .ing .cast { display: none; }
.ing .cast {
  position: absolute; left: 50%; bottom: -4%;
  width: 66%; height: 11%;
  transform: translateX(-50%);
  background: radial-gradient(50% 50% at 50% 50%, rgba(26,23,18,.16), transparent 72%);
  pointer-events: none;
}
.ing .line {
  position: absolute; top: 50%; height: 1px;
  width: clamp(26px, 3.6vw, 62px);
  background: var(--olive); opacity: .5;
}
.ing .label {
  position: absolute; top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--f-body); font-weight: 600;
  font-size: var(--t-kicker); letter-spacing: .04em;
  color: var(--ink-2);
}
/* Quadrant-driven, from Math.sin(deg) - which side of the plate the chip
   landed on. This has nothing to do with html[dir] and must never be
   "fixed" with a logical property: the connector always points home. */
.ing--r .line  { right: 100%; transform-origin: right center; }
.ing--l .line  { left: 100%;  transform-origin: left center; }
.ing--r .label { left: calc(100% + .55rem); }
.ing--l .label { right: calc(100% + .55rem); }

/* The container opted out of RTL above, so Hebrew inside a label opts back
   in explicitly, isolated so a mixed run never reorders across the boundary. */
html[lang="he"] .ing .label { direction: rtl; unicode-bidi: isolate; }

/* ----------  the scroll budget  ------------------------------------------ */
.act { pointer-events: none; }
#act-wheel   { height: calc(var(--act1-screens) * 100svh); }
#act-explode { height: calc(var(--act2-screens) * 100svh); }

/* ----------  recipe: the dark chamber  ----------------------------------- */
#recipe {
  position: relative;
  background: var(--charcoal);
  color: var(--cream);
  padding-block: clamp(5rem, 12vh, 10rem);
}
/* A whisper of grain on the chamber (MotionSites 3.9 - feTurbulence as a
   data URI). The sticky panels paint over it; it textures the head and the
   seams, never the photographs. */
#recipe::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: .05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
#recipe .kicker { color: var(--chickpea); }
#recipe .h2     { color: var(--cream); }
#recipe .lede   { color: rgba(245,240,230,.62); }
.recipe-head { text-align: center; padding-inline: var(--pad-x); max-width: 52rem; margin-inline: auto; }

.rule {
  position: absolute; top: 0; bottom: 0; width: 1px;
  inset-inline-start: clamp(1rem, 4vw, 4rem);     /* chrome -> logical. Right in Hebrew. */
  background: rgba(245,240,230,.14);
}
.rule-fill {
  display: block; width: 100%; height: 100%;
  background: var(--chickpea);
  transform: scaleY(0); transform-origin: top center;
}

/* Full-bleed sticky panels that stack: each step is one screen, the
   photograph fills it edge to edge, and the next step scrolls over the
   previous one (MotionSites sticky card stacking). The stack works because
   every li sticks at top:0 inside the flowing ol - a panel holds its place
   until the list ends, and each later sibling paints above it in DOM order. */
.steps { list-style: none; margin: clamp(3rem,8vh,6rem) 0 0; padding: 0; }
.step {
  position: sticky; top: 0;
  height: 100svh;
  overflow: clip;
  /* The chamber, not a cream card floating in it. The subjects ship as
     cutouts (tools/images.py treats steps as an alpha family), so there is
     no sweep to hide - each one simply stands in the dark. */
  background: var(--charcoal);
}
.step-media {
  position: absolute; inset: 0;
  /* The subject is an object in a room, not a backdrop. It gets the middle
     of the panel and the room gets the rest. */
  display: grid; place-items: center;
  padding: clamp(2rem, 9vh, 7rem) var(--pad-x) clamp(9rem, 26vh, 16rem);
  will-change: transform;
}
/* contain and capped. cover cropped the subject out of its own frame; an
   uncapped contain simply made the cutout as tall as the room. */
.step-media img {
  width: auto; height: auto;
  max-width: min(58vw, 42rem);
  max-height: 100%;
  object-fit: contain;
}
/* The cutout lost the shoot's contact shadow with its background, and a
   subject with no shadow reads as a sticker. One soft ellipse under it puts
   it back on a floor. Same trick as .ing .cast, and for the same reason it
   is an element and not filter: drop-shadow(). */
.step-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(56% 30% at 50% 74%, rgba(0,0,0,.55), transparent 70%),
    radial-gradient(48% 44% at 50% 40%, rgba(245,240,230,.09), transparent 72%);
}

/* The house glass (MotionSites - 23 of 76 templates, word for word).
   The two expensive gotchas, honoured here: children that need absolute
   positioning must say so themselves, and NOTHING above this element in the
   stacking order may keep a transform after its animation ends - a retained
   ancestor transform silently disables backdrop-filter. That is why the
   stack scale-down in js/sections/recipe.js animates .step-media, never
   .step itself. */
.liquid-glass {
  /* The house value, unchanged: white 0.01 is tuned for glass over dark
     ground, and the chamber is exactly that. It was briefly flipped to a
     charcoal tint while the panels were still cream - the tint is a
     consequence of the ground, not a preference. */
  background: rgba(255, 255, 255, 0.01);
  background-blend-mode: luminosity;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: none;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
  position: relative;
  overflow: hidden;
}
.liquid-glass::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit; padding: 1.4px;
  background: linear-gradient(180deg,
    rgba(255,255,255,0.45) 0%, rgba(255,255,255,0.15) 20%,
    rgba(255,255,255,0) 40%, rgba(255,255,255,0) 60%,
    rgba(255,255,255,0.15) 80%, rgba(255,255,255,0.45) 100%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

.step-copy {
  position: absolute;                              /* over the glass default */
  inset-inline-start: var(--pad-x);                /* chrome -> logical */
  inset-block-end: clamp(1.5rem, 6svh, 4rem);
  max-width: min(34rem, calc(100% - 2 * var(--pad-x)));
  padding: clamp(1.4rem, 3vw, 2.4rem) clamp(1.6rem, 3.4vw, 2.8rem);
  border-radius: 22px;
  color: var(--cream);
  /* Entrance: opacity only on the glass itself - a transitioned transform
     that settles at translateY(0) still counts as a retained transform in
     some engines and kills the blur. The rise lives on the children. */
  opacity: 0;
  transition: opacity .6s var(--ease-house);
}
.step.is-in .step-copy { opacity: 1; }
.step-n {
  display: block;
  font-family: var(--f-display); font-weight: 300;
  font-size: clamp(2.6rem, 6vw, 5.2rem); line-height: 1;
  color: var(--chickpea); margin-bottom: .4rem;
}
.step-copy h3 {
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(2rem, 4.6vw, 4rem); line-height: var(--lead-h);
  letter-spacing: var(--track-h); margin: 0; color: var(--cream);
}
/* Word-by-word rise with blur (MotionSites 3.3): each word waits its turn
   on the house ladder and comes up from behind the clip line. */
.w-clip { display: inline-block; overflow: hidden; vertical-align: bottom; }
.w {
  display: inline-block;
  opacity: 0; transform: translateY(110%); filter: blur(4px);
  transition: opacity .55s var(--ease-house), transform .55s var(--ease-house),
              filter .55s var(--ease-house);
  transition-delay: calc(var(--wi, 0) * 90ms);
}
.step.is-in .w { opacity: 1; transform: translateY(0); filter: blur(0); }
.step-copy p { font-family: var(--f-air); font-weight: 300;
               font-size: var(--t-lede); line-height: 1.6;
               color: rgba(245,240,230,.82); margin: .8rem 0 0; max-width: 34ch; }
.step-time { display: inline-block; margin-top: .9rem; font-size: var(--t-kicker);
             letter-spacing: var(--track-kicker); color: rgba(245,240,230,.6); }

/* ----------  find: the recommender  -------------------------------------- */
/* Comes out of the dark recipe chamber into daylight - it is the one place
   the reader acts, so it reads as a form, not a poster. The site's own cream
   and Basalt, never the recipe's charcoal. */
#find {
  /* Full-bleed, like #close below it, so the two paper sections read as one
     continuous field. The background must reach the viewport edges - capping
     the SECTION at 54rem left a lighter rectangle floating on the darker
     --cream body with hard seams down both sides. The width cap belongs on
     the CONTENT, not the ground it sits on. */
  background: var(--paper);
  padding: 0 var(--pad-x) clamp(4rem, 10vh, 7rem);
}
#find > * { max-width: 54rem; margin-inline: auto; }

/* The atmosphere band: full-bleed, flush to the top of the section, dissolving
   into the cream below so the heading sits on paper, not on a hard photo edge.
   The #find > * cap and centering are overridden here (higher specificity via
   the id) so the band reaches both viewport edges. */
#find > .find-atmo {
  max-width: none;
  margin: 0 calc(-1 * var(--pad-x)) clamp(2rem, 6vh, 3.5rem);
  position: relative;
  height: clamp(150px, 32vh, 320px);
  overflow: hidden;
}
.find-atmo picture { display: block; height: 100%; }
.find-atmo img { width: 100%; height: 100%; object-fit: cover; object-position: center 42%; display: block; }
/* Fade the photo's warm cream into --paper along the bottom so there is no
   seam where the band meets the questions. */
.find-atmo::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to bottom, transparent 52%, var(--paper) 98%);
}
/* First question then wants breathing room from the band. */
#find .find-head { padding-top: clamp(2.5rem, 7vh, 4.5rem); }
.find-head { text-align: center; max-width: 40rem; margin-inline: auto; }
.find-step { margin-top: clamp(2.2rem, 6vh, 3.5rem); }
.find-q {
  font-family: var(--f-body); font-weight: 600;
  font-size: var(--t-kicker); letter-spacing: var(--track-kicker);
  color: var(--olive); text-align: center; margin: 0 0 1.1rem;
}
.find-chips {
  display: flex; flex-wrap: wrap; gap: .6rem; justify-content: center;
}
.find-chip {
  font-family: var(--f-body); font-weight: 600;
  font-size: clamp(.85rem, 1.1vw, 1rem);
  padding: .6rem 1.15rem; border-radius: 100px;
  border: 1px solid var(--hair); background: transparent; color: var(--ink);
  cursor: pointer; line-height: 1.15;
  transition: background-color .3s var(--ease-house), color .3s var(--ease-house),
              border-color .3s var(--ease-house);
}
.find-chip:hover { border-color: var(--stone); }
.find-chip.is-on { background: var(--ink); color: var(--cream); border-color: var(--ink); }
.find-chip[disabled] { opacity: .38; cursor: not-allowed; }
/* Style chips carry a one-line hint under the name. */
.find-chip-t { display: block; }
.find-chip-s {
  display: block; font-family: var(--f-air); font-weight: 300;
  font-size: .74em; color: var(--stone); margin-top: .12rem;
}
.find-chip.is-on .find-chip-s { color: rgba(245,240,230,.72); }

.find-results { margin-top: clamp(2.2rem, 6vh, 3.5rem); }
.find-results-head {
  font-family: var(--f-display); font-weight: 700;
  font-size: var(--t-h2); line-height: var(--lead-h); letter-spacing: var(--track-h);
  text-align: center; color: var(--ink); margin: 0 0 1.4rem;
}
.find-card {
  display: flex; align-items: center; gap: 1rem;
  padding: 1.05rem 1.25rem; margin: 0 auto .7rem; max-width: 40rem;
  border: 1px solid var(--hair); border-radius: 14px;
  background: var(--cream); color: var(--ink); text-decoration: none;
  transition: border-color .3s var(--ease-house), transform .2s var(--ease-house);
}
.find-card:hover { border-color: var(--ink); transform: translateY(-1px); }
.find-card-b { flex: 1; min-width: 0; }
.find-card-name {
  display: block; font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.25rem, 2.4vw, 1.7rem); line-height: 1.05;
}
.find-card-meta {
  display: block; font-family: var(--f-body); font-weight: 400;
  font-size: var(--t-kicker); color: var(--stone); margin-top: .3rem;
}
.find-rate {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center;
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.15rem, 2vw, 1.5rem); line-height: 1; color: var(--ink);
}
.find-star { color: var(--oil, #E3A21A); }
:root { --oil: #C89A2B; }
.find-rate-src {
  font-family: var(--f-body); font-weight: 400;
  font-size: .58em; color: var(--stone); margin-top: .3rem; white-space: nowrap;
}
.find-rate--live {
  flex-direction: row; font-family: var(--f-body); font-weight: 600;
  font-size: var(--t-kicker); color: var(--olive); text-align: center;
  max-width: 8.5rem; line-height: 1.3;
}
.find-card-go { flex: 0 0 auto; color: var(--stone); font-size: 1.1rem; }
.find-foot {
  font-family: var(--f-body); font-size: var(--t-kicker); color: var(--stone);
  text-align: center; margin: 1.4rem auto 0; max-width: 34rem;
}
.find-empty {
  font-family: var(--f-air); font-weight: 300; font-size: var(--t-lede);
  color: var(--ink-2); text-align: center;
}

/* ----------  close  ------------------------------------------------------ */
#close {
  text-align: center; padding: clamp(5rem,14vh,10rem) var(--pad-x);
  background: var(--paper);
}
/* Two actions side by side: restart the film, or leave for the studio's
   other work. Wrap and centre so a narrow screen stacks them cleanly. */
.close-actions {
  margin-top: 2rem;
  display: flex; flex-wrap: wrap; gap: .8rem; justify-content: center;
}
.restart, .more {
  padding: .8rem 2rem; border-radius: 100px;
  border: 1px solid var(--hair); background: transparent; color: var(--ink);
  cursor: pointer; font-size: var(--t-kicker); letter-spacing: var(--track-kicker);
  text-decoration: none; display: inline-block;
  transition: background-color .4s var(--ease-house), color .4s var(--ease-house);
}
.restart:hover { background: var(--ink); color: var(--cream); }
/* The primary exit: the plate points back to the portfolio it came from, so
   this one is filled while restart stays a quiet outline. */
.more { background: var(--ink); color: var(--cream); border-color: var(--ink); }
.more:hover { background: transparent; color: var(--ink); }
.credit { margin-top: 3rem; font-size: var(--t-kicker); color: var(--stone); }

/* ----------  chrome  ----------------------------------------------------- */
.rail { position: fixed; z-index: var(--chrome-z);
        inset-inline-start: clamp(.9rem, 2.2vw, 2rem); top: 50%;
        transform: translateY(-50%); }
.rail ol { list-style: none; margin: 0; padding: 0; display: grid; gap: .7rem; }
.rail button {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
  background: var(--hair); cursor: pointer;
  transition: background-color .4s var(--ease-house), transform .4s var(--ease-house);
}
.rail button[aria-current="true"] { background: var(--paprika); transform: scale(1.5); }
.counter {
  position: fixed; z-index: var(--chrome-z);
  inset-inline-end: clamp(.9rem, 2.2vw, 2rem); inset-block-end: 1.4rem;
  margin: 0; font-family: var(--f-display); font-size: var(--t-kicker);
  color: var(--stone);
}
.rail, .counter { opacity: 0; transition: opacity .5s var(--ease-house); }
.rail.is-on, .counter.is-on { opacity: 1; }

/* will-change is switched on only while the stage is the active trigger.
   Fourteen transformed-and-scaled layers held permanently exhaust GPU memory
   on a phone; eight opacity-only layers (which is what mikdash had) do not. */
#stage.is-hot .plate,
#stage.is-hot .ing { will-change: transform, opacity; }

/* ?flat=1 - see js/main.js. Compositor layers photograph as blank in a
   headless capture, so verification runs turn the promotion off. */
html.is-flat #stage.is-hot .plate,
html.is-flat #stage.is-hot .ing { will-change: auto; }

/* ----------  mobile: the ring becomes a list  ---------------------------- */
@media (max-width: 760px) {
  :root { --act1-screens: 5.2; --act2-screens: 2.6; }
  /* The plate keeps its share of a narrow screen, and the arc's unit is the
     stage width, so the travel scales with it automatically. */
  .plate { top: 38%; width: 78vw;
           margin-left: -39vw; margin-top: -39vw; }
  .wheel-copy { top: 80%; }
  #ring { --ring-r: min(36vw, 26svh); --bowl-y: 46%; }
  /* The recipe panels stop being "a subject floating in a room with a card
     parked over it" and become two honest rows: picture, then words.
     On a phone the absolute layout left the pot at 58vw - about 220px on a
     390px screen - marooned in a tall black field with a gap under it, while
     the card floated at the bottom edge. A 9:19.5 screen has no room for a
     composition that spends its height on air. */
  .step { display: grid; grid-template-rows: 1fr auto; }
  .step-media {
    position: relative; inset: auto;
    /* end, not centre. Centring the subject in a flexible row banks the
       leftover height UNDER it, so the pot floated with a hand's width of
       empty black between it and its own caption. Sat on the bottom of the
       row it stands on the card, which is also where its shadow is. */
    align-items: end;
    padding: clamp(1.25rem, 3svh, 2.5rem) var(--pad-x) 0;
  }
  .step-media img { max-width: 88vw; max-height: 100%; }
  .step-copy {
    position: relative; inset: auto;
    max-width: none;
    margin: clamp(1rem, 2.5svh, 1.75rem) var(--pad-x) clamp(1.25rem, 3svh, 2.5rem);
    padding: 1.25rem 1.4rem;
    border-radius: 18px;
  }
  /* The floor follows the subject up: with the picture in the top row, a
     shadow at 74 percent of the panel would sit under the card instead. */
  .step-scrim {
    background:
      radial-gradient(58% 18% at 50% 68%, rgba(0,0,0,.55), transparent 70%),
      radial-gradient(58% 32% at 50% 42%, rgba(245,240,230,.09), transparent 72%);
  }
  .step-copy h3 { font-size: clamp(1.75rem, 8vw, 2.4rem); }
  .step-n { font-size: clamp(2rem, 9vw, 2.8rem); }

  .rail { display: none; }
}
/* The list must fit ONE stage height: the stage is overflow:clip, so a taller
   list is not scrollable, it is silently beheaded at the top. Budgeted for the
   shorter of 390x844 and 500x636. tools/checks.js 08-list measures it. */
#ring.ring--list {
  direction: inherit;
  display: grid; align-content: center; gap: .55rem;
  padding: 0 var(--pad-x);
}
/* In list mode the plate is not part of the list - it stays where the arc
   left it, behind the column, dimmed by js/sections/explode.js. */
#ring.ring--list .ing {
  position: static; transform: none !important;
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem;
}
#ring.ring--list .cut  { width: clamp(40px, 11vw, 56px); }
#ring.ring--list .cast { display: none; }
#ring.ring--list .line { position: static; width: auto; justify-self: stretch;
                         transform-origin: left center; }
html[lang="he"] #ring.ring--list .line { transform-origin: right center; }
/* Bigger on a phone - the kicker size read as fine print in the list. */
#ring.ring--list .label {
  position: static; transform: none;
  font-size: clamp(1.05rem, 4.6vw, 1.3rem); font-weight: 600; color: var(--ink);
}

/* The list's own title. Hidden while the ring is animated (the plate and the
   flying chips carry the meaning there); shown once it collapses to a list. */
.ring-title { display: none; }
#ring.ring--list .ring-title,
html.is-reduced .ring-title {
  display: block; grid-column: 1 / -1;
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.8rem, 7.5vw, 2.6rem); line-height: var(--lead-h);
  letter-spacing: var(--track-h); color: var(--ink);
  text-align: center; margin: 0 0 clamp(.8rem, 3vh, 1.6rem);
}

/* ----------  reduced motion: the arc collapses into a gallery  -----------
   js/scroll.js builds no timeline under this query, so everything below has
   to stand on its own with no inline styles from GSAP to fight.

   The six plates become a paired gallery: plate, its copy, plate, its copy.
   The pairing is real and not a coincidence of source order - .arc and
   .wheel-copy both go display:contents, which lifts their twelve children
   into ONE grid, and js/render.js stamps --i on each so `order` interleaves
   them. Comparison is what act 1 was for, and with no time to compare in,
   side by side is how it is done.

   One selector, not a media query duplicated beside a class. js/main.js sets
   .is-reduced from the media query AND from ?reduce=1, so there is exactly
   one way in and it can be exercised on demand - a fallback reachable only
   through system preferences is a fallback nobody ever verifies. A parallel
   @media copy would be dead code here anyway: js/render.js builds every plate,
   copy block, chip and step, so with JS off there is no page to collapse. */

html.is-reduced #act-wheel,
html.is-reduced #act-explode { display: none; }
html.is-reduced .stage {
  position: static; height: auto; overflow: visible;
  padding-block: clamp(3rem, 8vh, 6rem);
  display: grid; gap: clamp(1.5rem, 4vw, 3rem) clamp(1.5rem, 4vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  align-items: center;
  padding-inline: var(--pad-x);
}
html.is-reduced .scroll-hint,
html.is-reduced .rail,
html.is-reduced .counter { display: none; }
html.is-reduced .tint { display: none; }

/* display:contents, so the plates and the copy blocks become siblings in the
   stage's own grid and `order` can interleave them. */
html.is-reduced .arc,
html.is-reduced .wheel-copy { display: contents; }
html.is-reduced .plate {
  position: static; transform: none !important;
  opacity: 1 !important; visibility: visible !important;
  /* Capped, and the cap is what keeps the fallback readable. Uncapped, six
     plates each took most of a phone screen and the page ran to thirteen
     screens before the recipe even started - a gallery you have to scroll
     through one item at a time is the scroll narrative again, which is the
     one thing this path exists to avoid. */
  width: auto; max-width: 20rem; margin: 0 auto;
  aspect-ratio: 1;
  order: calc(var(--i) * 2);
}
html.is-reduced .plate-cast { display: none; }
/* The return seat is plate 0 coming back around so act 2 has something to
   open. With no act 2 to open it, it is the same photograph printed twice -
   a duplicate in a gallery whose whole job is comparison. */
html.is-reduced .plate[data-return] { display: none; }
html.is-reduced .wc {
  grid-area: auto;
  opacity: 1 !important; visibility: visible !important;
  transform: none !important;
  order: calc(var(--i) * 2 + 1);
  text-align: start;
}

html.is-reduced #ring { position: static; opacity: 1 !important; visibility: visible !important;
        display: grid; gap: .75rem; padding: 0 var(--pad-x); direction: inherit;
        grid-column: 1 / -1; order: 99; }
html.is-reduced .ing { position: static; transform: none !important;
        opacity: 1 !important; visibility: visible !important;
        display: grid; grid-template-columns: auto 1fr; align-items: center; gap: .8rem; }
html.is-reduced .ing .cut  { width: clamp(40px, 12vw, 64px); }
html.is-reduced .ing .cast { display: none; }
html.is-reduced .ing .line { display: none; }
html.is-reduced .ing .label { position: static; transform: none;
  font-size: clamp(1.05rem, 4.6vw, 1.3rem); font-weight: 600; color: var(--ink); }
html.is-reduced .rule-fill { transform: scaleY(1); }

/* The recipe stack unfolds into plain flowing panels, every word and card
   visible with no entrance to wait for. */
/* Height too, not just position. Leaving the panels at 100svh made the
   fallback five screens of mostly empty room - the scroll narrative again,
   which is the one thing this path exists to avoid. */
html.is-reduced .step { position: relative; height: auto; padding-block: 2rem; }
html.is-reduced .step-media { position: relative; inset: auto; padding: 0; }
html.is-reduced .step-media img { max-width: min(26rem, 100%); max-height: none; }
html.is-reduced .step-scrim { display: none; }
html.is-reduced .step-copy { position: relative; inset: auto; max-width: none;
                             margin-top: 1.2rem; }
html.is-reduced .step-copy { opacity: 1; transition: none; }
html.is-reduced .w { opacity: 1; transform: none; filter: none; transition: none; }
html.is-reduced .step-media { transform: none !important; }
