/* ==========================================================================
   CRVD.KITCHEN — Release 0.2 (Global Visual System — Sprint 18.2)
   Direction A3 "Magazine" (Warm Charcoal foundation) for the dark product
   environment, combined with the A2 warm-paper treatment scoped to the
   Background Reader only. Two complementary environments, one token system.
   See docs/02-Infrastructure-Architecture for the Sprint 18/18.1/18.2 record.
   ========================================================================== */

:root{
  /* ---- backgrounds: dark product environment ---- */
  --canvas:#0F0D12;          /* page canvas -- C · Vessel base, warm near-black (Constitution §2.1) */
  --nav-bg:#08060B;          /* topbar / bottom nav -- one step darker than canvas */
  --surface:#1E1B22;         /* primary surface: cards, recipe cards, shefindings */
  --surface-2:#262129;       /* secondary surface: inputs, textareas */
  --surface-elevated:#2C2530; /* sheets, modals -- sits above primary surface */
  --surface-premium:#241620;  /* PL8 surfaces -- restrained, not neon */

  /* ---- backgrounds: reading environment (Background Reader only) ---- */
  --editorial-paper:#F4EEE0;      /* warm cream paper surface */
  --editorial-ink:#221E1A;        /* primary text on paper */
  --editorial-ink-secondary:#584E40; /* secondary text on paper */

  /* ---- text: dark product environment ---- */
  --text-primary:#F5F1EC;
  --text-body:#C9C0C4;
  --text-secondary:#A79FA4;
  --muted:#948C93;
  --text-disabled:#5C5560;

  /* ---- borders: dark product environment ---- */
  --hairline:#2E272D;          /* subtle divider -- decorative list rows only */
  --border-default:#6A6666;    /* actionable boundaries -- clears 3:1 vs surface */
  --border-strong:#7C7678;     /* hover / emphasis */
  --border-interactive:#E219B5; /* selected / focused actionable boundary */
  --border-premium:rgba(226,25,181,0.35); /* PL8 surfaces only */
  --editorial-divider:rgba(34,30,26,0.15); /* dividers on the paper surface */

  /* ---- actions ---- */
  /* --magenta-brand is the raw brand value and is never remapped. --magenta is
     rescoped to editorial ink inside the Reader, so anything that genuinely
     needs brand magenta on a Reader surface must reference --magenta-brand.
     Purely an alias: no rendered value changes anywhere. */
  --magenta-brand:#E219B5;
  /* THE SIGNAL -- shared CSS mask source (Signal Material + Motion
     sprint; Free/PL8 no longer an insertion point as of the Signal
     Consolidation sprint -- those decorative instances were removed
     outright). A CSS-only duplicate of SIGNAL_SVG's exact path/rect
     coordinates (app.js), used ONLY as a mask shape for material
     layers -- never as the rendered mark itself. Duplicated rather
     than referenced by id specifically because SIGNAL_SVG is inserted
     multiple times in the same document (Hero, Menschen, entry,
     ceremony) and HTML ids must be unique -- an id-based mask
     reference would collide. SIGNAL_SVG itself (app.js) is NEVER
     modified by this token; if that geometry ever changes, this
     string must be updated to match by hand. */
  --signal-mask-src:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><path d="M 168.02 163.43 A 93 93 0 1 1 188.94 127.19" fill="none" stroke="white" stroke-width="14" stroke-linecap="butt"/><rect x="173.54" y="139.50" width="14" height="14" fill="white" transform="rotate(30 180.54 146.50)"/></svg>');
  /* THE SIGNAL -- canonical Satin Magenta material body (Signal
     Governance sprint, DESIGN_CONSTITUTION.md §1.2/DH-13). One shared
     gradient recipe so every instance draws from the literal same
     definition rather than a copy-pasted value that could quietly
     drift out of sync -- "material varies in intensity, never in
     type." Consumers set their OWN opacity on top of this (Hero .11,
     Menschen 1) to vary intensity without touching the recipe itself. */
  --signal-satin-gradient:linear-gradient(155deg,
    #4a0f3d 0%, #7a1763 24%, #b81f95 44%, #86185f 64%, #3c0c32 100%);
  --magenta:var(--magenta-brand);  /* primary actions, selected nav, focus, PL8 */
  --magenta-text-safe:#E534BD; /* small magenta text on dark surfaces (badge-pl8, chef-note) --
                                   base --magenta measured 4.16-4.35:1 on real rendered dark
                                   surfaces, short of AA for text under 18px; this lightened
                                   variant verified 4.59:1+ */
  --action-destructive:#D05778; /* verified 4.59:1 on canvas */
  --presence-active:#4FA876; /* muted sage, not neon -- active-presence dot only, not a
                                 second UI accent; magenta remains the sole action colour */
  /* Sprint 18.3 -- --accent-editorial / --accent-editorial-paper (gold/brass)
     removed per Founder correction: no decorative accent colors beyond
     magenta, anywhere. Hierarchy on dark surfaces and on paper is now
     carried by --text-primary (white / dark ink) and --text-body / --muted
     (warm grey / brown-grey) only, plus weight and underline for emphasis. */

  --surface-selected:rgba(226,25,181,0.12); /* single shared tint for active toggle/chip states */

  --font-d:'Oswald','Arial Narrow',Impact,sans-serif;
  --font-c:'Roboto',-apple-system,"Segoe UI",Arial,sans-serif;
  --patient:cubic-bezier(0.65,0,0.35,1);
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px; --s9:80px;
  --radius:14px;
  --preview-banner-h:56px;
  /* Sprint 19.2 -- shared mobile content safe-area: clears the bottom nav
     (~64px) and the PL8 FAB, which floats above it at bottom:86px with its
     own ~48px height (obstruction reaches ~134px from the screen bottom).
     Applies via .app-content's own padding, so every screen gets it -- not
     a per-recipe fix. Overridden by the desktop rule below 1024px, where
     there's no bottom nav or low-riding FAB to clear. */
  --mobile-safe-bottom:190px;
}
@media (min-width:480px){ :root{ --preview-banner-h:36px; } }

*{ box-sizing:border-box; }
html,body{ margin:0; padding:0; background:var(--canvas); color:var(--text-primary); font-family:var(--font-c); }
body{ -webkit-font-smoothing:antialiased; padding-top:var(--preview-banner-h); }
h1,h2,h3,h4{ margin:0; font-family:var(--font-d); text-transform:uppercase; }
p{ margin:0; }
a{ color:inherit; }
button{ font-family:inherit; }
img{ max-width:100%; display:block; }

/* ---------- persistent public-preview marker (infrastructure, not product design) ---------- */
#preview-banner{ position:fixed; top:0; left:0; right:0; z-index:50; min-height:var(--preview-banner-h);
  display:flex; align-items:center; justify-content:center; text-align:center;
  padding:6px max(14px, env(safe-area-inset-right)) 6px max(14px, env(safe-area-inset-left));
  background:var(--surface-2); border-bottom:1px solid var(--magenta);
  color:var(--text-body); font-size:11.5px; line-height:1.4; }
#preview-banner strong{ color:var(--text-primary); }
.preview-note{ font-size:12px; line-height:1.5; color:var(--muted); text-align:left; margin:0 0 20px; }

/* ---------- shared components ---------- */
.btn{ font-family:var(--font-c); font-weight:600; font-size:14px; letter-spacing:0.01em; border-radius:999px;
  padding:14px 28px; cursor:pointer; border:none; display:inline-block; text-decoration:none; min-height:46px;
  transition:opacity 240ms var(--patient), transform 160ms var(--patient), border-color 200ms var(--patient); }
.btn-primary{ background:var(--magenta); color:var(--canvas); }
.btn-primary:hover{ opacity:0.9; transform:translateY(-1px); }
.btn-ghost{ background:transparent; color:var(--text-primary); border:1px solid var(--hairline); }
.btn-ghost:hover{ border-color:var(--border-strong); }
.btn-quiet{ background:none; border:none; color:var(--muted); text-decoration:underline; font-size:13px; cursor:pointer; padding:6px 2px; }
.btn-quiet:hover{ color:var(--text-primary); }
.btn:focus-visible, a:focus-visible, input:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.btn-block{ width:100%; text-align:center; }

.card{ background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius); padding:var(--s5); }
.kicker{ font-family:var(--font-c); font-size:12px; font-weight:600; letter-spacing:0.14em; text-transform:uppercase; color:var(--muted); margin:0 0 10px; }
.badge{ display:inline-block; font-size:11px; font-weight:600; letter-spacing:0.08em; text-transform:uppercase; padding:4px 10px; border-radius:999px; }
.badge-free{ background:var(--surface-2); color:var(--text-secondary); }
.badge-pl8{ background:var(--surface-premium); color:var(--magenta-text-safe); }
.field{ margin-bottom:var(--s5); text-align:left; }
.field label{ display:block; font-size:12.5px; color:var(--muted); margin-bottom:6px; }
.field input{ width:100%; background:var(--surface-2); border:1px solid var(--hairline); border-radius:10px; padding:13px 14px;
  color:var(--text-primary); font-size:15px; font-family:var(--font-c); }
.field input::placeholder{ color:var(--muted); }
.field input:focus{ outline:2px solid var(--magenta); outline-offset:1px; border-color:var(--magenta); }

.hairline{ height:1px; background:var(--hairline); border:none; margin:var(--s5) 0; }

/* Post-signup ceremony (Refinement Sprint): the threshold moment between
   "completing the form" and "being inside," rebuilt so the Signal tells
   its own story instead of sitting beside a caption. Sole visual hero --
   fixed, full-bleed, opaque, above the entire app shell (preview banner
   included), so there is nothing else on screen to compete with it. No
   dialog chrome, nothing to dismiss; it ends itself. */
.ceremony-overlay{ position:fixed; inset:0; z-index:100; display:flex; align-items:center;
  justify-content:center; overflow:hidden; background:var(--canvas);
  opacity:0; transition:opacity 500ms var(--patient); }
.ceremony-overlay.ceremony-in{ opacity:1; }
/* Vessel atmosphere -- same locked vignette values as the homepage hero,
   scoped to this overlay so the ceremony visibly belongs to the same
   world rather than being a plain dialog on a flat ground. */
.ceremony-vignette{ position:absolute; inset:0; z-index:0; pointer-events:none;
  background:radial-gradient(1100px 800px at 50% 46%, rgba(255,255,255,.022) 0%, transparent 45%, rgba(0,0,0,.84) 100%); }
.ceremony-grain{ position:absolute; inset:0; z-index:0; pointer-events:none; opacity:.14; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='%75%72%6C%28%23n%29'/%3E%3C/svg%3E");
  background-size:120px 120px; }

.ceremony-signal{ position:relative; z-index:1; width:min(72vw,460px); height:min(72vw,460px);
  transition:opacity 150ms var(--patient); }
.ceremony-signal svg{ position:absolute; inset:0; width:100%; height:100%; display:block; overflow:visible; }
/* Fix Ceremony -> PACE Transition Artifact: the ring/seat and statement
   are the only high-contrast shapes in the overlay -- the overlay's own
   flat background already matches the PACE canvas exactly, so once
   these are hidden there's nothing left to visibly cross-fade against
   the new scene underneath. Fast and in parallel with the container's
   own opacity fade, not sequential -- no added time. */
.ceremony-overlay.ceremony-out .ceremony-signal,
.ceremony-overlay.ceremony-out .ceremony-text{ opacity:0; }
/* Ring tone is the existing ink token mixed with the existing canvas token --
   no new color, no glow -- so the ring reads as the same warm-near-black
   atmosphere catching light, not a grey shape laid on top of it. */
.ceremony-ring{ fill:none; stroke:color-mix(in srgb, var(--text-primary) 44%, var(--canvas) 56%);
  stroke-width:14; stroke-linecap:butt; }
/* The square starts displaced radially outward at the gap's own angle (30
   deg) -- "clearly visible outside the gap area" -- and travels inward to
   the exact resting position used everywhere else in the product (the
   locked construction, untouched). Rotation is fixed at all times; only
   the approach moves. */
.ceremony-seat{ fill:color-mix(in srgb, var(--text-primary) 44%, var(--canvas) 56%);
  transform-box:fill-box; transform-origin:50% 50%;
  transform:translate(13.9px,8px) rotate(30deg);
  transition:transform 950ms var(--patient), fill 550ms var(--patient); }
.ceremony-seat.arrived{ transform:translate(0,0) rotate(30deg); }
.ceremony-seat.peak{ fill:var(--magenta-brand); transition:transform 950ms var(--patient), fill 260ms var(--patient); }

/* Typography sits inside the ring's own negative space -- framed by the
   symbol, not placed beside it. */
.ceremony-text{ position:absolute; inset:14%; z-index:2; display:flex; flex-direction:column;
  align-items:center; justify-content:center; text-align:center; pointer-events:none;
  transition:opacity 150ms var(--patient); }
/* Setup line recedes (lower rest opacity) so the statement below it reads
   as the clear peak, not a second line of equal weight. */
.ceremony-quiet{ font-family:var(--font-c); font-size:clamp(11px,1.6vw,13px); font-weight:600;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--muted);
  margin:0 0 14px; opacity:0; transition:opacity 600ms var(--patient); }
.ceremony-overlay.ceremony-in .ceremony-quiet{ opacity:.6; }
.ceremony-statement{ font-family:var(--font-d); font-weight:700; text-transform:uppercase;
  font-size:clamp(25px,5.2vw,39px); line-height:1.06; letter-spacing:-0.008em;
  color:var(--text-primary); margin:0; opacity:0; transform:translateY(6px);
  transition:opacity 700ms var(--patient), transform 700ms var(--patient); }
.ceremony-statement.statement-in{ opacity:1; transform:translateY(0); }

@media (prefers-reduced-motion:reduce){
  .ceremony-overlay, .ceremony-quiet, .ceremony-statement, .ceremony-seat{ transition:none !important; }
  .ceremony-overlay{ opacity:1; }
  .ceremony-overlay .ceremony-quiet{ opacity:.6; }
  .ceremony-overlay .ceremony-statement{ opacity:1; transform:none; }
  .ceremony-seat{ transform:translate(0,0) rotate(30deg); }
}

/* ==========================================================================
   PUBLIC WEBSITE — full-bleed, natural scroll, no app frame
   ========================================================================== */
/* overflow-x:CLIP, not hidden. Per spec `overflow-x:hidden` forces the
   computed `overflow-y` to `auto`, which makes this element a SCROLL
   CONTAINER -- and position:sticky sticks to its nearest scrolling
   ancestor. Because #public-site is a scroll container that never
   actually scrolls (the window does), every sticky descendant silently
   never activated: the Held Stage travelled with the document exactly as
   if sticky were absent. `clip` gives the same horizontal-overflow
   protection without establishing a scroll container, so sticky resolves
   against the viewport as intended. Verified: the stage's image travels
   0px across the whole scene with clip, ~1170px with hidden. */
#public-site{ display:none; position:relative; width:100%; overflow-x:clip; }
#public-site canvas#pub-net{ position:fixed; inset:0; width:100vw; height:100vh; z-index:0; pointer-events:none; }
#public-site .veil{ position:fixed; inset:0; z-index:1; pointer-events:none;
  /* C · Vessel vignette, locked values (Constitution §2.1): minimal centre
     lift, hard fall to a near-black edge wash -- layered over the existing
     Sprint 14 hero glow/field canvas, not replacing it. */
  background:radial-gradient(1100px 800px at 50% 40%, rgba(255,255,255,.022) 0%, transparent 45%, rgba(0,0,0,.84) 100%); }
#public-site .pub-grain{ position:fixed; inset:0; z-index:1; pointer-events:none;
  opacity:.14; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='%75%72%6C%28%23n%29'/%3E%3C/svg%3E");
  background-size:120px 120px; }

.pub-nav{ position:fixed; top:var(--preview-banner-h); left:0; right:0; z-index:5; display:flex; align-items:center; justify-content:space-between;
  padding:18px max(24px, env(safe-area-inset-right)) 18px max(24px, env(safe-area-inset-left));
  background:linear-gradient(to bottom, rgba(16,14,19,0.92), transparent); }
/* Hero Refinement Sprint (all breakpoints): the large centred hero
   wordmark is now the homepage's single brand anchor, at every breakpoint
   -- the small nav-corner instance is retired, not just hidden on mobile. */
.pub-nav .wordmark{ display:none; }
.wordmark-dot{ fill:var(--magenta); }
.pub-nav .pub-nav-actions{ display:flex; gap:10px; align-items:center; }
.pub-nav .btn{ padding:10px 20px; font-size:13px; }
.pub-nav .pub-login{ background:none; border:none; color:var(--muted); font-size:13px; cursor:pointer; display:none; }
/* Hero Refinement Sprint: the hero's own centred primary CTA is removed
   (see .pub-hero .actions below), so the nav CTA is now the sole
   "Kostenlos mitmachen" on the homepage -- visible at every breakpoint,
   not just desktop. */

/* Public Experience Redesign, Phase 2 -- editorial scale and space.
   Vertical rhythm moves from a flat 96px to viewport-relative breathing
   room, so spacing scales with the composition instead of staying constant
   while type grows. Peak sections get more air than quiet ones: the space
   is part of the hierarchy, not a uniform gutter. */
.pub-section{ position:relative; z-index:2; width:100%; padding:clamp(112px,17vh,208px) max(24px, env(safe-area-inset-right)) clamp(112px,17vh,208px) max(24px, env(safe-area-inset-left)); }
/* Peak scenes are given a viewport floor rather than only padding. A scene
   that cannot be scrolled past without at least most of a screen to itself
   reads as a scene; one sized purely by its content reads as a block. This
   is the single change that turns sections into movements -- but it is a
   MIN-height, not a fixed height, so nothing is padded into emptiness:
   content taller than the floor simply grows past it. */
.pub-section.pub-peak{ padding-top:clamp(136px,23vh,280px); padding-bottom:clamp(136px,23vh,280px);
  min-height:82svh; display:flex; flex-direction:column; justify-content:center; }
/* Hero -> Story -> Free Community pacing (Transition Refinement Sprint):
   deliberately wider than the section default so each boundary reads as a
   pause, not a page break -- during the extra scroll distance the fixed
   Signal (unchanged, still hero-scoped CSS) is the only thing on screen,
   which is what makes it feel like it's guiding the page rather than
   sitting behind it. Composition/spacing only; the Signal itself is not
   touched here. */
#pub-story{ padding-top:120px; padding-bottom:120px; }
#pub-free{ padding-top:120px; }
.pub-section-inner{ max-width:680px; margin:0 auto; }
.pub-section-inner.wide{ max-width:820px; }
/* 680px physically cannot hold 60px+ type without wrapping every two or
   three words -- the old column width was itself a cause of the "document,
   not editorial" feel. Major statements now get 900px. */
.pub-section-inner.pub-wide{ max-width:900px; }
.pub-hero{ position:relative; min-height:calc(100svh - var(--preview-banner-h)); display:flex; align-items:center; overflow:hidden; }
/* The Signal, atmospheric background use (Constitution §1.2/§2.1): large,
   behind the foreground content, masked so it fades at its own edges
   rather than sitting on the surface like an illustration. Partially
   cropped by position is correct -- §5.1: "a fragment is correct for a
   mark whose meaning is incompleteness."
   MATERIAL, brought into the Satin Magenta family (Signal Governance
   sprint, DH-13): the flat near-white fill is retired the same way as
   the Menschen instance -- inline <svg> set to opacity:0 below, a
   masked ::before takes over as the visible material. "The material
   should match. The prominence should not": SAME gradient recipe as
   Menschen (same tonal stops, same magenta family -- material varies
   in intensity, never in type, per §1.2), but at a fraction of the
   opacity, and with NO sheen layer at all -- Hero's choreography
   structure is explicitly out of scope this sprint, and a sheen would
   need a new GSAP tween to trigger it meaningfully; staying static
   here is the conservative reading of "do not make the Hero louder."
   Never louder than the wordmark remains true: --so/--sc below are
   completely unchanged (still recede-only, still scroll-driven by the
   existing Hero choreography), and the new ::before's own low opacity
   keeps the mark's peak intensity well under where the near-white
   .075 fill used to sit perceptually. */
.pub-hero-signal{ position:absolute; top:6%; right:-20%; z-index:1; pointer-events:none;
  width:min(84vw,820px); height:min(84vw,820px);
  /* --so/--sc MUST be declared here, not left to the var() fallback.
     GSAP reads the computed start value when it builds a tween, and an
     undeclared custom property computes to the empty string, which GSAP
     parses as 0 -- producing a tween that runs 0 -> target, i.e. the
     Signal fading UP into view. That is both backwards and a governance
     breach (recede-only). Declaring them gives GSAP a real 1 to start
     from. Verified live: without these, --so animated 0 -> 0.28. */
  --so:1; --sc:1;
  opacity:var(--so,1);
  /* CROP-MASK, corrected (Signal Scale + Proportion sprint): 55%/78% ->
     78%/97%. Root cause of the perceived weight difference against the
     "Du nimmst deinen Platz ein." ceremony Signal (which has no mask at
     all) was never geometry -- the stroke-to-viewBox ratio is 14/200
     (7%) in every instance, byte-identical. It was this mask: computed
     against the radial-gradient's farthest-corner sizing basis, the
     ring's own stroke sits at ~65.8% of that basis, which the OLD
     55%/78% stops placed inside the fade zone -- measured effective
     ring alpha ~53%, silently thinning the stroke by roughly half
     before opacity/--so are even applied. 78%/97% (already proven
     correct on the Menschen instance) keeps the ring's own radius
     entirely inside the fully-opaque zone -- ring alpha 100% -- and
     only fades the empty box corners beyond it, which was the mask's
     actual intended job all along. Opacity/scale/position are
     untouched -- those are the axes allowed to vary by context; this
     fixes only the accidental ratio distortion. */
  -webkit-mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  transform-origin:50% 50%;
  transform:translate(var(--px,0px), calc(var(--py,0px) + var(--sy,0px))) scale(var(--sc,1));
  animation:signalBreathe 19s cubic-bezier(.22,.61,.36,1) infinite; }
.pub-hero-signal svg{ width:100%; height:100%; display:block; opacity:0; }
.pub-hero-signal::before{
  content:''; position:absolute; inset:0;
  -webkit-mask-image:var(--signal-mask-src); mask-image:var(--signal-mask-src);
  -webkit-mask-size:100% 100%; mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  background:var(--signal-satin-gradient);
  opacity:.11; }
/* Signal scroll variables (Premium Hero sprint). --so (opacity) and --sc
   (scale) are written by the Hero choreography in app.js so the Signal
   outlasts the rest of the composition and then recedes -- "bridge by
   persistence": it is the last thing standing, and it never brightens or
   grows to earn that. Both are recede-only (never above 1), so the Signal
   can never become louder than its resting state and never forms a second
   ceremonial peak.
   Critically, GSAP writes only these VARIABLES -- never `transform`
   itself. CSS below stays the single transform owner, composing scroll
   (--sc), pointer parallax (--px/--py) and the breathing keyframe into
   one expression. That is what keeps pointer parallax working while the
   scroll timeline runs, with no competing writers.
   --sy remains supported as a translate contributor but is currently
   unused; the Premium choreography moves the Signal by scale/opacity
   only. Elements reusing this keyframe (.entry-signal-inner,
   .ceremony-signal-inner) never set any of these and fall through to the
   defaults, so they are completely unaffected. */
@keyframes signalBreathe{
  0%,100%{ transform:translate(var(--px,0px), calc(var(--py,0px) + var(--sy,0px))) scale(var(--sc,1)) rotate(0deg); }
  50%{ transform:translate(var(--px,0px), calc(var(--py,0px) + var(--sy,0px))) scale(calc(1.03 * var(--sc,1))) rotate(3deg); } }
/* Persistent Signal (Mobile Hero & Persistent Signal Integration, approved):
   below 640px the Signal becomes a fixed, viewport-anchored layer instead
   of scrolling away with the hero -- "the Signal remains the stage" as the
   user moves into the next section. Geometry, colour, mask and the
   breathing/parallax motion are all unchanged; only position and scale
   move, per the stated rule (composition/scale/positioning/continuity
   only). Desktop is untouched -- still position:absolute, scoped to the
   hero, as implemented in the Front Door Production Integration Sprint. */
@media (max-width:640px){
  .pub-hero-signal{ position:fixed; top:2%; right:-38%; width:155vw; height:155vw; }
}
/* Centered-brand hero wordmark, now universal (Hero Refinement Sprint):
   was mobile-only (Mobile Hero Recomposition Exploration), now the single
   brand anchor at every breakpoint since the nav-corner instance is gone.
   Height reduced ~19% from the prior 64px to give the kicker, headline,
   message and "Mehr erfahren" more room -- geometry, position and colour
   otherwise unchanged. */
.hero-brand-wordmark{ display:flex; justify-content:center; margin-bottom:21px; }
.hero-brand-wordmark svg{ display:block; width:auto; height:52px; }
.pub-hero .pub-section-inner{ position:relative; z-index:2; text-align:center; }
.pub-hero h1{ font-size:clamp(38px,6.5vw,70px); line-height:1.05; letter-spacing:0.008em; margin:22px 0 0; }
.pub-hero .muted-line{ color:var(--muted); font-weight:500; }
.pub-hero .lede{ font-size:17px; line-height:1.6; opacity:0.9; margin:22px auto 0; max-width:460px; }
.pub-hero .actions{ margin-top:36px; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.pub-hero .reassurance{ margin-top:18px; font-size:12.5px; color:var(--muted); }
/* Presence in the hero must read as atmosphere, not a claim -- quieter
   than the dashboard's presence line (Vessel Integration Sprint): smaller,
   more muted, last in the content stack, never competing with the CTA. */
.pub-hero-presence{ justify-content:center; margin-top:28px; font-size:12px; opacity:.8; }
/* Green communicates active presence specifically -- scoped to the public
   hero only. The dashboard's presence dot (Vessel Integration Sprint)
   keeps its original muted --text-secondary treatment; not part of this
   sprint's scope. */
.pub-hero-presence .presence-dot{ background:var(--presence-active); }
/* Sprint 14 final -- the CRVD.KITCHEN opening shot. No traveling element,
   no keyframes, no glow/halo, no loop. One class toggle drives three
   staggered one-way settles that all start together and finish in order
   purely by differential duration: headline first, "TREND." a beat later,
   primary CTA last (material shadow depth + one clipped reflection
   sweep). Entirely disabled under prefers-reduced-motion -- the page then
   paints directly in its final, settled state. Secondary CTA and "KEIN"
   are never touched. */

/* "TREND." -- refined satin material treatment only (Founder-approved
   final pass): a tight, warm gradient plus a very fine vertical grain,
   clipped to the letterforms. This gradient itself never animates or
   sweeps -- only its brightness settles, alongside the rest of the
   system, via the media-query block below. */
.pub-hero .hero-trend{
  font-weight:500;
  background-image:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.05) 0px, rgba(255,255,255,0.05) 1px, rgba(0,0,0,0.045) 1px, rgba(0,0,0,0.045) 2px),
    linear-gradient(100deg, #887F84 0%, #9B928E 45%, #A29A96 55%, #887F84 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent; -webkit-text-fill-color:transparent;
}

/* Primary CTA -- resting material shadow (never a colored glow) plus the
   one-shot reflection sweep. The sweep element is clipped by the button's
   own overflow:hidden, so it is only ever visible while crossing the
   button and invisible at both ends -- never a resting shine, never
   repeats. Scoped to .pub-hero so no other button on the site is
   affected. */
.pub-hero .btn-primary{ position:relative; overflow:hidden;
  box-shadow:0 10px 24px -8px rgba(35,10,20,0.45), 0 2px 6px rgba(20,6,12,0.3), inset 0 1px 0 rgba(255,255,255,0.10); }
.pub-hero .btn-primary::after{
  content:''; position:absolute; top:-60%; left:0; width:34%; height:220%;
  background:linear-gradient(100deg, transparent 0%, rgba(255,255,255,0.24) 50%, transparent 100%);
  transform:translateX(340%) rotate(10deg);
  pointer-events:none;
}

@media (prefers-reduced-motion: no-preference){
  .pub-hero .hero-headline-settle{ transition:opacity 800ms var(--patient), filter 800ms var(--patient); }
  .pub-hero .hero-trend{ transition:opacity 1100ms var(--patient), filter 1100ms var(--patient); }
  .pub-hero .btn-primary{ transition:box-shadow 1600ms var(--patient); }
  .pub-hero .btn-primary::after{ transition:transform 1200ms var(--patient); }

  .pub-hero.hero-arriving .hero-headline-settle{ opacity:0.85; filter:brightness(0.92); }
  .pub-hero.hero-arriving .hero-trend{ opacity:0.8; filter:brightness(0.88); }
  .pub-hero.hero-arriving .btn-primary{ box-shadow:0 2px 6px -3px rgba(20,6,12,0.15), inset 0 0 0 rgba(255,255,255,0); }
  .pub-hero.hero-arriving .btn-primary::after{ transform:translateX(-260%) rotate(10deg); }

  /* ARRIVAL -> SCROLL OWNERSHIP HANDOFF (Premium Hero sprint).
     The arrival above animates opacity/filter for up to ~1.6s. The scroll
     choreography animates opacity on the same elements. A visitor who
     scrolls immediately after load would otherwise have both writing the
     same properties at once, which flickers. So the moment app.js builds
     the scroll timeline it stamps .hero-scroll-owned here, and every
     arrival transition is dropped: arrival keeps its visual result (it is
     one-way and lands on the resting state) but stops being an animator.
     Exactly one owner at any moment, decided by a class rather than by
     whether the user happened to wait. */
  .pub-hero.hero-scroll-owned .hero-headline-settle,
  .pub-hero.hero-scroll-owned .hero-trend,
  .pub-hero.hero-scroll-owned .btn-primary,
  .pub-hero.hero-scroll-owned .btn-primary::after{ transition:none; }
}

/* Recession, section arrival (Hero Recession Sprint): staggered by child
   position rather than as one block -- kicker, then heading, then the
   rest of the section's body, 0/90/180ms apart. Same one-way,
   differential-duration grammar the Hero arrival already uses. Works
   across every .pub-section unmodified because each one already opens
   kicker -> heading -> body, in that DOM order. */
.pub-section-inner.pub-reveal > *{ opacity:0; transform:translateY(22px); transition:opacity 700ms var(--patient), transform 700ms var(--patient); }
.pub-section.seen .pub-section-inner.pub-reveal > *{ opacity:1; transform:translateY(0); }
.pub-section.seen .pub-section-inner.pub-reveal > *:nth-child(2){ transition-delay:90ms; }
.pub-section.seen .pub-section-inner.pub-reveal > *:nth-child(n+3){ transition-delay:180ms; }

/* ---------------------------------------------------------------------- *
 * PUBLIC EDITORIAL SCALE (Public Experience Redesign, Phase 2)
 *
 * The page previously ran 70 (hero) then 34/38/38/38/38/38 then 50 -- one
 * peak followed by six identical mid-weight sections. That flatness, not
 * missing animation, is what made everything after the Hero feel like a
 * different (and lesser) product.
 *
 * The rhythm is now deliberate:
 *   PEAK   Die Frage    ~64   the differentiating idea
 *   QUIET  Das Essen    ~28   the image carries the section
 *   MID    Die Menschen ~44   voices carry the section
 *   PEAK   Frei         ~56   /  QUIET  PL8 ~30  -- hierarchy as scale
 *   FINAL  Einladung    ~72   deliberately the largest type on the page
 *
 * The finale out-scaling the Hero is intentional: the conversion moment
 * should be the loudest thing a visitor reaches, not a smaller echo of the
 * opening. All of these are one-line declarations so the rhythm can be
 * read, and adjusted, in a single place. */
.pub-h-peak{ font-size:clamp(38px,6vw,64px); line-height:1.04; letter-spacing:0.006em; margin:18px 0 0; }
/* Frei sits just below Die Frage. Two peaks at an identical size read as
   two equal claims; the differentiating idea has to stay the taller one. */
#pub-frei .pub-h-peak{ font-size:clamp(34px,5.2vw,56px); }
.pub-h-mid{ font-size:clamp(30px,4.6vw,44px); line-height:1.1; margin:16px 0 0; }
.pub-h-quiet{ font-size:clamp(22px,2.9vw,28px); line-height:1.25; margin:0; }
.pub-h-sub{ font-size:clamp(21px,2.9vw,30px); line-height:1.3; margin:14px 0 0; font-weight:500; }
.pub-h-final{ font-size:clamp(40px,7vw,72px); line-height:1.02; letter-spacing:0.004em; margin:18px 0 0; }

/* Supporting copy scales with the statements it supports -- a 64px
   headline over 15px body is what made the old sections read as cramped. */
/* The gap between a statement and its support is doing hierarchy work:
   too tight and they read as one paragraph, so it scales with the
   viewport rather than sitting at a fixed 22px. */
.pub-lede-lg{ font-size:clamp(17px,1.55vw,20px); line-height:1.6; color:var(--text-body);
  max-width:34em; margin:clamp(30px,4.6vh,52px) 0 0; }
.pub-aside{ font-size:14.5px; line-height:1.6; color:var(--text-secondary);
  margin:clamp(22px,3vh,32px) 0 0; max-width:40em; }

/* ---------------------------------------------------------------------- *
 * THE HELD STAGE -- Phase 1 (Frage -> Essen).
 *
 * Replaces the travelling-Aperture architecture. Previously each scene
 * had its OWN position:sticky stage, which only behaves as a stage once
 * that section's top reaches the viewport top -- entry and exit were
 * therefore ordinary document flow and the food container visibly
 * travelled upward before and after the staged moment.
 *
 * Now ONE sticky stage spans both scenes: it locks at the start of Frage
 * and releases only after Essen. Both scenes are absolutely positioned
 * layers inside it, so nothing in the stage is ever positioned as a
 * function of scrollY and no content container can travel.
 *
 * The stage itself must never receive a transform -- .veil / .pub-grain /
 * #pub-net are position:fixed siblings, and a transformed ancestor would
 * become their containing block (the documented boxed-vignette bug
 * class). GSAP animates only the LAYERS inside the stage, never the
 * stage or the wrapper.
 *
 * .pub-held-wrap supplies scroll duration only and has no visual
 * treatment. Menschen is not part of this stage yet (Phase 2). */
/* 230vh -> 290vh (Held Stage Phase 2, Menschen integration). The
   existing Frage->Food timeline is untouched -- its tween positions in
   initHeldStage are exactly as before; extending the wrapper only gives
   the newly-appended food-recession + 3-voice + Signal-return + release
   tweens their own room after it, inside Founder's approved 285-300vh
   target. Verified live against real measured pixel ranges, not just
   computed from this value -- see the sprint report for actual numbers. */
/* 260vh -> 285vh (Refinement sprint): the increase is entirely the new
   Signal-as-final-beat content (photograph further darkens, a brief
   unscheduled pause, the Signal emerges and settles) -- voice pacing
   itself was tightened, not lengthened; see initHeldStage. */
/* 285vh -> 300vh (Visual Polish sprint): Founder's read was that the
   Voice 1 -> 2 transition felt rushed. Fix is longer HOLD per voice
   (initHeldStage timeline duration grew 2.38->2.50, +5%), and this
   height grew by the same proportion so the extra timeline duration
   becomes more reading time at the same pacing, not the same duration
   spread thinner. 300vh lands exactly at the previously Founder-
   approved 285-300vh ceiling from the prior sprint -- no new range
   opened. */
/* 300vh -> 318vh (Food -> Signal Handoff sprint): the old pre-emerge
   food-darkening step (~0.20 timeline units) is removed outright and
   reallocated into a new post-emerge Food Recession beat plus a
   genuine Signal Solo Hold (initHeldStage timeline duration grew
   2.50->2.65, +6%) -- net growth is deliberately small because most of
   the new beats' time was reclaimed from the step they replaced, not
   added on top of an unchanged timeline. Height grew by the same ~6%
   so the Solo Hold gets real reading distance rather than the same
   pacing spread thinner. */
.pub-held-wrap{ position:relative; z-index:2; width:100%; height:318vh; }
.pub-held-stage{ position:sticky; top:0; height:100svh; width:100%; overflow:hidden; }
.pub-held-layer{ position:absolute; inset:0; display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  padding:0 max(24px, env(safe-area-inset-right)) 0 max(24px, env(safe-area-inset-left)); }
/* Frage sits above the photograph in paint order; the photograph is
   mounted beneath it at opacity 0 from the first frame and dissolves up
   in place -- it has no position to arrive from. */
.pub-held-frage{ z-index:2; }
/* Headline lines as blocks so each can be given its own timing. They
   replace the previous <br/>, so the visual line break is unchanged. */
.pub-h-peak .hl-line{ display:block; }
/* LUMINOUS EMERGENCE (Hero Hold + Food Emergence sprint) -- retires the
   clip-path growing-rectangle reveal. That mechanism was geometrically
   correct (image never scaled or moved) but still read as a box opening,
   because clip-path IS a rectangle changing shape. This replaces it with
   a purely perceptual reveal: the image is full-bleed and stationary
   from the first frame, sitting behind Frage at opacity 0 exactly as
   before, but instead of the image itself fading in, a dedicated scrim
   layer above it animates -- position and opacity, written by
   initHeldStage as CSS variables. No boundary is ever visible; the
   gradient is deliberately far larger than the viewport, so there is
   nothing for the eye to read as an edge, wipe, or spotlight. */
.pub-held-essen{ z-index:1; opacity:0; padding:0; }
/* --scrim-x/--scrim-y position a gradient far larger than the stage
   (300% in both axes) so its visible edge is never inside the viewport
   -- only its soft interior gradient ever appears on screen. --scrim-op
   fades the whole layer out at the end so it costs nothing once the
   reveal is complete. Desktop only (see mobile override): this is the
   one piece of the effect with real compositing cost, and the emergence
   metaphor survives without it on touch devices via brightness/opacity
   alone. */
.pub-essen-scrim{ position:absolute; inset:-100%; z-index:2; pointer-events:none;
  --scrim-x:50%; --scrim-y:50%; --scrim-op:1;
  background:radial-gradient(ellipse 140% 100% at var(--scrim-x) var(--scrim-y),
    rgba(11,10,14,0) 0%, rgba(11,10,14,0.55) 38%, rgba(11,10,14,0.96) 62%, #0B0A0E 100%);
  opacity:var(--scrim-op); }
/* Full-bleed at FINAL size from the start. Never scaled, never resized,
   never moved. Only its filter (light) and object-position (a small
   in-place drift) ever change. */
.pub-essen-img{ position:absolute; inset:0; width:100%; height:100%;
  /* --essen-blur MUST be declared here, not left to the var() fallback
     alone -- same lesson as --so on the Hero Signal: GSAP reads the
     computed value when building a tween, and an undeclared custom
     property computes to an empty string, which broke the ENTIRE filter
     shorthand (all three functions, not just blur) to `none` the moment
     the recession tween ran. Verified live: without this, filter went
     brightness(1) contrast(1) blur(0px) -> none at the recession's first
     frame. */
  --essen-blur:0;
  /* --essen-blur added for Held Stage Phase 2 (food recession into
     Menschen's atmosphere) -- defaults to 0px, so Phase 1's emergence is
     completely unaffected; it only becomes nonzero once the recession
     tween (initHeldStage, position 0.98) runs, well after Phase 1
     completes. Additive, not a change to the approved emergence. */
  filter:brightness(var(--essen-b,0.72)) contrast(var(--essen-c,0.97)) blur(var(--essen-blur,0px));
  /* Object-position tuned for the current hand-and-plate photograph
     (1536x1024, source ratio 1.5). At every desktop breakpoint tested
     (1440x900 = 1.6, 1920x1080 = 1.778) cover is WIDTH-constrained --
     the image scales to fill width exactly and only HEIGHT gets
     cropped, so only the Y value has any effect here. The hand enters
     from near the top of the frame; Y:18% keeps the crop mostly out of
     the bottom margin (spare table/plate) rather than into the hand.
     Re-tune this for any replacement image with different subject
     placement -- see the swap note on the <img> tag in app.js. */
  object-fit:cover; object-position:50% 18%; display:block; }
.pub-essen-flag{ position:absolute; top:16px; left:16px; z-index:3;
  font-family:var(--font-a); font-size:10.5px; letter-spacing:0.16em; text-transform:uppercase;
  color:var(--text-secondary); background:rgba(0,0,0,0.5); border:1px solid var(--hairline);
  border-radius:999px; padding:7px 13px; }
.pub-essen-caption{ position:absolute; left:0; right:0; bottom:0; z-index:3;
  padding:0 clamp(24px,5vw,64px) clamp(32px,6vh,64px);
  background:linear-gradient(to top, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.28) 46%, transparent 100%);
  padding-top:clamp(60px,12vh,140px); }
.pub-essen-caption .pub-h-quiet{ margin:0; }

/* ---------------------------------------------------------------------- *
 * DIE MENSCHEN -- Held Stage Phase 2, TWO voices. Third layer, above the
 * (now receding) photograph. Real COMMUNITY_SEED statements, no cards,
 * no avatars, no counts: the `acks` numbers are deliberately never
 * rendered, since displaying engagement metrics on a product with no
 * members yet would be exactly the social-proof theatre this scene has
 * to avoid.
 *
 * TYPOGRAPHY: --font-d (Oswald), matching every other declarative
 * statement on the page (Frage, Frei, Finale). Sentence case, not the
 * uppercase convention those headlines use -- these are full sentences,
 * not 2-4 word taglines, and forcing full sentences into all-caps
 * measurably hurts reading speed, conflicting with the brief's own
 * readability requirement. Line breaks are fully authored (see the
 * markup in app.js) -- five short lines each, none close enough to the
 * column width to risk an unplanned browser wrap.
 *
 * POSITIONING, corrected (Refinement sprint): both voices previously sat
 * in different corners (mv-1 upper-left, mv-2 lower-right), which read
 * as "randomly placed" rather than composed. Both now share the exact
 * same position -- one statement dissolves, the next arrives in its
 * place, like a caption belongs to the room rather than jumping across
 * it. Placed lower-left, deliberately away from the photograph's busiest
 * content (the hand-and-plate interaction sits center-to-lower-center
 * per the object-position tuning above). Timing in initHeldStage now
 * ensures voice 1 fully recedes before voice 2 begins entering -- with a
 * shared position, any real overlap would show text stacked on text,
 * not just "two things visible at once" the way separate corners could
 * tolerate.
 * VERTICAL POSITION raised 24% -> 32% (Visual Polish sprint, ref 01):
 * bottom:24% sat lower than the reference's composition, closer to
 * hugging the very edge than "occupying the image confidently." Raising
 * it moves the block into the same lower-middle band the reference
 * shows while staying clear of .pub-essen-caption's own bottom-anchored
 * box below it. */
.pub-held-menschen{ z-index:3; pointer-events:none; padding:0; }
.pub-menschen-voice{ position:absolute; margin:0; opacity:0;
  font-family:var(--font-d); font-weight:500; line-height:1.22;
  font-size:clamp(27px,3.4vw,42px); letter-spacing:0.004em;
  color:var(--text-primary); max-width:min(560px, 78vw);
  bottom:32%; left:clamp(24px,7vw,80px); text-align:left; }

/* THE SIGNAL -- BRAND PUNCTUATION (Refinement sprint; re-scaled in the
   Visual Polish sprint, ref 02; material rebuilt in the Signal
   Material + Motion sprint). This is its own final beat -- Voice 2
   recedes, the photograph darkens further, a brief unscheduled pause,
   THEN the Signal emerges alone as the scene's closing statement (see
   initHeldStage's timeline comment for the exact sequence). "The people
   disappear, the room goes dark, then CRVD's mark remains."
   SIZE, reduced (Signal Consolidation sprint): min(52vh,560px) ->
   min(42vh,450px). Founder's own real-browser read against a fresh
   screenshot was that it still consumed too much of the frame ("should
   feel dominant, but not consume almost the entire viewport") even
   after the Visual Polish sprint's earlier reduction. Still sized to
   the smaller viewport axis so the complete ring form fits inside the
   frame with room around it.
   CROP-MASK: unchanged this sprint -- 78%/97% radial vignette, soft
   feather only at the empty corners of the bounding box, never
   cropping the ring's own geometry (Visual Polish sprint).
   MATERIAL, rebuilt (Signal Material + Motion sprint, Option C /
   "Satin 3D"): the flat currentColor fill is retired. The rendered
   <svg> (SIGNAL_SVG, geometry byte-identical, never touched) is set to
   opacity:0 below -- it stays in the DOM for markup/ARIA consistency
   with every other Signal instance, but no longer PAINTS anything.
   Two masked pseudo-elements take over the visible material, both
   shaped by --signal-mask-src (a CSS-only duplicate of the exact same
   path/rect coordinates -- see the :root comment for why it's
   duplicated rather than id-referenced):
     ::before -- the satin BODY. A tight-range magenta gradient
     (deep -> lifted -> deep) standing in for restrained metal/satin
     tonal variation. No silver, no new hue -- it still reads as Social
     Magenta at a glance, just no longer perfectly flat.
     ::after -- the ONE-SHOT SHEEN. A soft diagonal highlight band
     GSAP sweeps across the material exactly once during arrival (see
     initHeldStage) via --sheen (position, 0->1) and --sheen-op (a
     short rise/fall wrapped around the sweep, both GSAP-driven CSS
     custom properties) -- never a loop, never re-triggered.
   GLOW retired: the previous drop-shadow bloom is gone entirely --
   "no glow-heavy treatment." Dimensionality now comes from the
   material's own tonal variation and the one controlled reflection
   pass, not from light bleeding outside the mark's edges.
   Motion: scale reset/target changed 1->0.94 / 1.05->1.0 in
   initHeldStage per Founder's exact spec ("scale approximately
   0.94 -> 1.0") -- same signalBreathe reuse and --so/--sc/--sy
   variable discipline as every other Signal instance otherwise
   unchanged -- no bounce, no pulse, no spin, ever. */
.pub-menschen-signal{ position:absolute; inset:0; display:flex;
  align-items:center; justify-content:center; }
.pub-menschen-signal-inner{ width:min(42vh,450px); height:min(42vh,450px);
  position:relative; opacity:var(--so,0);
  -webkit-mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  transform-origin:50% 50%;
  transform:translate(var(--px,0px), calc(var(--py,0px) + var(--sy,0px))) scale(var(--sc,0.94));
  animation:signalBreathe 19s cubic-bezier(.22,.61,.36,1) infinite; }
.pub-menschen-signal-inner svg{ width:100%; height:100%; display:block; opacity:0; }
.pub-menschen-signal-inner::before,
.pub-menschen-signal-inner::after{
  content:''; position:absolute; inset:0;
  -webkit-mask-image:var(--signal-mask-src); mask-image:var(--signal-mask-src);
  -webkit-mask-size:100% 100%; mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat; }
.pub-menschen-signal-inner::before{
  background:var(--signal-satin-gradient); }
.pub-menschen-signal-inner::after{
  background:linear-gradient(115deg,
    transparent 0%, transparent 42%, rgba(255,241,248,.65) 50%, transparent 58%, transparent 100%);
  background-size:280% 280%;
  background-position: calc(-90% + var(--sheen,0) * 260%) calc(-90% + var(--sheen,0) * 260%);
  opacity:var(--sheen-op,0); }
  /* mix-blend-mode:soft-light REMOVED (Food -> Signal Handoff artifact-
     cleanup sprint). A mask-image + mix-blend-mode combination on the
     same element is a well-documented cross-browser compositing risk --
     the blend forces an isolated compositing group for its stacking
     context, and some engines flatten that group against an opaque
     backdrop rather than a true transparent one, which reads as exactly
     the "rectangular dark panel bounded to the element's own box"
     Founder described, most visible once everything behind it (the
     food) has faded low enough to expose it. Plain alpha at .65 base +
     the --sheen-op envelope still reads as a soft highlight during its
     brief one-shot window; losing the blend-into-material quality is a
     small, acceptable trade against a persistent background artifact
     that was visible far outside the sheen's own beat. */

/* FREI . PL8 -- the gap does the work of the old section break. */
.pub-beat-gap{ height:clamp(84px,15vh,178px); }
.pub-beat-quiet{ opacity:0.88; max-width:44em; }
.pub-actions-mid{ margin-top:clamp(48px,7vh,78px); }

/* THE SIGNAL AS ENVIRONMENTAL BRAND ARCHITECTURE -- removed (Signal
   Consolidation sprint). Two partial rings previously sat here as
   static background decoration (Visual Polish sprint, ref 03), then
   got a quieted Satin material (Signal Material + Motion sprint).
   Founder's call: repeating the mark here weakens the one strong
   brand-punctuation moment (the Menschen beat) rather than reinforcing
   it -- "we already have enough THE SIGNAL moments on the page." Not
   replaced with any other decorative treatment; Frei/PL8 now carries
   itself on typography, spacing and motion alone, same as before any
   Signal fragments were ever added here. .pub-section.pub-peak's
   overflow:hidden was also removed -- it existed solely to contain
   these rings and nothing else in this section can overflow. */

.pub-columns{ display:flex; gap:36px; flex-wrap:wrap; justify-content:center; text-align:left; margin-top:36px; }
.pub-col{ max-width:220px; }
.pub-col .b-label{ font-size:12px; font-weight:600; letter-spacing:0.14em; text-transform:uppercase; color:var(--text-primary); margin:0 0 8px; }
.pub-col p{ font-size:14.5px; line-height:1.6; color:var(--text-body); }

.pub-statement{ font-family:var(--font-c); font-style:italic; font-weight:400; font-size:clamp(19px,3vw,24px); line-height:1.5;
  color:var(--text-primary); max-width:560px; margin:24px auto 0; }
/* Finale's italic line as the visual pivot (Visual Polish sprint, ref
   04) -- Founder named it directly: "may use CRVD Magenta if that
   improves the hierarchy." It does: this is the one line in the whole
   sequence marking the turn from case-being-made to action-being-
   offered, and magenta is what makes that turn legible at a glance
   rather than just at reading speed. Scoped to #pub-finale only --
   .pub-statement has exactly one other use in the file (this one), so
   this could have been the base rule, but scoping keeps the base rule
   honest for any future non-finale reuse. */
#pub-finale .pub-statement{ color:var(--magenta-brand); margin-top:clamp(28px,4vh,40px); }

.pub-steps{ display:flex; flex-direction:column; gap:32px; margin-top:40px; text-align:left; }
.pub-step{ display:flex; gap:20px; align-items:flex-start; }
.pub-step-num{ font-family:var(--font-d); font-weight:700; font-size:32px; color:var(--magenta); opacity:0.8; line-height:1; flex-shrink:0; width:52px; }
.pub-step h3{ font-size:17px; margin:2px 0 6px; }
.pub-step p{ font-size:14px; line-height:1.6; color:var(--text-body); margin:0; }

.pub-ticker{ margin-top:32px; text-align:left; }
.pub-tick-line{ font-size:14px; line-height:1.6; color:var(--text-body); margin:0 0 10px; }
.pub-tick-line .who{ color:var(--text-primary); font-weight:600; }
.pub-tick-line .when{ color:var(--text-secondary); font-size:12px; }

.pub-finale{ min-height:calc(100svh - var(--preview-banner-h)); display:flex; align-items:center; text-align:center; }
.pub-finale .pub-lede-lg{ margin-left:auto; margin-right:auto; }
/* RECOMPOSED AS ONE CENTERED FINAL FRAME (Visual Polish sprint, ref 04).
   The section was already vertically/horizontally centered via the
   rules above -- what was actually missing was finish, not containment:
   .pub-lede-lg and .pub-statement already carry their own narrower
   max-widths (34em / 560px) independent of the section container, which
   is what gives the reference's "controlled narrow block" feel for the
   supporting copy while the headline keeps the wider .pub-wide container
   it needs to hold its authored 2-line break (a narrower container was
   tried and rejected here -- it forced "Heute isst du sowieso." and "Die
   Frage ist nur, wie." to each wrap a second time, turning 2 lines into
   4). The real gaps were: the action row had no explicit layout of its
   own (two inline-block buttons just wrapped at whatever gap their HTML
   whitespace happened to produce), and .reassurance had NEVER been
   styled outside .pub-hero's own scope -- on the Finale it was rendering
   as a full-size, unmuted paragraph, which is very likely the real
   source of "feels scattered": the last line was reading as loud as the
   lines before it instead of trailing off. */
#pub-finale .actions{ display:flex; align-items:center; justify-content:center;
  gap:18px; flex-wrap:wrap; }
#pub-finale .reassurance{ margin-top:22px; font-size:13px; color:var(--muted); }

.pub-footer{ position:relative; z-index:2; padding:32px max(24px, env(safe-area-inset-right)) max(48px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
  text-align:center; color:var(--muted); font-size:12.5px; }

@media (min-width:641px){
  .pub-nav .pub-login{ display:inline-block; }
}
@media (min-width:768px){
  .pub-section-inner{ max-width:760px; }
}
@media (min-width:1024px){
  .pub-section-inner{ max-width:820px; }
  .pub-section-inner.wide{ max-width:920px; }
  .pub-section-inner.pub-wide{ max-width:940px; }
  .pub-columns{ justify-content:center; }
  .pub-steps{ flex-direction:row; gap:44px; }
  .pub-step{ flex-direction:column; gap:14px; }
  .pub-step-num{ width:auto; font-size:38px; }
}

/* Mobile is recomposed, not scaled down (Public Experience Redesign,
   Phase 2). The peak/quiet rhythm is preserved -- it is the point of the
   design -- but compressed, because at 390px the desktop ratios would
   leave a 64px headline occupying most of the viewport with nothing to
   anchor it. Vertical rhythm tightens proportionally so the page does not
   become a sequence of near-empty screens on a small device: generous is
   the goal, sparse is not. */
@media (max-width:640px){
  .pub-section{ padding-top:clamp(84px,12vh,124px); padding-bottom:clamp(84px,12vh,124px); }
  /* No viewport floor on mobile: an 82svh minimum would strand short
     scenes as near-empty screens on a phone. Padding alone carries the
     rhythm here. */
  .pub-section.pub-peak{ padding-top:clamp(100px,15vh,152px); padding-bottom:clamp(100px,15vh,152px); min-height:0; }
  .pub-h-peak{ font-size:clamp(31px,8.6vw,40px); }
  .pub-h-final{ font-size:clamp(33px,9.6vw,46px); }
  .pub-h-mid{ font-size:clamp(26px,7.4vw,34px); }
  .pub-h-sub{ font-size:clamp(19px,5.4vw,23px); }
  .pub-h-quiet{ font-size:clamp(20px,5.6vw,24px); }
  .pub-lede-lg{ font-size:16px; }
  /* Das Essen mobile -- independently composed, not shrunk. No pin (the
     scene never had one), shorter scene height (160vh vs 240vh -- less
     travel needed since there's no separate settle phase to fit), a
     shorter sticky stage (88svh: Safari's collapsing address bar changes
     100svh mid-scroll on iOS, and 88svh leaves a visible margin so that
     shift is never perceptible on the one element in the page tall
     enough for it to matter), and a genuinely portrait contained box
     (4:5) since the source photograph is landscape and a 16:10 box at
     390px width leaves the dish a thin, unappetising band. */
  /* Held stage on mobile: same EXPERIENCE (stage locks, Frage holds,
     food resolves in place), no pin, native touch untouched. 100svh --
     not vh -- because Safari's collapsing address bar changes vh
     mid-scroll, which on a full-viewport locked stage would be a visible
     resize of the photograph. Shorter wrapper: touch flicks cover more
     distance per gesture, so the same journey needs less travel. */
  /* 215vh -> 235vh, proportional to the desktop increase (260->285vh)
     for the Refinement sprint's new Signal-as-final-beat content. Same
     narrative, cheaper mechanism throughout -- verified live against
     real measured pixel ranges. */
  /* 235vh -> 247vh, proportional to the desktop increase (285->300vh)
     for the Visual Polish sprint's longer voice holds. */
  /* 247vh -> 262vh, proportional to the desktop increase (300->318vh)
     for the Food -> Signal Handoff sprint's recession + solo hold. */
  .pub-held-wrap{ height:262vh; }
  .pub-held-stage{ height:100svh; }
  /* Mobile is HEIGHT-constrained the opposite way from desktop: cover
     keeps the full source height and crops WIDTH severely (a ~390px-
     wide slice out of a 1536px-wide source at 390x844), so X is what
     matters here, not Y. 44% keeps the hand-to-food interaction point
     inside that narrow window rather than centering on empty space to
     one side of it. */
  .pub-essen-img{ object-position:44% 50%; }
  /* Scrim retired on mobile -- a 300%-oversized radial gradient is real
     compositing cost, and the emergence metaphor survives without it:
     brightness/contrast alone (still animated in initHeldStage) still
     reads as light arriving, just without the directional quality. */
  .pub-essen-scrim{ display:none; }
  .pub-essen-caption{ padding-bottom:clamp(28px,5vh,48px); }
  /* Menschen mobile -- smaller type, narrower blocks so asymmetrical
     placement doesn't collide with the viewport edges, and NO positional
     drift on the Signal (opacity + scale only, per the approved mobile
     spec) since a continuous background-thread drift is the one part of
     this appearance most likely to feel unstable on iOS Safari's own
     scroll/zoom handling. */
  /* Shared position (both voices), matching the desktop composition fix
     -- one statement dissolves, the next arrives in its place. Raised
     26% -> 34% (Visual Polish sprint), same reasoning as the desktop
     32% adjustment above. */
  .pub-menschen-voice{ font-size:25px; line-height:1.24; max-width:78vw;
    bottom:34%; left:20px; text-align:left; }
  /* 240vw -> min(46vh,320px) (Visual Polish sprint) -> min(37vh,260px)
     (Signal Consolidation sprint), proportional to the desktop
     reduction (52->42vh, a ~19% cut) so the mark reads at the same
     relative dominance on both breakpoints. Still sized to the smaller
     viewport axis so the complete ring form fits on screen instead of
     only a fragment. */
  .pub-menschen-signal-inner{ width:min(37vh,260px); height:min(37vh,260px); }
  .pub-beat-gap{ height:clamp(48px,7vh,72px); }
  /* Full-width primary CTA -- the mid-page conversion point should not be
     a small pill on the surface where most visitors will read this. */
  .pub-actions-mid .btn{ width:100%; }
}

/* ==========================================================================
   MEMBER PRODUCT — responsive PWA shell. No device frame at any width.
   ========================================================================== */
#app{ display:none; min-height:100vh; background:var(--canvas); }
.app-shell{ display:flex; flex-direction:column; min-height:100vh; }

/* Sprint 4 -- Registration & Entry atmosphere. Independent of the public
   homepage's own particle canvas (that code is untouched) -- same visual
   language, own small implementation, scoped to the entry screens only. */
.entry-shell{ position:relative; min-height:calc(100vh - 65px - var(--preview-banner-h)); display:flex; align-items:center; overflow:hidden; }
#entry-net{ position:fixed; inset:0; width:100vw; height:100vh; z-index:0; pointer-events:none; }
.entry-veil{ position:fixed; inset:0; z-index:1; pointer-events:none;
  background:radial-gradient(1100px 800px at 50% 46%, transparent 0%, var(--canvas) 82%); }
.entry-grid{ position:relative; z-index:2; width:100%; max-width:420px; margin:0 auto; padding:48px 24px;
  display:flex; flex-direction:column; gap:32px; text-align:center; }
.entry-story .kicker{ margin-bottom:14px; }
.entry-story h1{ font-family:var(--font-d); font-weight:700; font-size:clamp(28px,5vw,40px); line-height:1.1; text-transform:uppercase; margin:0 0 14px; }
.entry-story .muted-line{ color:var(--muted); font-weight:500; }
.entry-story .entry-lede{ font-size:14.5px; color:var(--text-body); margin:0 0 12px; }
.entry-story .entry-next{ font-size:13.5px; color:var(--text-body); line-height:1.6; max-width:360px; margin:0 auto; }
.entry-story .entry-next strong{ color:var(--text-primary); }
.entry-form{ text-align:center; }
/* Vessel Login Hero Rework: on the register/entry screens, the Signal is
   the hero -- not atmosphere, the actual story ("the detached square is
   the seat you're about to take"). Considerably more visible than the
   homepage hero's own background use since it has to be legible as
   storytelling, not felt as texture (opacity .16 here vs. Hero's low-
   intensity Satin Magenta material, DESIGN_CONSTITUTION.md §1.2/DH-13 --
   this instance still uses the flat fill this file predates, out of
   scope for the Signal Governance sprint). Same construction, same
   breathing motion (continuity, not a new animation), just louder. */
.entry-story{ position:relative; }
.entry-signal{ position:absolute; inset:0; z-index:-1; pointer-events:none;
  display:flex; align-items:center; justify-content:center; }
.entry-signal-inner{ width:min(78vw,640px); height:min(78vw,640px);
  color:rgba(245,241,236,.16);
  /* CROP-MASK, corrected (Signal Scale + Proportion sprint): 55%/80% ->
     78%/97%, same fix and same reasoning as .pub-hero-signal -- the old
     stops placed the ring's own stroke radius inside the mask's fade
     zone (measured ring alpha ~57%), thinning it well below its actual
     7%-of-viewBox weight. 78%/97% keeps the ring fully opaque and only
     fades the empty corners. Base fill alpha (.16) is untouched --
     opacity is an allowed context variable, this fixes only the ratio
     distortion. */
  -webkit-mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  animation:signalBreathe 19s cubic-bezier(.22,.61,.36,1) infinite; }
.entry-signal-inner svg{ width:100%; height:100%; display:block; }

@media (min-width:1024px){
  .entry-grid{ max-width:960px; flex-direction:row; align-items:center; gap:72px; text-align:left; padding:48px max(48px, env(safe-area-inset-right)) 48px max(48px, env(safe-area-inset-left)); }
  .entry-story{ flex:1; }
  .entry-story .entry-next{ margin:0; }
  .entry-form{ flex:0 0 380px; text-align:center; }
}

/* Editorial Header (PACE Intro Final Refinement follow-up): the header
   was reading as generic app chrome regardless of logo size -- a
   floating glass toolbar with a colored chip. Fixed by giving the bar
   real vertical air (14px -> 24px) and quieting the FREE badge, not by
   scaling the mark further. Horizontal padding/structure unchanged. */
.app-topbar{ display:flex; align-items:center; justify-content:space-between;
  padding:24px max(20px, env(safe-area-inset-right)) 24px max(20px, env(safe-area-inset-left));
  border-bottom:1px solid var(--hairline); position:sticky; top:var(--preview-banner-h); background:rgba(16,14,19,0.92); backdrop-filter:blur(6px); z-index:10; }
.app-brand{ background:none; border:none; color:var(--text-primary); font-family:var(--font-d); font-weight:700; font-size:14px; letter-spacing:0.06em; text-transform:uppercase; cursor:pointer; padding:0; }
.app-brand .m{ color:var(--magenta); }
/* Real wordmark SVG -- the brand anchor for the entire app shell (16px
   -> 19px -> 22px -> 24px mobile / 26px -> 30px desktop across passes).
   Still clearly secondary to THE SIGNAL's much larger scale at both
   sizes; most of this pass's presence increase comes from the header's
   own breathing room, not further scaling the mark itself. */
.app-brand-svg{ display:flex; align-items:center; }
.app-brand-svg svg{ display:block; height:24px; width:auto; }
.app-brand-svg .wordmark-dot{ fill:var(--magenta); }
@media (min-width:1024px){
  .app-brand-svg svg{ height:30px; }
}
.app-topbar-right{ display:flex; align-items:center; gap:16px; }
/* Editorial Header: the FREE chip read as a SaaS plan-tier badge --
   filled pill, high contrast. In the topbar only, it drops to a quiet
   tracked-caps label (no fill, no padding, muted color) so it doesn't
   compete with the brand mark. Uppercase/letterspacing kept -- that
   typographic treatment is itself editorial, the filled shape was the
   actual tell. Scoped to .app-topbar so .badge-free stays unchanged
   everywhere else it's used (dashboard, etc). */
.app-topbar .badge-free{ background:transparent; color:var(--muted); padding:0; }
.app-icon-btn{ width:38px; height:38px; border-radius:50%; border:1px solid var(--hairline); background:var(--surface); color:var(--text-primary);
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.app-icon-btn svg{ width:18px; height:18px; }
/* Ceremony -> PACE Vessel Transition: on arrival straight out of the
   ceremony, the topbar holds off and fades up softly once the PACE
   content itself has landed, instead of slamming back in at full
   contrast the instant the overlay is gone. */
@keyframes topbarSoftIn{ from{ opacity:0; transform:translateY(-6px); } to{ opacity:1; transform:translateY(0); } }
.app-topbar-soft{ animation:topbarSoftIn 700ms var(--patient) 850ms backwards; }

.app-body{ flex:1; display:flex; }
.app-content{ flex:1; min-width:0; padding:var(--s5) max(16px, env(safe-area-inset-right)) var(--mobile-safe-bottom) max(16px, env(safe-area-inset-left)); }
.app-content-inner{ max-width:640px; margin:0 auto; }

/* Ceremony -> PACE Vessel Transition: PACE is the second beat of the
   ceremony, not a new screen. Same Vessel atmosphere (vignette/grain)
   as the ceremony overlay, carried across the cut instead of dropping
   to a flat canvas. THE SIGNAL stays present in its settled resting
   state -- identical geometry, no travel, no peak -- as a quiet
   throughline connecting the two screens; PACE's own question is the
   hero here, so the Signal sits low-opacity and masked behind it,
   never competing. Composition is deliberately centered in the
   available height instead of stranded at the top. */
.pace-arrival{ position:relative; display:flex; align-items:center; justify-content:center;
  min-height:62vh; padding:var(--s6) 0; }
/* Fix PACE Intro Container: vignette/grain were scoped to .pace-arrival's
   own (narrower, shorter) box, clipped by its overflow -- the gradient's
   dark falloff terminated at that box's edges, reading as a framed panel
   sitting on the flatter canvas outside it. Full-viewport + fixed (the
   same pattern the ceremony overlay and homepage hero already use) so
   the atmosphere is the room, not a card placed on top of it. */
.pace-vignette{ position:fixed; inset:0; z-index:0; pointer-events:none;
  background:radial-gradient(1100px 800px at 50% 46%, rgba(255,255,255,.022) 0%, transparent 45%, rgba(0,0,0,.55) 100%); }
.pace-grain{ position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.12; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='%75%72%6C%28%23n%29'/%3E%3C/svg%3E");
  background-size:120px 120px; }
.pace-signal{ position:absolute; inset:0; z-index:0; pointer-events:none; display:flex; align-items:center; justify-content:center; }
/* PACE Intro Final Refinement: smaller and dimmer still (420px/.1 opacity
   -> 340px/.07), and the breathing animation is removed entirely -- a
   still, quiet trace of the ceremony, not a second living element next
   to the PACE question. Noticed, not read. */
.pace-signal-inner{ width:min(52vw,340px); height:min(52vw,340px); color:rgba(245,241,236,.07);
  /* CROP-MASK, corrected (Signal Proportion Consistency sprint): 55%/80%
     -> 78%/97%, same root-cause fix as .pub-hero-signal and
     .entry-signal-inner -- the old stops placed the ring's own stroke
     radius inside the mask's fade zone, thinning it well below its
     actual 7%-of-viewBox weight. 78%/97% keeps the ring fully opaque
     and only fades the empty corners. Opacity (.07), size, and the
     removed breathing animation are all untouched. */
  -webkit-mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
  mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%); }
.pace-signal-inner svg{ width:100%; height:100%; display:block; }
.pace-content{ position:relative; z-index:1; max-width:480px; text-align:center; padding:0 var(--s4); }
.pace-question{ font-family:var(--font-d); font-weight:700; text-transform:uppercase;
  font-size:clamp(28px,5vw,40px); line-height:1.1; margin:0 0 18px; color:var(--text-primary); }
.pace-lede{ font-size:14.5px; color:var(--text-body); line-height:1.6; margin:0 0 36px; }
/* PACE Intro Final Refinement: the CTA reads as the third beat, not a
   slab competing with the question above it -- intrinsic width instead
   of full-block, and a touch less vertical weight. Same magenta, same
   wording, same button language; only width/height/spacing change. */
.pace-cta{ width:auto; min-width:220px; padding:12px 30px; min-height:44px; }

/* ==========================================================================
   RECOGNITION SCREEN — Direction A, editorial sequence.
   Replaces the 2x2 selection grid. Four situations read as a vertical
   editorial sequence; no cards, no boxes, no filled surfaces. Whitespace
   is the only separator. Mobile is the base composition; desktop adds an
   asymmetric two-zone split where the question stays present (sticky)
   while the situations scroll past it.

   Atmosphere layers are position:fixed and must never sit under a
   transformed ancestor -- render() applies .screen-anim-static to this
   screen for exactly that reason. Grain uses the encoded filter
   reference the Netlify asset scanner requires.
   ========================================================================== */
.rec{ position:relative; }
.rec-vignette{ position:fixed; inset:0; z-index:0; pointer-events:none;
  background:radial-gradient(1100px 800px at 50% 46%, rgba(255,255,255,.022) 0%, transparent 45%, rgba(0,0,0,.55) 100%); }
.rec-grain{ position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.12; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='%75%72%6C%28%23n%29'/%3E%3C/svg%3E");
  background-size:120px 120px; }
.rec-grid{ position:relative; z-index:1; max-width:1240px; margin:0 auto; }

.rec-ask{ position:relative; }
.rec-question{ font-family:var(--font-d); font-weight:700; text-transform:uppercase;
  font-size:clamp(28px,4.5vw,44px); line-height:1.1; letter-spacing:0.004em;
  color:var(--text-primary); margin:0 0 var(--s4); }
.rec-support{ font-family:var(--font-c); font-size:13.5px; line-height:1.6;
  color:var(--muted); margin:0; max-width:38ch; }
/* Signal is desktop-only -- see the min-width:1024px block. On mobile the
   reading column occupies the full width and a ring behind 38-word German
   paragraphs costs legibility without adding continuity; the canvas,
   vignette and grain already carry the Vessel world. */
.rec-signal{ display:none; }

.rec-list{ margin-top:var(--s7); }

/* Each complete situation is one recognition target: a real button for
   keyboard and screen-reader correctness, styled as editorial text rather
   than as a control. The always-present transparent left border reserves
   the space the selected rule occupies, so selection causes no layout
   shift. */
.rec-item{ display:block; width:100%; text-align:left; background:none; border:none;
  border-left:2px solid transparent; padding:0 0 0 var(--s4); margin:0;
  font-family:inherit; color:inherit; cursor:pointer;
  transition:border-color 240ms var(--patient); }
.rec-item + .rec-item{ margin-top:var(--s8); }
.rec-anchor{ display:block; font-family:var(--font-d); font-weight:700;
  font-size:clamp(20px,2.4vw,26px); line-height:1.22; letter-spacing:0.002em;
  color:var(--text-primary); margin-bottom:var(--s2);
  transition:color 240ms var(--patient); }
.rec-body{ display:block; font-family:var(--font-c); font-size:15.5px; line-height:1.65;
  color:var(--text-body); max-width:62ch; transition:color 240ms var(--patient); }
/* Hover is deliberately almost nothing -- it previews the left rule that
   selection uses, teaching the mechanic. No lift, no shadow, no fill. */
.rec-item:hover{ border-left-color:var(--hairline); }
.rec-item:focus-visible{ outline:2px solid var(--magenta); outline-offset:8px; }
.rec-item.is-recognized{ border-left-color:var(--magenta); }
/* Recognized: the true one stays lit and the others settle back. They
   remain fully readable and fully selectable -- recession, never
   elimination, and nothing reads as a wrong answer. */
.rec.has-selection .rec-item:not(.is-recognized) .rec-anchor,
.rec.has-selection .rec-item:not(.is-recognized) .rec-body{ color:var(--text-secondary); }

.rec-closing{ font-family:var(--font-c); font-size:12.5px; color:var(--muted);
  margin:var(--s7) 0 0; padding-left:var(--s4); }
.rec-continue{ margin-top:var(--s5); padding-left:var(--s4);
  opacity:0; visibility:hidden; transition:opacity 420ms var(--patient); }
.rec.has-selection .rec-continue{ opacity:1; visibility:visible; }
.rec-continue-btn{ width:auto; min-width:200px; padding:12px 30px; min-height:44px; }

@media (min-width:1024px){
  /* Desktop amplifies: the question becomes a persistent editorial anchor
     rather than a heading that scrolls away. Not a sidebar -- no rule, no
     panel, no background. */
  .rec-grid{ display:grid; grid-template-columns:38% 1fr; gap:var(--s9); align-items:start; }
  /* min-height is tied to the viewport, not to the right column's content
     height -- the situation copy is explicitly working/revisable, so
     sizing the left zone off it would make this layout brittle to a
     content edit. Centering the text within that height is what turns
     the space below the question into deliberate framing instead of
     leftover room the Signal happens to get clipped against. */
  .rec-ask{ position:sticky; top:calc(var(--preview-banner-h) + 128px); padding-right:var(--s6);
    min-height:56vh; display:flex; flex-direction:column; justify-content:center; }
  .rec-list{ margin-top:0; }
  .rec-signal{ display:flex; position:absolute; inset:0; z-index:-1;
    align-items:center; justify-content:flex-start; pointer-events:none; }
  .rec-signal-inner{ width:min(27vw,440px); height:min(27vw,440px);
    margin-left:-14%; color:rgba(245,241,236,.06);
    /* CROP-MASK, corrected (Signal Proportion Consistency sprint): same
       55%-based-stops root-cause fix as every other instance -- see
       .pub-hero-signal's comment for the full reasoning. Opacity (.06),
       size, and position (margin-left:-14%) untouched. */
    -webkit-mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%);
    mask-image:radial-gradient(circle at 50% 50%, black 78%, transparent 97%); }
  .rec-signal-inner svg{ width:100%; height:100%; display:block; }
}

/* ----------------------------------------------------------------------
   ADAPTATION PROOF -- one descending editorial column. The member has
   finished deciding; this screen is about receiving something, so there is
   no two-zone split and no grid. Composition encodes interaction state:
   Recognition's two zones held a question against options, a single column
   says the deciding is over.

   THE SIGNAL is absent by design -- ceremonial peak, then atmospheric
   memory on Recognition, then zero. Vessel continuity is carried by the
   canvas, vignette, grain and typography alone.
   ---------------------------------------------------------------------- */
.ap{ position:relative; }
/* .app-content reserves --mobile-safe-bottom (190px) for the bottom nav and
   the PL8 FAB. This screen renders neither -- render()'s isMode branch hides
   the nav, and 'welcome' isn't in FAB_SCREENS -- so that clearance was pure
   dead space, leaving ~63px of empty scroll below a composition that already
   fits the viewport. Scoped with :has() instead of touching the global rule,
   the same idiom .app-content-reader:has(.background-overview) already uses.
   Safe-area inset preserved. */
@media (max-width:1023px){
  .app-content:has(.ap){ padding-bottom:max(var(--s5), env(safe-area-inset-bottom)); }
}
/* .app-shell is min-height:100vh, but the preview banner is fixed above it
   and body carries a matching padding-top -- so the shell always ends one
   banner-height past the viewport and every screen scrolls by that much.
   Subtracting it is the idiom the codebase already uses for full-height
   elements (.pub-hero, .entry-shell); the variable is responsive (56px
   mobile / 36px from 480px), so this stays correct at both. Scoped to this
   screen: the banner is prototype-only scaffolding, and the screens that
   legitimately scroll are unaffected either way. #app and .app-shell both
   declare min-height:100vh, so both need it -- correcting only one leaves
   the other setting the page height. */
#app:has(.ap), .app-shell:has(.ap){ min-height:calc(100vh - var(--preview-banner-h)); }
/* Same fixed atmosphere as Recognition. Fixed layers require this screen
   to be in render()'s staticScreen opt-out -- .screen-anim's translateY
   would otherwise become their containing block and scope them to the
   wrapper's content box (the diagnosed boxed-vignette artifact). */
.ap-vignette{ position:fixed; inset:0; z-index:0; pointer-events:none;
  background:radial-gradient(1100px 800px at 50% 46%, rgba(255,255,255,.022) 0%, transparent 45%, rgba(0,0,0,.55) 100%); }
.ap-grain{ position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.12; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='%75%72%6C%28%23n%29'/%3E%3C/svg%3E");
  background-size:120px 120px; }

/* Centred column, left-aligned text: centring the text would break the
   editorial register, centring the column keeps the descent calm without
   drifting into dashboard asymmetry. */
.ap-col{ position:relative; z-index:1; max-width:600px; margin:0 auto; padding-top:var(--s5); }

/* Below Recognition's question scale -- the journey's type scale descends
   deliberately as the entrance sequence winds down. */
.ap-headline{ font-family:var(--font-d); font-weight:700; text-transform:uppercase;
  font-size:clamp(26px,3.4vw,38px); line-height:1.12; letter-spacing:0.004em;
  color:var(--text-primary); margin:0 0 var(--s6); }

/* The artifact is content, not a component: no border, no filled card, no
   nested button. The whole thing is the tap target, and it carries the
   visual weight of the screen. */
.ap-artifact{ display:block; width:100%; text-align:left; background:none; border:none;
  padding:0; margin:0; font-family:inherit; color:var(--text-primary); cursor:pointer;
  transition:transform 220ms var(--patient); }
.ap-artifact:hover{ transform:translateY(-2px); }
.ap-artifact:focus-visible{ outline:2px solid var(--magenta); outline-offset:10px; }

/* The only intentional magenta at rest. Same weight and colour as the
   Recognition screen's selected-state rule, one step down the causal
   chain: there magenta meant "this is your current situation", here it
   means "this exists because of that". Sits above the image so the member
   reads why before what. */
.ap-proof{ display:block; border-left:2px solid var(--magenta); padding-left:var(--s4);
  font-family:var(--font-c); font-size:15px; line-height:1.55; color:var(--text-body);
  max-width:46ch; margin-bottom:var(--s5); }

.ap-thumb{ display:block; height:210px; border-radius:var(--radius); position:relative; overflow:hidden;
  background:linear-gradient(135deg, var(--surface-elevated), var(--canvas)); margin-bottom:var(--s4); }
.ap-thumb::after{ content:''; position:absolute; inset:0;
  background:radial-gradient(60% 60% at 30% 30%, rgba(255,255,255,.05), transparent 70%); }

/* Largest type inside the artifact, but deliberately under the headline --
   the frame still outranks the object it frames. */
.ap-name{ display:block; font-family:var(--font-d); font-weight:700; text-transform:uppercase;
  font-size:clamp(21px,2.4vw,27px); line-height:1.15; letter-spacing:0.002em; margin-bottom:var(--s2); }
.ap-meta{ display:block; font-family:var(--font-c); font-size:13px; color:var(--muted); }

/* Quiet by construction: text weight, no slab, no magenta. Onboarding
   should stop rather than announce that it has finished. */
/* Underline via text-decoration rather than a border, so it stays tight to
   the word while the vertical padding still carries the 44px tap target --
   a bordered box would float the rule away from the text. */
.ap-onward{ display:inline-block; background:none; border:none;
  padding:14px 0; margin-top:var(--s6);
  font-family:var(--font-c); font-size:14px; color:var(--muted); cursor:pointer;
  text-decoration:underline; text-underline-offset:6px; text-decoration-thickness:1px;
  text-decoration-color:var(--hairline);
  transition:color 200ms var(--patient), text-decoration-color 200ms var(--patient); }
.ap-onward:hover{ color:var(--text-primary); text-decoration-color:var(--border-strong); }
.ap-onward:focus-visible{ outline:2px solid var(--magenta); outline-offset:4px; }

@media (min-width:1024px){
  /* Desktop amplifies through surrounding space only -- the column keeps
     its intrinsic width at every width, and nothing is added to fill the
     canvas at 1920.

     Vertical placement is centred in the space actually available rather
     than set by a fixed top padding: a fixed padding put the composition
     23px off the bottom at 1280x800 and left 303px of room below it at
     1920x1080 -- the same number in two very different rooms.

     The upward bias comes from .app-content's own asymmetric padding
     (32px top / 64px bottom), so the column settles above true centre for
     free. An explicit margin was tried first and rejected: it adds layout
     height, and at 1280x800 there are only ~39px of slack, so it
     reintroduced a scrollbar on the one size with no room to spare. */
  .app-content:has(.ap){ display:flex; flex-direction:column; justify-content:center; }
  .ap-col{ padding-top:0; }
  .ap-thumb{ height:250px; }
}

/* bottom nav — mobile / tablet */
.app-bottomnav{ position:fixed; left:0; right:0; bottom:0; z-index:10; display:flex; justify-content:space-around;
  background:rgba(16,14,19,0.96); backdrop-filter:blur(8px); border-top:1px solid var(--hairline);
  padding:8px max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); }
.app-bottomnav button{ background:none; border:none; color:var(--muted); font-size:11px; display:flex; flex-direction:column; align-items:center; gap:4px;
  padding:6px 10px; cursor:pointer; min-width:56px; min-height:48px; }
.app-bottomnav button svg{ width:20px; height:20px; }
.app-bottomnav button.active{ color:var(--text-primary); }
.app-bottomnav button.active svg{ color:var(--magenta); }

/* sidebar nav — desktop */
.app-sidebar{ display:none; }

/* PL8 AI floating control */
.pl8-fab{ position:fixed; right:max(18px, env(safe-area-inset-right)); bottom:86px; z-index:11;
  background:var(--magenta); color:var(--canvas); border:none; border-radius:999px; padding:13px 20px 13px 16px;
  display:flex; align-items:center; gap:8px; font-weight:700; font-size:13px; letter-spacing:0.02em; cursor:pointer;
  box-shadow:0 8px 24px rgba(226,25,181,0.28); transition:transform 200ms var(--patient); }
.pl8-fab:hover{ transform:translateY(-2px); }
.pl8-fab svg{ width:17px; height:17px; }

.screen-anim{ animation:screenIn 320ms var(--patient); }
/* Fix PACE Intro Screen Animation Opt-Out: no animation, no transform --
   PACE intro only, so its fixed-position atmosphere layers never get a
   transformed containing block. See render() for where this is applied. */
.screen-anim-static{ animation:none; transform:none; }
@keyframes screenIn{ from{ opacity:0; transform:translateY(8px); } to{ opacity:1; transform:translateY(0); } }

/* ---------------------------------------------------------------------- *
 * Sprint 13 -- Motion: guidance (Dashboard), recognition (Recipe Detail).
 * Each fires once, in its own place; none of them persist or repeat.
 * The Mode-grid entry that used to belong here (.mode-card-in /
 * modeCardIn) was the old 2x2 grid's animation -- removed as dead code
 * once Direction A replaced that grid (Legacy Personalization Decoupling
 * audit; the old grid's own removal predates this sprint).
 * ---------------------------------------------------------------------- */
.mode-belonging-in{ animation:belongingIn 450ms var(--patient) backwards; animation-delay:900ms; }
@keyframes belongingIn{ from{ opacity:0; transform:translateY(6px); } to{ opacity:1; transform:translateY(0); } }

.chef-note{ min-height:1.2em; font-size:17px; line-height:1.4; color:var(--magenta-text-safe); font-family:var(--font-d); text-transform:uppercase; letter-spacing:0.01em; opacity:0; margin-bottom:var(--s2); }
.hero-bloom{ animation:heroBloom 450ms var(--patient) forwards; }
.hero-bloom-fade{ animation:heroBloomFade 450ms var(--patient) forwards; }
@keyframes heroBloom{ from{ box-shadow:0 0 0 0 rgba(226,25,181,0); transform:scale(1); } to{ box-shadow:0 0 90px 14px rgba(226,25,181,0.4); transform:scale(1.025); } }
@keyframes heroBloomFade{ from{ box-shadow:0 0 90px 14px rgba(226,25,181,0.4); transform:scale(1.025); } to{ box-shadow:0 0 0 0 rgba(226,25,181,0); transform:scale(1); } }
.chef-note-show{ animation:chefNoteIn 300ms var(--patient) forwards; }
.chef-note-hide{ animation:chefNoteOut 300ms var(--patient) forwards; }
@keyframes chefNoteIn{ from{ opacity:0; transform:translateY(10px) scale(0.94); } to{ opacity:1; transform:translateY(0) scale(1); } }
@keyframes chefNoteOut{ from{ opacity:1; transform:translateY(0) scale(1); } to{ opacity:0; transform:translateY(6px) scale(0.97); } }
.recognition-dim > *{ transition:opacity 400ms var(--patient); }
.recognition-dim > *:not(#recipe-hero):not(#chef-note){ opacity:0.5; }

/* dashboard -- Sprint 18.2 hierarchy: Wochenfokus (1) > free primary path (2)
   > Community (3) > PL8 teaser (4). Free and PL8 deliberately no longer share
   a treatment: the free path reads stronger through fill+accent-bar, PL8
   reads quieter through the restrained premium surface, so PL8 never
   visually outweighs the free experience. */
.dash-greeting{ font-size:13px; color:var(--text-secondary); margin-bottom:4px; }
.dash-continuity{ font-size:15px; margin-bottom:var(--s6); }
.dash-grid{ display:grid; grid-template-columns:1fr; gap:24px; }
.ritual-card{ background:linear-gradient(160deg, var(--surface-elevated) 0%, var(--surface) 100%); border:1px solid var(--hairline); border-radius:var(--radius); padding:var(--s6); cursor:pointer; }
.ritual-card h3{ font-size:18px; margin:6px 0 8px; }
.ritual-card p:not(.kicker){ font-size:14px; color:var(--text-body); line-height:1.55; }
.presence{ display:flex; align-items:center; gap:8px; margin:10px 0 22px; font-size:13px; color:var(--text-secondary); font-family:var(--font-c); }
.presence-dot{ width:7px; height:7px; border-radius:50%; background:var(--text-secondary); flex-shrink:0; animation:presencePulse 3.4s cubic-bezier(.22,.61,.36,1) infinite; }
@keyframes presencePulse{ 0%,100%{ opacity:.4; transform:scale(0.85); } 50%{ opacity:1; transform:scale(1); } }
.ritual-progress{ display:flex; gap:6px; margin-top:14px; }
.ritual-progress span{ flex:1; height:4px; border-radius:2px; background:var(--hairline); }
.ritual-progress span.done{ background:var(--magenta); }
.quicklinks{ display:grid; grid-template-columns:1fr 1fr; gap:16px; }
.quicklink{ background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius); padding:16px; text-align:left; cursor:pointer; color:var(--text-primary); }
.quicklink:hover{ border-color:var(--border-strong); }
/* the primary free action: distinguished by fill + a left accent bar, not a
   tint-box -- deliberately a different pattern than the PL8 teaser below. */
.quicklink-primary{ background:var(--surface-2); border:1px solid var(--hairline); border-left:3px solid var(--magenta); padding-left:13px; }
.quicklink-primary h4{ font-weight:700; }
.quicklink .kicker{ margin-bottom:4px; }
.quicklink h4{ font-size:15px; }
.pl8-teaser-card{ border:1px solid var(--hairline); border-radius:var(--radius); padding:var(--s5); background:var(--surface-premium); cursor:pointer; }
.pl8-teaser-card:hover{ border-color:var(--border-premium); }
.pl8-teaser-card h4{ font-size:15px; margin:6px 0 6px; }
.pl8-teaser-card p{ font-size:13.5px; color:var(--text-body); line-height:1.5; }

/* recipes -- Sprint 18.2: thumbnail gradients moved off the magenta glow
   (which read as decorative noise, not food) to a restrained warm charcoal
   gradient plus a soft warm highlight, closer to gentle kitchen light than
   a colored UI accent. */
/* Sprint 19.1 -- visual hierarchy pass. Surface roles used across Recipes:
   --surface-elevated marks the one recommended/primary tile (feature tile,
   facts tile); --surface marks ordinary content tiles (recipe cards);
   everything else (fit lists, ingredients, steps, the PL8 aside) stays flat
   against the canvas and is separated by spacing and hairlines, not boxes. */
.recipe-grid{ display:grid; grid-template-columns:1fr; gap:20px; }
.recipe-card{ background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius); overflow:hidden; cursor:pointer; transition:border-color 200ms var(--patient); }
.recipe-card:hover{ border-color:var(--border-strong); }
.recipe-card .thumb{ height:130px; background:linear-gradient(135deg, var(--surface-elevated), var(--canvas)); position:relative; }
.recipe-card .thumb::after{ content:''; position:absolute; inset:0; background:radial-gradient(60% 60% at 30% 30%, rgba(255,255,255,0.04), transparent 70%); }
.recipe-card .body{ padding:16px; }
.recipe-card h4{ font-size:15px; font-weight:700; margin:6px 0 6px; }
.recipe-card .meta{ font-size:12.5px; color:var(--text-secondary); }

/* the feature tile -- one recommended recipe, on the elevated surface,
   wider and taller than the standard cards below it. Stacked on mobile,
   a horizontal row on tablet+.
   Sprint 19.2 correction -- root cause of the black title: this is a
   <button>, and the browser's default button color (not the page's
   inherited white) was winning for any descendant that didn't declare its
   own color -- .recipe-feature-title never did. Every other interactive
   text element in this codebase sets its own color explicitly (see
   .btn-quiet, .community-tab, .portion-stepper button); .recipe-feature
   broke that pattern. Fixed at the source (color set on the button itself,
   so the fix covers any future child too) and reinforced explicitly on the
   title. Also: no outer border (was reading as a bordered dashboard
   module), narrower image column, more internal air, a hover lift instead
   of a border-color change (nothing to change color on since there's no
   border) -- reads as one soft recommendation, not a component. */
.recipe-feature{ display:flex; flex-direction:column; width:100%; text-align:left; background:var(--surface-elevated);
  border:none; border-radius:var(--radius); overflow:hidden; cursor:pointer; margin-bottom:var(--s7);
  color:var(--text-primary); transition:transform 200ms var(--patient); }
.recipe-feature:hover{ transform:translateY(-2px); }
.recipe-feature:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.recipe-feature-thumb{ height:170px; background:linear-gradient(135deg, var(--surface-elevated), var(--canvas)); position:relative; }
.recipe-feature-thumb::after{ content:''; position:absolute; inset:0; background:radial-gradient(60% 60% at 30% 30%, rgba(255,255,255,0.05), transparent 70%); }
.recipe-feature-body{ padding:var(--s6); }
.recipe-feature-kicker{ font-size:12px; font-weight:600; letter-spacing:0.1em; text-transform:uppercase; color:var(--muted); margin:0 0 10px; }
.recipe-feature-title{ font-size:25px; line-height:1.1; margin:0 0 10px; color:var(--text-primary); }
.recipe-feature-meta{ font-size:13px; color:var(--text-secondary); margin:0; }
/* Dashboard Step 2 -- Today's Food reuses .recipe-feature verbatim, adding
   only the proof line (Recipes Overview's featured tile has no equivalent)
   and neutralizing the component's own margin-bottom, which was tuned for
   sitting atop a card grid, not for filling a dash-grid cell. */
.recipe-feature-proof{ font-size:14px; line-height:1.55; color:var(--text-body); margin:0 0 10px; }
.dash-grid .recipe-feature{ margin-bottom:0; }

/* Sprint 19.2 -- the hero and the intro below it now read as one group:
   the chef-note line (invisible until the Sprint 13 recognition moment)
   was reserving ~48px of dead space by itself between the two; its margin
   is cut down to almost nothing while its min-height stays untouched, so
   the recognition animation still can't shift layout when it fires. */
.recipe-detail-hero{ height:200px; border-radius:var(--radius); background:linear-gradient(135deg, var(--surface-elevated), var(--canvas)); position:relative; margin-bottom:var(--s3); }
.recipe-detail-hero::after{ content:''; position:absolute; inset:0; border-radius:var(--radius); background:radial-gradient(60% 60% at 30% 20%, rgba(255,255,255,0.05), transparent 70%); }

/* recipe detail intro -- title is the loudest thing on the page after the
   hero; description gets a readable measure and line-height; metadata
   stays small and quiet, clearly the last thing you read before acting. */
.recipe-intro{ margin-bottom:var(--s5); }
.recipe-title{ font-size:30px; line-height:1.05; margin:2px 0 10px; }
.recipe-desc{ color:var(--text-body); font-size:15px; line-height:1.65; max-width:54ch; margin:0 0 8px; }
.recipe-meta{ color:var(--muted); font-size:12.5px; margin:0; }

.portion-stepper{ display:flex; align-items:center; gap:14px; margin:var(--s5) 0; }
.portion-stepper button{ width:34px; height:34px; border-radius:50%; border:1px solid var(--hairline); background:var(--surface); color:var(--text-primary); cursor:pointer; font-size:16px; }

/* the PL8 mention is a quiet aside, not a callout box -- a left rule marks
   it as "supporting information," never competing with the recipe itself. */
.recipe-pl8-aside{ font-size:13px; line-height:1.55; color:var(--muted); padding:2px 0 2px 14px; border-left:2px solid var(--border-premium); margin:var(--s5) 0; }
.recipe-pl8-aside .btn-quiet{ font-size:13px; padding:0; }

.recipe-prep{ margin-top:var(--s7); padding-top:var(--s6); border-top:1px solid var(--hairline); }
.recipe-section-title{ font-size:13px; letter-spacing:0.1em; text-transform:uppercase; color:var(--muted); margin:0 0 12px; }
.recipe-section-title:not(:first-child){ margin-top:var(--s6); }
.ingredient-list{ list-style:none; margin:0; padding:0; }
.ingredient-list li{ display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--hairline); font-size:14.5px; }
.step-list{ margin:0; padding:0 0 0 20px; }
.step-list li{ font-size:14.5px; line-height:1.7; margin-bottom:10px; color:var(--text-body); }

/* Sprint 19 -- Recipe Experience v1. Calm typography and short lists, not a
   dashboard: no bars, no percentages, no icons.
   Sprint 19.1 -- "Perfekt für" / "Passt zu dir" are grouped as a matching
   pair (2 columns once there's room); fit-labels dropped the uppercase
   transform -- "Passt zu dir, wenn du ..." is a clause, not a short label,
   and reads as shouting in full caps. The facts row is the one deliberate
   tile in this whole screen: a contained --surface panel for quick-glance
   data, deliberately different from the flowing list content around it. */
.recipe-fit{ margin:var(--s6) 0; padding-top:var(--s5); border-top:1px solid var(--hairline); }
.recipe-fit-lists{ display:flex; flex-direction:column; gap:var(--s5); margin-bottom:var(--s5); }
.recipe-fit-block{ margin:0; }
.recipe-fit-label{ font-size:13px; letter-spacing:0.02em; color:var(--muted); margin:0 0 10px; font-family:var(--font-c); text-transform:none; }
.recipe-fit-list{ list-style:none; margin:0; padding:0; }
.recipe-fit-list li{ position:relative; padding-left:16px; font-size:14.5px; line-height:1.6; color:var(--text-body); margin-bottom:6px; }
.recipe-fit-list li::before{ content:'–'; position:absolute; left:0; color:var(--muted); }
.recipe-facts{ display:flex; flex-direction:column; gap:12px; background:var(--surface); border-radius:var(--radius); padding:var(--s4) var(--s5); }
.recipe-fact{ display:flex; justify-content:space-between; align-items:baseline; }
.recipe-fact-label{ font-size:12.5px; color:var(--muted); }
.recipe-fact-value{ font-size:14px; font-weight:600; color:var(--text-primary); text-align:right; }
.recipe-stars{ letter-spacing:2px; font-weight:400; }
.recipe-star{ color:var(--hairline); font-size:14px; }
.recipe-star.filled{ color:var(--text-primary); }

@media (min-width:641px){
  .recipe-feature{ flex-direction:row; }
  .recipe-feature-thumb{ width:32%; height:auto; flex-shrink:0; }
  .recipe-feature-body{ padding:var(--s7); display:flex; flex-direction:column; justify-content:center; }
  .recipe-feature-title{ font-size:28px; }
  .recipe-fit-lists{ flex-direction:row; gap:var(--s6); }
  .recipe-fit-block{ flex:1; }
  .recipe-facts{ flex-direction:row; gap:0; }
  .recipe-fact{ flex:1; flex-direction:column; align-items:flex-start; gap:4px; border-right:1px solid var(--hairline); padding:0 var(--s5); }
  .recipe-fact:first-child{ padding-left:0; }
  .recipe-fact:last-child{ border-right:none; }
  .recipe-fact-value{ text-align:left; }
}

/* Sprint 20.23 -- Community's Hintergrund tab and its chapter-discovery
   styles (.community-tabs/.community-tab/.community-hintergrund/
   .community-intro-*/.community-chapters/.community-chapter-link) were
   removed entirely: Community is single-view again, the story now lives at
   its own primary nav destination. */
.compose-box{ background:var(--surface-2); border-radius:10px; padding:var(--s4); margin:0 0 var(--s6); }
.compose-box textarea{ width:100%; background:none; border:none; border-bottom:1px solid var(--hairline); border-radius:0; padding:4px 0 10px; color:var(--text-primary);
  font-family:var(--font-c); font-size:14.5px; min-height:42px; resize:vertical; }
.compose-box textarea:focus{ outline:2px solid var(--magenta); outline-offset:2px; border-bottom-color:var(--magenta); }
.feed-item{ padding:20px 0; border-bottom:1px solid var(--hairline); }
.feed-item .who{ font-weight:600; font-size:14.5px; }
.feed-item .when{ font-size:12px; color:var(--muted); margin-left:8px; }
.feed-item p{ font-size:14.5px; line-height:1.6; color:var(--text-body); margin-top:6px; }
.feed-item .ack{ margin-top:8px; font-size:12.5px; color:var(--muted); background:none; border:none; padding:0; font-family:inherit; display:block; text-align:left; }
.feed-item button.ack{ cursor:pointer; }
.feed-item .ack.acked{ color:var(--text-secondary); }

/* Sprint 16 -- Background Reader. A plain list, not a card grid; generous
   type for calm reading, not dense reference material. No thumbnails, no
   dates, no tags -- deliberately the opposite visual language of a blog.
   Sprint 18.2 -- the reading environment: the Reader is the one screen that
   trades the dark product canvas for a warm paper surface ("opening a book"
   inside CRVD.KITCHEN). Implemented by rescoping the shared tokens
   (--text-primary, --text-body, --muted, --hairline, --magenta) to their
   paper-context values on .app-content-reader -- every rule below, and every
   inline style in app.js that reads these same custom properties, re-themes
   automatically with no per-rule duplication. .app-content-reader is set by
   render() only when state.screen === 'background'. */
.app-content-reader .app-content-inner{
  background:var(--editorial-paper);
  --text-primary:var(--editorial-ink);
  --text-body:var(--editorial-ink-secondary);
  --muted:var(--editorial-ink-secondary);
  --hairline:var(--editorial-divider);
  --magenta:var(--editorial-ink);
  color:var(--editorial-ink);
  border-radius:16px;
  padding:48px 44px;
  max-width:640px;
}
/* Sprint 18.3 -- paper and ink, not luxury packaging: the kicker and every
   selected/hover state on the paper surface move from gold to the same two
   neutrals as everywhere else -- muted brown-grey at rest, full dark ink
   (plus weight, never a new hue) when active. */
.app-content-reader .kicker{ color:var(--editorial-ink-secondary); }
.background-list{ display:flex; flex-direction:column; }

/* ======================================================================
   Sprint 20.20/20.21 -- "B Final" storytelling overview (production).
   Applies ONLY to the overview (.background-overview). Chapter detail pages
   keep the plain .app-content-inner paper panel and are untouched.
   Three values stay behind flag classes set by screenBackground(), kept as
   dev-only levers (see BACKGROUND_FLAGS in app.js) -- not production options:
     .bg-hero-masthead     | .bg-hero-hero
     .bg-surface-paper (default, approved 20.21) | .bg-surface-warmwhite
     .bg-scale-wide        | .bg-scale-uniform
   ====================================================================== */
.app-content-reader .background-overview{
  max-width:none; border-radius:0; padding:0;
  /* --magenta is rescoped to editorial ink by the Reader; the overview uses
     real brand magenta as its accent, so restore it from the fixed alias. */
  --magenta:var(--magenta-brand);
  --text-primary:#1C191E;
  --text-body:#6E6A70;
  --muted:#B9B3BB;
  --hairline:rgba(28,25,30,0.10);
  color:#1C191E;
  margin:0;
  /* The overview owns its padding so the surface reaches every edge. The
     parent's padding is neutralised just below -- .app-content uses three
     different padding regimes across breakpoints (16px / 48px / zeroed on
     mobile), so cancelling it with negative margins would mean duplicating
     all three here and re-breaking every time one changes. */
  padding:var(--s5) max(16px, env(safe-area-inset-right)) var(--mobile-safe-bottom) max(16px, env(safe-area-inset-left));
}
/* Applies only while the overview is on screen; chapter detail pages keep
   .app-content's normal padding. Without :has() support the page still works
   -- the surface simply stops short of the edges. */
.app-content-reader:has(.background-overview){ padding-top:0; padding-bottom:0;
  padding-left:0 !important; padding-right:0 !important; }
@media (min-width:1024px){
  .app-content-reader .background-overview{ padding:var(--s6) var(--s7) var(--s8); }
}
.background-overview.bg-surface-warmwhite{ background:#FAF8F5; }
.background-overview.bg-surface-paper{ background:var(--editorial-paper); }
.bg-inner{ max-width:1000px; margin:0 auto; }

/* masthead -- deliberately below the quietest chapter tier: a header, not a
   headline. A feed has a small header and large content. */
.bg-mast{ padding:44px 0 0; margin-bottom:60px; }
.bg-mast-tag{ font-family:var(--font-c); font-size:11px; font-weight:700; letter-spacing:0.2em;
  text-transform:uppercase; color:var(--magenta); margin:0 0 16px; }
.bg-mast-title{ line-height:1.05; max-width:22ch; margin:0; color:var(--text-primary); }
.bg-hero-masthead .bg-mast-title{ font-size:clamp(21px,4.6vw,30px); }
.bg-hero-hero .bg-mast-title{ font-size:clamp(38px,9.5vw,72px); line-height:0.96; max-width:14ch; }
.bg-mast-sub{ font-family:var(--font-c); font-size:clamp(15px,1.8vw,17px); line-height:1.55;
  color:var(--text-body); margin:14px 0 0; max-width:46ch; }

/* act markers -- a drawn boundary, which spacing alone never achieved */
.bg-act{ border-top:1px solid rgba(226,25,181,0.42); padding-top:18px; margin:0 0 8px; }
.bg-act-num{ display:block; font-family:var(--font-c); font-size:11px; font-weight:700;
  letter-spacing:0.2em; text-transform:uppercase; color:var(--magenta); }
.bg-act-title{ font-size:clamp(19px,2.8vw,25px); margin:10px 0 0; color:var(--text-primary); }
.bg-act-sub{ font-family:var(--font-c); font-size:14.5px; line-height:1.5; color:#8A868E; margin:7px 0 0; }
.bg-actbreak{ height:132px; }

/* chapters */
.background-story-item{ display:block; width:100%; text-align:left; background:none; border:none; padding:0;
  color:var(--text-primary); cursor:pointer; }
.bg-ch-num{ display:block; font-family:var(--font-c); font-size:11.5px; font-weight:700;
  letter-spacing:0.2em; color:var(--muted); margin-bottom:12px; }
/* Sprint 20.23 -- orientation cue on chapter 01 only. Identical component to
   .bg-mast-tag/.bg-act-num (11px/700/0.2em/uppercase/magenta), not a new
   tier -- reused, not invented. */
.bg-ch-start{ display:block; font-family:var(--font-c); font-size:11px; font-weight:700;
  letter-spacing:0.2em; text-transform:uppercase; color:var(--magenta); margin-bottom:8px; }
.background-story-title{ line-height:0.95; margin:0; color:var(--text-primary);
  transition:color 120ms var(--patient); }
.background-story-desc{ font-family:var(--font-c); font-size:clamp(15px,1.7vw,17.5px); line-height:1.55;
  color:var(--text-body); max-width:44ch; margin:14px 0 0; }
.background-story-arrow{ display:inline-flex; align-items:center; justify-content:center; margin-top:22px;
  width:42px; height:42px; border-radius:50%; border:1.5px solid rgba(226,25,181,0.55);
  font-size:16px; color:var(--magenta); transition:background 140ms var(--patient), color 140ms var(--patient); }
.background-story-item:hover .background-story-title,
.background-story-item:active .background-story-title{ color:var(--magenta); }
.background-story-item:hover .background-story-arrow{ background:var(--magenta); border-color:var(--magenta); color:#fff; }
.background-story-item:focus-visible{ outline:2px solid var(--magenta); outline-offset:4px; }
/* magenta numerals mark the peaks, so the structure is legible by colour
   alone while scrolling; quiet chapters stay grey. */
.bg-scale-wide .bg-ch--open .bg-ch-num,
.bg-scale-wide .bg-ch--finale .bg-ch-num{ color:var(--magenta); font-size:13px; }

/* scale hierarchy (flagged) */
.bg-scale-wide .bg-ch--open   .background-story-title{ font-size:clamp(45px,13.5vw,100px); }
.bg-scale-wide .bg-ch--finale .background-story-title{ font-size:clamp(48px,15vw,112px); }
.bg-scale-wide .bg-ch--mid    .background-story-title{ font-size:clamp(26px,5.8vw,34px); }
.bg-scale-wide .bg-ch--std    .background-story-title{ font-size:clamp(32px,8vw,52px); }
.bg-scale-wide .bg-ch--open   { margin:64px 0 104px; }
.bg-scale-wide .bg-ch--finale { margin:96px 0 0; }
.bg-scale-wide .bg-ch--mid    { margin-bottom:46px; }
.bg-scale-wide .bg-ch--std    { margin-bottom:76px; }
.bg-scale-uniform .background-story-title{ font-size:clamp(32px,8vw,56px); }
.bg-scale-uniform .background-story-item{ margin-bottom:64px; }
.bg-scale-uniform .background-story-item:last-child{ margin-bottom:0; }

.bg-closing{ font-family:var(--font-c); font-size:14px; line-height:1.5; color:#8A868E;
  margin-top:64px; padding-top:22px; border-top:1px solid var(--hairline); }
.bg-closing::before{ content:'— '; color:var(--magenta); }
.background-body{ font-size:16.5px; line-height:1.9; color:var(--text-body); margin-bottom:36px; max-width:56ch; }
.background-next{ display:block; }
.background-next:hover{ font-weight:600; }
.background-closing{ margin-top:32px; font-size:12.5px; color:var(--muted); font-style:italic; }

@media (max-width:640px){
  /* Mobile: the paper surface takes the full content width instead of
     reading as "a small beige card floating inside black margins" -- app-
     content's own side padding is removed here and absorbed into the paper
     panel's own padding instead. */
  .app-content-reader{ padding-left:0 !important; padding-right:0 !important; }
  .app-content-reader .app-content-inner{ border-radius:0; padding:40px 22px; max-width:none; }
  /* Sprint 20.20 -- overview keeps its own comfortable side padding here
     rather than the 40px/22px chapter-panel padding above. */
  .app-content-reader .background-overview{
    padding:var(--s5) 22px var(--mobile-safe-bottom);
  }
  .bg-mast{ padding-top:36px; }
  .bg-actbreak{ height:104px; }
}

/* PACE (bodytype) */
.bt-grid{ display:grid; grid-template-columns:1fr; gap:14px; }
.bt-card{ background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius); padding:var(--s5); text-align:left; cursor:pointer; }
.bt-card:hover{ border-color:var(--border-strong); }
.bt-card.selected{ border-color:var(--magenta); background:var(--surface-premium); }
.bt-card h3{ font-size:19px; margin:6px 0 8px; }
.bt-card p{ font-size:14px; color:var(--text-body); line-height:1.55; }
.bt-detail-back{ background:none; border:none; color:var(--muted); font-size:13px; cursor:pointer; margin-bottom:var(--s4); padding:6px 0; }
.bt-trait{ margin-bottom:var(--s5); }
/* Sprint 18.2 -- trait labels are ordinary informational text (Mindset,
   Stärken, Herausforderungen), not an action -- moved off magenta onto the
   same quiet kicker treatment used everywhere else. */
.bt-trait h4{ font-size:13px; color:var(--muted); letter-spacing:0.14em; text-transform:uppercase; margin-bottom:6px; }
.bt-trait p{ font-size:14.5px; line-height:1.6; color:var(--text-body); }

/* premium */
.pl8-hero{ text-align:center; padding:var(--s6) 0; }
.pl8-hero h1{ font-size:clamp(28px,6vw,42px); margin:10px 0; }
.pl8-feature-list{ display:grid; grid-template-columns:1fr; gap:12px; margin:var(--s6) 0; text-align:left; }
.pl8-feature{ display:flex; gap:12px; align-items:flex-start; }
.pl8-feature svg{ flex-shrink:0; width:20px; height:20px; color:var(--magenta); margin-top:2px; }
.pl8-feature h4{ font-size:14.5px; margin-bottom:4px; }
.pl8-feature p{ font-size:13.5px; color:var(--text-body); line-height:1.5; }
.pl8-price-card{ background:var(--surface-premium); border:1px solid var(--border-premium); border-radius:var(--radius); padding:var(--s6); text-align:center; }

.ai-tabs{ display:flex; gap:8px; margin-bottom:var(--s5); border-bottom:1px solid var(--hairline); }
.ai-tabs button{ background:none; border:none; color:var(--muted); font-size:13.5px; font-weight:600; padding:10px 4px; margin-right:16px; cursor:pointer; border-bottom:2px solid transparent; }
.ai-tabs button.active{ color:var(--text-primary); border-bottom-color:var(--magenta); }
.ai-callout{ background:var(--surface-premium); border:1px solid var(--border-premium); border-radius:var(--radius); padding:var(--s5); margin-bottom:var(--s5); }
.ai-callout p{ font-size:13.5px; color:var(--text-body); line-height:1.55; }
.shopping-list{ list-style:none; margin:0; padding:0; }
.shopping-list li{ display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid var(--hairline); font-size:14px; }
.shopping-list input{ width:16px; height:16px; }
.mealplan-day{ display:flex; justify-content:space-between; padding:12px 0; border-bottom:1px solid var(--hairline); font-size:14px; }
.mealplan-day .d{ color:var(--text-secondary); width:80px; flex-shrink:0; }

/* account */
.account-section{ margin-bottom:var(--s7); }
.account-section h3{ font-size:12px; letter-spacing:0.12em; color:var(--muted); text-transform:uppercase; margin-bottom:14px; }
.account-row{ display:flex; justify-content:space-between; align-items:center; padding:12px 0; border-bottom:1px solid var(--hairline); font-size:14.5px; gap:var(--s4); }
.account-row .v{ color:var(--text-secondary); font-size:13.5px; }
/* This specific row's value is now a full situation sentence rather than a
   short type name (Legacy Personalization Decoupling, Phase 2) -- let it
   wrap and right-align instead of forcing a single line. A dedicated class
   keeps this scoped to that one row; every other .account-row (short
   values like "Mitgliedschaft") keeps the default single-line vertical
   centering untouched. */
.account-row-wrap{ align-items:flex-start; }
.account-row-wrap .v{ text-align:right; max-width:60%; line-height:1.45; }
.danger-zone{ margin-top:var(--s7); }
.danger-zone .btn-ghost{ border-color:var(--action-destructive); color:var(--action-destructive); }

/* sheets */
.sheet-backdrop{ position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:20; display:flex; align-items:flex-end; justify-content:center; }
.sheet{ background:var(--surface-elevated); border-top-left-radius:20px; border-top-right-radius:20px; padding:var(--s6) var(--s5) max(var(--s6), env(safe-area-inset-bottom));
  width:100%; max-width:520px; text-align:center; }
.sheet-handle{ width:36px; height:4px; border-radius:2px; background:var(--hairline); margin:0 auto var(--s5); }

@media (prefers-reduced-motion:reduce){
  .pub-reveal, .screen-anim, .presence-dot, .pub-hero-signal, .entry-signal-inner, .ceremony-signal-inner, .app-topbar-soft{ transition-duration:0.01ms !important; animation-duration:0.01ms !important; }
  .pub-section-inner.pub-reveal > *{ transition-duration:0.01ms !important; transition-delay:0ms !important; animation-duration:0.01ms !important; }
  /* Das Essen -- initDasEssenAperture() never runs under reduced motion
     (heroStackReady() gates on the same media query), so without this
     override the Held Stage's 230vh wrapper height would sit there as
     dead scroll space around two absolutely stacked layers, with the
     photograph permanently at opacity:0 and unreachable -- the "blank
     scene" failure mode called out in the brief. Collapse both back to
     normal document flow; no JS branch required. */
  /* Held stage UN-STACKED. initHeldStage() never runs under reduced
     motion (heroStackReady() gates on the same query), so without this
     the two layers would stay absolutely stacked -- Essen permanently at
     opacity:0 and unreachable -- inside 230vh of dead scroll. Return
     both scenes to ordinary document flow: wrapper and stage collapse to
     auto height, layers become static blocks, the photograph becomes a
     normal large editorial image, and the caption stops being an overlay
     and sits beneath it. Fully readable, nothing hidden, no dead range. */
  .pub-held-wrap{ height:auto; }
  .pub-held-stage{ position:static; height:auto; overflow:visible; }
  .pub-held-layer{ position:static; inset:auto; padding:clamp(96px,14vh,168px) max(24px, env(safe-area-inset-right)); }
  /* filter and scrim MUST both be cleared here: the animated reveal
     never runs under reduced motion, so the CSS start values (brightness
     0.72, full-opacity scrim) would otherwise leave the static
     photograph permanently dim and permanently obscured. */
  .pub-held-essen{ opacity:1;
    padding:clamp(40px,6vh,72px) max(24px, env(safe-area-inset-right)) clamp(96px,14vh,168px); }
  .pub-essen-scrim{ display:none; }
  .pub-essen-img{ position:static; width:100%; height:auto; max-width:1100px; aspect-ratio:16/10; border-radius:14px;
    filter:none; }
  .pub-essen-flag{ position:static; align-self:flex-start; margin-top:14px; }
  .pub-essen-caption{ position:static; padding:clamp(24px,4vh,36px) 0 0; background:none; text-align:center; }
  /* Menschen un-stacked (Held Stage Phase 2). Same reasoning as the
     Frage/Essen un-stack above: initHeldStage never runs here, so the
     two voices must not remain absolutely positioned/opacity:0
     (unreachable). Voices become ordinary stacked paragraphs, read top
     to bottom in the same order they'd appear in the animated
     sequence. */
  .pub-held-menschen{ position:static; inset:auto;
    padding:clamp(56px,9vh,96px) max(24px, env(safe-area-inset-right)) clamp(96px,14vh,168px);
    display:flex; flex-direction:column; gap:clamp(28px,4.5vh,44px); align-items:flex-start; }
  /* THE SIGNAL under reduced motion, corrected (Signal Material +
     Motion sprint): previously dropped entirely (display:none) --
     reasonable when the mark was flat color with no value except its
     reveal motion, but Founder's explicit spec for this sprint is
     "no hidden content": the Signal now carries a real, static-legible
     Satin material, so it is shown, un-stacked into the same static
     flow as the voices above it, at its settled scale/opacity, no
     sheen. --sheen-op's own var(--sheen-op,0) fallback already
     resolves to 0 whenever GSAP never runs (this media query's whole
     branch), so no explicit override is needed to suppress the sheen. */
  .pub-menschen-signal{ position:static; display:flex; justify-content:flex-start;
    margin-top:clamp(28px,4.5vh,44px); }
  .pub-menschen-signal-inner{ transform:none; animation:none; opacity:1; --so:1; }
  .pub-menschen-voice{ position:static; opacity:1; max-width:100%; text-align:left; transform:none; }
}

/* ---------- Tablet ---------- */
@media (min-width:641px){
  .app-content-inner{ max-width:600px; }
  /* .quicklinks' 3-column override was sized for the Dashboard's former
     3-item quicklink row (Rezepte/Community/PL8); with exactly 2 items
     since Dashboard Step 4, it left an empty third cell at tablet widths.
     .quicklinks has no other markup consumer in the file, so removing the
     override here is safe -- the base 2-column rule (already re-asserted
     at >=1024px) now applies unbroken from 0 up. */
  .recipe-grid{ grid-template-columns:1fr 1fr; }
  .bt-grid{ grid-template-columns:1fr 1fr; }
}

/* ---------- Desktop: sidebar navigation, wide grids, no bottom nav ---------- */
@media (min-width:1024px){
  .app-bottomnav{ display:none; }
  .pl8-fab{ bottom:28px; }
  .app-sidebar{ display:flex; flex-direction:column; width:232px; flex-shrink:0; border-right:1px solid var(--hairline);
    padding:var(--s6) var(--s4); gap:4px; position:sticky; top:calc(65px + var(--preview-banner-h)); align-self:flex-start; height:calc(100vh - 65px - var(--preview-banner-h)); }
  .app-sidebar button{ display:flex; align-items:center; gap:12px; background:none; border:none; color:var(--muted); font-size:14px;
    padding:12px 14px; border-radius:10px; cursor:pointer; text-align:left; }
  .app-sidebar button svg{ width:19px; height:19px; }
  .app-sidebar button.active{ background:var(--surface); color:var(--text-primary); }
  .app-sidebar button.active svg{ color:var(--magenta); }
  .app-content{ padding:var(--s6) var(--s7) var(--s8); }
  .app-content-inner{ max-width:1100px; }
  .dash-grid{ grid-template-columns:2fr 1fr; align-items:start; }
  .quicklinks{ grid-template-columns:1fr 1fr; }
  .recipe-grid{ grid-template-columns:1fr 1fr 1fr; }
  .bt-grid{ grid-template-columns:1fr 1fr; max-width:760px; }
  .account-content{ display:grid; grid-template-columns:1fr 1fr; gap:var(--s7); align-items:start; }
  .pl8-feature-list{ grid-template-columns:1fr 1fr; }
}
