/* Placeholder-fidelity chrome. All user-facing strings are DRAFT until the copy
   pass (rulebook A22 + Q3 gate); style here is utilitarian scaffolding, not design. */

/* Safe areas go through variables, never raw env() (interface session
   2026-08-12). The dev phone frame is a TRANSFORMED fixed box, and inside one
   env(safe-area-inset-*) resolves to 0 — so every bottom-padding judgement made
   in the lab was 34px optimistic against a real notched phone. The frame now
   substitutes the device's real insets, and the lab tells the truth. */
:root { --safe-b: env(safe-area-inset-bottom); --safe-t: env(safe-area-inset-top); }

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; background: #141b16;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #e8ede6;
  -webkit-tap-highlight-color: transparent; }
#game, #game canvas { position: fixed; inset: 0; }

#ui { position: fixed; inset: 0; pointer-events: none; }
#ui > * { pointer-events: auto; }

/* ─────────────────────────────────────────────────────────────────────────
   THE ACTION CHROME — SETTLED (Oleh 2026-08-15): direction b, "the world is
   the interface", painted parchment tokens. The losing skins (a "field
   station", c "one moment") and label materials (tag/halo/sign) are deleted;
   git holds them. The body carries `chrome-b mat-icon` permanently — the
   selectors below keep those names because their specificity depends on them.

   Standing law (rulebook 4.7 / spec §6): no meters, no numbers, no streak.
   A finished day is shown as SETTLED, never as a disabled slab — greying a
   button out reads as refusal, and this game never refuses anyone.
   ───────────────────────────────────────────────────────────────────────── */

.action-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
  padding: 10px 16px calc(14px + var(--safe-b));
  display: flex; gap: 10px; justify-content: center; align-items: stretch; }
.action { flex: 1; border: none; cursor: pointer; color: #171d15;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-size: 14.5px; font-weight: 600; text-align: left; letter-spacing: .005em;
  transition: opacity .22s ease, background .22s ease, transform .18s ease; }
.action .face { display: flex; flex-direction: column; line-height: 1.12; }
.action .sub { font-size: 10.5px; font-weight: 400; opacity: .66; margin-top: 1px;
  letter-spacing: .01em; }
.action .glyph { flex: none; width: 19px; height: 19px; opacity: .7; }
.action .glyph svg { width: 100%; height: 100%; fill: none;
  stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* The "problems?" affordance — the one place the app admits it is software.
   Sits opposite the status pill, quiet enough to disappear until wanted. */
.support-dot { position: fixed; top: calc(6px + var(--safe-t)); left: 10px; z-index: 11;
  width: 30px; height: 30px; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(20, 27, 22, .42); color: rgba(232, 237, 230, .62);
  font-size: 15px; line-height: 1; padding: 0; }

/* ── B · THE WORLD IS THE INTERFACE ────────────────────────────────────────
   No dock. The two daily actions ride world anchors — veg at the stump (the
   fibre store made diegetic), reveal on the road at the fog boundary — the
   same per-frame idiom the spring-bud button already ships. main.ts supplies
   the coordinates; the buttons are small enough to read as world labels. */
/* Redesign round 2 (2026-08-13, after Oleh confirmed b): the labels are FIELD
   LABELS — the naturalist's own material (the copy canon's voice made
   physical), pinned BESIDE their places, never over them. A breathing ground
   light on the spot does the pointing; the label only names. Off-screen —
   the NORMAL case once the frontier moves — the label becomes a small tab
   clipped to the frame edge; the edge is the direction, no arrow. */
body.chrome-b .action-bar { display: contents; }
body.chrome-b .action { position: absolute; flex: none; z-index: 10;
  transform: translate(-50%, -50%); white-space: nowrap;
  padding: 6px 11px; font-size: 12.5px; }
body.chrome-b .action .sub { display: none; }
body.chrome-b .action .glyph { width: 15px; height: 15px; }
body.chrome-b .action[data-state="done"],
body.chrome-b .action[data-state="asleep"] { opacity: .5; }
body.chrome-b .action:disabled { cursor: default; }
/* Fermented has no world anchor of its own until Area 2 is built (C4); its
   final form is an open discussion (teaser vs B6 arrival) — parked. */
body.chrome-b .action.ferm { position: fixed; left: 50%; top: auto;
  bottom: calc(12px + var(--safe-b)); transform: translateX(-50%); }

/* The ground light: soft ellipse breathing ON the action spot. Deliberately
   dim and greenish-neutral — warm cream luminance stays reserved for creature
   state (colour system §3), so this must never read as a creature's glow. */
.action-light { position: absolute; z-index: 9; display: none;
  width: 118px; height: 56px; transform: translate(-50%, -50%);
  pointer-events: none; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(232, 240, 212, .44), rgba(232, 240, 212, 0) 72%); }
body.chrome-b .action-light.on { display: block;
  animation: light-breathe 3.6s ease-in-out infinite; }
@keyframes light-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
/* The bud tab's invitation (Oleh 2026-08-14): the same ground-light grammar
   the other travel tabs carry, in the WARM hue of the available bud's own
   halo — the pocket glows with the thing it points to. Warm-on-warm needs
   more push than the green lights to register at all (Oleh: invisible at the
   shared intensity), so it runs bigger and brighter. */
/* Seated on the mini-stump (Oleh 2026-08-14, composition round): the veg
   token RESTS on its cut face — shallow contact shadow instead of the hover
   drop, and its ground light tightens to hug the pedestal so the stump face
   itself seems to glow. Reveal keeps the deeper shadow (it stands on road
   and fog, not on furniture); ferm inherits the seated look with its stump. */
body.mat-icon.chrome-b .action.veg.painted .tokimg,
body.mat-icon.chrome-b .action.ferm.painted .tokimg {
  filter: drop-shadow(0 1px 2px rgba(15, 22, 12, .3)); }
body.mat-icon.chrome-b .action.veg.painted[data-state="done"] .tokimg,
body.mat-icon.chrome-b .action.ferm.painted[data-state="done"] .tokimg {
  filter: saturate(.8) drop-shadow(0 1px 2px rgba(15, 22, 12, .26)); }
.action-light.veg { width: 86px; height: 50px; }
body.chrome-b .action-light.veg.on .inhale { width: 80px; height: 80px;
  margin: -40px 0 0 -40px; }

.action-light.bud { width: 150px; height: 72px; filter: blur(4px); background:
  radial-gradient(closest-side, rgba(255, 196, 88, .5) 28%,
    rgba(255, 186, 70, .72) 60%, rgba(255, 176, 56, 0) 96%); }

/* ── b materials · ICON — the action is a small pictured object at its spot
   (Oleh, 2026-08-13). Placeholder marks; the painted roll replaces them if
   this form wins. Reveal = eye at the point the road dives under the fog;
   done = the object rests, faded, wearing a small settled mark. */
body.mat-icon.chrome-b .action { width: 42px; height: 42px; padding: 0;
  border-radius: 50%; justify-content: center;
  background: rgba(242, 236, 215, .92); color: #33402a;
  border: 1px solid rgba(90, 96, 70, .5);
  box-shadow: 0 2px 7px rgba(15, 22, 12, .35); }
body.mat-icon.chrome-b .action .face { display: none; }
body.mat-icon.chrome-b .action .glyph { width: 22px; height: 22px; opacity: .95; }
body.mat-icon.chrome-b .action[data-state="done"],
body.mat-icon.chrome-b .action[data-state="asleep"] { opacity: 1;
  background: rgba(242, 236, 215, .55); color: rgba(51, 64, 42, .55); }
body.mat-icon.chrome-b .action[data-state="done"]::after { content: '✓';
  position: absolute; top: -4px; right: -4px; width: 16px; height: 16px;
  border-radius: 50%; background: #6f9552; color: #f6f3e4; font-size: 11px;
  line-height: 16px; text-align: center; font-weight: 700;
  box-shadow: 0 1px 3px rgba(15, 22, 12, .4); }
/* Edge tab, icon form (Oleh fix round, 2026-08-14): a small parchment POCKET
   emerging flush from the frame edge, with the FULL token seated in it at a
   smaller size — the earlier half-tucked clip cut the painted eye and read as
   a rendering bug, not a deliberate tuck. Higher specificity than both the
   global tab transform (l.~340) and .painted's bare form, so it wins without
   ordering games. The pocket is a signpost: its tap travels, never acts. */
body.mat-icon.chrome-b .action[data-offscreen],
body.mat-icon.chrome-b .action.painted[data-offscreen] { width: 56px; height: 40px;
  background: rgba(242, 236, 215, .92); border: 1px solid rgba(90, 96, 70, .5);
  box-shadow: 0 2px 7px rgba(15, 22, 12, .35); }
body.mat-icon.chrome-b .action[data-offscreen="up"] {
  border-radius: 0 0 20px 20px; border-top: none; transform: translate(-50%, 0); }
body.mat-icon.chrome-b .action[data-offscreen="down"] {
  border-radius: 20px 20px 0 0; border-bottom: none; transform: translate(-50%, -100%); }
body.mat-icon.chrome-b .action[data-offscreen] .glyph { width: 18px; height: 18px; }
body.mat-icon.chrome-b .action.painted[data-offscreen] .tokimg { width: 30px; height: 30px; }
/* Tap answer: the token dips — pressed, acknowledged — while the travel glide
   does the real responding. */
body.mat-icon.chrome-b .action[data-offscreen]:active .tokimg,
body.mat-icon.chrome-b .action[data-offscreen]:active .glyph { transform: scale(.88); }

/* Painted tokens (batch 3): the rolled PNG IS the whole button — the CSS disc,
   border and stroke glyph all retire for that mark. Done fades the token
   itself; the rolled settled-mark badge replaces the CSS ✓ when it lands. */
body.mat-icon.chrome-b .action .tokimg { display: none; }
body.mat-icon.chrome-b .action.painted { background: none; border: none; box-shadow: none; }
body.mat-icon.chrome-b .action.painted .tokimg { display: block; width: 42px; height: 42px;
  filter: drop-shadow(0 2px 4px rgba(15, 22, 12, .35)); }
body.mat-icon.chrome-b .action.painted .glyph { display: none; }
body.mat-icon.chrome-b .action.painted[data-state="done"] { background: none;
  opacity: .74; }
/* Asleep is SEMI-active, not spent (Oleh 2026-08-14): fuller presence than
   done plus a slow sleep-breath, so "resting until the bud wakes" never reads
   as "already used today". Its tap plays the walled answer. */
body.mat-icon.chrome-b .action.painted[data-state="asleep"] { background: none;
  opacity: .92; }
body.mat-icon.chrome-b .action.painted[data-state="asleep"] .tokimg {
  animation: asleep-breath 4.6s ease-in-out infinite; }
/* Strengthened 2026-08-15 (Oleh: "barely noticeable" — the 1.04 swell moved
   under 2px at token size, and a dead-still asleep token risks reading as
   "no more map / bugged"): a clear calm breath, swell + brighten together. */
@keyframes asleep-breath {
  0%, 100% { transform: scale(.97); opacity: .8; }
  50% { transform: scale(1.07); opacity: 1; } }
body.mat-icon.chrome-b .action.painted[data-state="done"] .tokimg { filter: saturate(.8)
  drop-shadow(0 2px 4px rgba(15, 22, 12, .3)); }
body.mat-icon.chrome-b .action .donemark { position: absolute; top: -5px; right: -5px;
  width: 18px; height: 18px; }
body.mat-icon.chrome-b .action.markimg::after { content: none; }

/* The bud button in token form (buttons #6/#7, 2026-08-14): main.ts owns the
   per-frame placement at the bud's base; this styles the material. Waiting
   shows the closed-bud painting at FULL presence — a different true state of
   the same object, never a dimmed control (its tap answers with the flower's
   refusal shake + one tooltip line). Ready breathes like every available
   token. The .draftface text form is the probe-gated fallback only. */
.bud-token { position: absolute; transform: translate(-50%, 0); z-index: 30;
  background: none; border: none; padding: 0; cursor: pointer; }
.bud-token:not([data-offscreen]):active { transform: translate(-50%, 0) scale(.9); }
.bud-token .tokimg { display: block; width: 42px; height: 42px;
  filter: drop-shadow(0 2px 4px rgba(15, 22, 12, .35)); }
/* Ready breath is JS-driven (main.ts) so the token scales in step with the
   flower's own pulse — one shared clock, never two free-running animations. */
/* Waiting: not a stale disabled control — the token stirs in its sleep every
   few seconds (a soft rock, then stillness), enough to catch the eye and
   invite the tap that teaches the wake rule (Oleh 2026-08-14). */
.bud-token[data-state="waiting"] .tokimg { animation: bud-stir 5.6s ease-in-out infinite;
  transform-origin: 50% 85%; }
@keyframes bud-stir {
  0%, 55%, 100% { transform: rotate(0deg) scale(1); }
  63% { transform: rotate(-4deg) scale(1.04); }
  71% { transform: rotate(3deg) scale(1.03); }
  79% { transform: rotate(-1.5deg) scale(1.01); }
  87% { transform: rotate(0deg) scale(1); }
}
.bud-token .draftface { display: block; padding: 8px 14px; border-radius: 14px;
  font: 600 13px system-ui; background: #cde8a6; color: #1c2415;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35); }
.bud-token[data-state="waiting"] .draftface { background: rgba(60, 72, 60, .85);
  color: #c7cfc2; }
/* Available-and-off-screen: the bud's tab form wears the REAL action-pocket
   classes ('bud-token action painted' + data-offscreen, set by main.ts), so
   its sticker is the other travel tabs' sticker by construction — no
   duplicated styling to drift. While the bud is available it is the ONLY tab
   on the frame (ui.budFocus hides the daily ones): a once-a-run moment must
   not compete with chores (Oleh 2026-08-14). */

/* Available = the token BREATHES (the world's aliveness verb), a slow scale
   with a soft halo swelling in step with the ground light (same 3.6s). Halo
   hue matches the action light's greenish-neutral — never the warm cream
   reserved for creature state (colour system §3). Done/asleep tokens hold
   still: stillness is the rest read. Animates the inner mark, not the button,
   so it never fights the tab-tuck and tilt transforms. */
body.mat-icon.chrome-b .action[data-state="ready"] .tokimg {
  animation: token-invite 3.6s ease-in-out infinite; }
body.mat-icon.chrome-b .action[data-state="ready"] .glyph {
  animation: glyph-invite 3.6s ease-in-out infinite; }
@keyframes token-invite {
  0%, 100% { transform: scale(1);
    filter: drop-shadow(0 2px 4px rgba(15, 22, 12, .35))
      drop-shadow(0 0 2px rgba(232, 240, 212, .12)); }
  50% { transform: scale(1.07);
    filter: drop-shadow(0 2px 4px rgba(15, 22, 12, .35))
      drop-shadow(0 0 9px rgba(232, 240, 212, .6)); } }
@keyframes glyph-invite {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); } }

/* ── Veg-ready: the in-breath (Oleh's pick, 2026-08-14, from the quiet/
   motes/inbreath round) — a soft green ring condenses INTO the spot and
   dies at its heart: the manger breathing in. Deliberate mirror of the
   reveal's outward radiate — give and receive read as opposite motions.
   Greenish-neutral only: warm cream is creatures-only (§3), amber is the
   reveal's mark. Real child div of the veg ground light (inherits anchor
   and ready-only visibility; animated pseudos don't paint in the frame). */
.action-light .inhale { display: none; }
.action-light.veg.on .inhale { display: block;
  position: absolute; left: 50%; top: 50%; width: 96px; height: 96px;
  margin: -48px 0 0 -48px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, rgba(214, 236, 178, 0) 46%,
    rgba(214, 236, 178, .7) 68%, rgba(206, 232, 168, 0) 96%);
  filter: blur(4px);
  animation: veg-inhale 3.6s ease-in infinite; }
@keyframes veg-inhale {
  0% { transform: scale(1.45); opacity: 0; }
  30% { opacity: .85; }
  78% { transform: scale(.62); opacity: .25; }
  100% { transform: scale(.5); opacity: 0; } }

/* Logging confirmation (Oleh's ask: the world must never feel bugged after a
   log): on chip confirm the token SETTLES — a glad dip as the manger takes
   the food, easing into the done rest; the settled mark pops in on the tail.
   Transform-only on the token (done state owns the opacity), and the class
   lands on freshly rebuilt done-face children, so nothing fights it. */
body.mat-icon.chrome-b .action.settle .tokimg,
body.mat-icon.chrome-b .action.settle .glyph {
  animation: veg-settle .9s cubic-bezier(.3, 0, .3, 1); }
body.mat-icon.chrome-b .action.settle::after,
body.mat-icon.chrome-b .action.settle .donemark {
  animation: mark-pop .45s ease-out .5s backwards; }
@keyframes veg-settle {
  0% { transform: scale(1.12); }
  38% { transform: scale(.9); }
  100% { transform: scale(1); } }
@keyframes mark-pop {
  0% { transform: scale(0); opacity: 0; }
  60% { transform: scale(1.25); }
  100% { transform: scale(1); opacity: 1; } }

/* Reveal-ready: golden mist radiating off the token's edges — the sun that
   burns fog, physically ready to clear it (Oleh's ask, 2026-08-13). COMP,
   colour system §3 collision on record: warm luminance is creatures-only, so
   this is amber-GOLD (not creature-cream) and reveal-only; baking it needs an
   explicit §3 carve-out. Own layer UNDER the button (the action-light
   pattern) — a negative-z pseudo inside the button proved unpaintable in this
   stacking arrangement, and an ANIMATED pseudo inside the transformed dev
   frame does not paint at all — so the two layers are real child divs. */
.action-mist { position: absolute; z-index: 9; width: 42px; height: 42px;
  transform: translate(-50%, -50%); pointer-events: none; display: none; }
body.mat-icon.chrome-b .action-mist.on { display: block; }
.action-mist .haze { position: absolute; inset: -20px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 190, 80, 0) 40%,
    rgba(255, 190, 80, .55) 62%, rgba(255, 180, 60, 0) 95%);
  filter: blur(6px);
  animation: mist-haze 3.6s ease-in-out infinite; }
.action-mist .ring { position: absolute; inset: -14px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 194, 84, 0) 52%,
    rgba(255, 194, 84, .8) 74%, rgba(255, 180, 62, 0) 100%);
  filter: blur(3px);
  animation: mist-radiate 3.6s ease-out infinite; }
@keyframes mist-haze {
  0%, 100% { opacity: .55; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.14); } }
@keyframes mist-radiate {
  0% { transform: scale(.8); opacity: 0; }
  22% { opacity: .95; }
  100% { transform: scale(1.75); opacity: 0; } }

/* Reveal choreography (Oleh, 2026-08-13): no fog chrome during the tear.
   EXIT — 240ms fade, complete inside the camera-glide overture (the veil's
   dissolve only ramps from t=0.35s), so the stage is clean before the fog
   moves. ENTRANCE — after the veil settles, the done token condenses in:
   slightly large and transparent, settling to rest, delayed a beat so the
   tomorrow-glow's closing breath isn't fought. Opacity-only on the shells
   (the invite animation owns the token's transform while ready). */
.action.fog-exit, .action-mist.fog-exit, .action-light.fog-exit {
  opacity: 0 !important; pointer-events: none;
  transition: opacity .24s ease-out !important; }
/* The never-tapped veg token asks for attention once in a while (Oleh
   2026-08-17): a small wiggle every ~8.5s, ready-state only, retired for good
   at the first sheet open. Animates the independent `rotate` property so it
   COMPOSES with token-invite (which owns transform/scale) instead of
   fighting it. */
body.mat-icon.chrome-b .action.veg.fidget[data-state="ready"] .tokimg {
  animation: token-invite 3.6s ease-in-out infinite,
    tok-fidget 8.5s ease-in-out 3.5s infinite; }
@keyframes tok-fidget {
  0%, 7%, 100% { rotate: 0deg; }
  1.2% { rotate: -8deg; }
  2.6% { rotate: 7deg; }
  4% { rotate: -5deg; }
  5.4% { rotate: 3deg; } }
/* The D1 materialize, loud (Oleh 2026-08-17: the quiet condense read as
   nothing happening): the token blurs in bright, overshoots, settles; the
   ground light flares wide and calms. `scale`/`filter` properties only —
   transform stays owned by the chrome placement and the invite breathe. */
.action.veg-arrive { animation: fog-chrome-enter .6s ease-out .1s backwards; }
.action.veg-arrive .tokimg {
  animation: token-invite 3.6s ease-in-out infinite,
    tok-materialize 1.5s cubic-bezier(.22, .8, .3, 1) .1s backwards; }
@keyframes tok-materialize {
  0% { opacity: 0; scale: .2; filter: blur(9px) brightness(1.7); }
  45% { opacity: 1; scale: 1.22; filter: blur(1.5px) brightness(1.4); }
  70% { scale: .93; filter: blur(0) brightness(1.18); }
  100% { scale: 1; filter: none; } }
.action-light.veg-arrive { display: block;
  animation: light-flare 1.9s ease-out .1s backwards; }
@keyframes light-flare {
  0% { opacity: 0; scale: .3; }
  40% { opacity: 1; scale: 1.9; }
  100% { opacity: .9; scale: 1; } }
.action.fog-enter { animation: fog-chrome-enter .55s ease-out .25s backwards; }
.action.fog-enter .tokimg, .action.fog-enter .glyph {
  animation: token-condense .55s ease-out .25s backwards; }
.action-mist.fog-enter, .action-light.fog-enter {
  animation: fog-chrome-enter .55s ease-out .4s backwards; }
/* No `to` frame: it animates to each element's OWN resting opacity (a done
   token rests at .74 — a hardcoded `to 1` would snap at class removal). */
@keyframes fog-chrome-enter {
  from { opacity: 0; } }
@keyframes token-condense {
  from { transform: scale(1.28); }
  to { transform: scale(1); } }

/* The tap's ritual (Oleh, 2026-08-13 — "you tapped, it really got pressed,
   starts working, does its thing"): press-down, spring back charged, a small
   working shiver, then the token spends itself. ~0.72s, matched by the map's
   REVEAL_PRELUDE_S so the dissolve enters only after the stage is clear.
   `forwards` holds the spent state until the choreography's entrance clears
   the class. The mist flares outward and dies with it. */
.action.ritual { pointer-events: none;
  animation: ritual-fade .72s ease-in forwards !important; }
.action.ritual .tokimg, .action.ritual .glyph {
  animation: token-ritual .72s cubic-bezier(.3, 0, .4, 1) forwards !important; }
@keyframes ritual-fade {
  0%, 80% { opacity: 1; }
  100% { opacity: 0; } }
@keyframes token-ritual {
  0% { transform: scale(1); }
  14% { transform: scale(.84); }
  34% { transform: scale(1.14); }
  52% { transform: scale(1.05) rotate(3deg); }
  70% { transform: scale(1.1) rotate(-2.5deg); }
  100% { transform: scale(.35) rotate(0deg); } }
.action-mist.flare { animation: mist-flare .72s ease-in forwards !important; }
.action-light.flare { animation: mist-flare .72s ease-in forwards !important; }
@keyframes mist-flare {
  0% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  55% { transform: translate(-50%, -50%) scale(1.4); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1.7); opacity: 0; } }

/* Tooltip primitive (placeholder fidelity) — tag material, self-dismissing.
   Born of the closed-eye ruling: a done action answers a tap with words
   instead of a countdown (timers rejected — a countdown is a number in
   costume, 7.3-adjacent, and quietly promises a notification, 7.2). */
/* Tooltip in the family material (chrome pass, 2026-08-15): the same paper
   and rim as the cards, a soft rise on arrival. */
.ui-tip { position: absolute; z-index: 30;
  transform: translate(-50%, calc(-100% + 4px));
  background: linear-gradient(#f6f1de, #ece3c6); color: #33402a;
  border: 1px solid rgba(90, 96, 70, .5);
  border-radius: 9px; padding: 6px 11px; font-size: 11.5px; font-weight: 600;
  box-shadow: 0 3px 9px rgba(15, 22, 12, .35), inset 0 0 0 2px rgba(163, 122, 74, .14);
  pointer-events: none; white-space: nowrap; opacity: 0;
  transition: opacity .25s ease, transform .25s ease; }
.ui-tip.show { opacity: 1; transform: translate(-50%, -100%); }

/* Edge-tab geometry, all materials: half-tucked into the frame (66% showing),
   riding the anchor's clamped x. Last so its transform wins the tilt rules. */
body.chrome-b .action[data-offscreen="up"] { transform: translate(-50%, -34%); }
body.chrome-b .action[data-offscreen="down"] { transform: translate(-50%, -66%); }

.status-pill { position: fixed; top: calc(8px + var(--safe-t)); right: 10px;
  font-size: 11px; font-weight: 600; color: #4a4230;
  background: rgba(242, 236, 215, .92); border: 1px solid rgba(90, 96, 70, .45);
  padding: 4px 10px; border-radius: 10px; display: none;
  box-shadow: 0 2px 6px rgba(15, 22, 12, .3); }
.status-pill.show { display: block; }

/* Modals sit ABOVE the action chrome: the dock is z-index 10, and a sheet that
   renders under it leaves the buttons floating over the chips. */
.sheet-scrim { position: fixed; inset: 0; background: rgba(10, 14, 10, .38); z-index: 20; }
/* The logging sheet as field-journal parchment (2026-08-14, chrome pass): the
   same paper the tokens are cut from — cream ground, warm painted rim, ink
   text — so tapping a parchment token opens parchment, not an app tray.
   Behavior untouched: one tap picks, scrim/Esc/back dismiss, <5s law holds. */
/* max-width + auto margins (2026-08-16): on a bare desktop window the sheet
   spanned the whole screen (Oleh's "became full-screen" report) — it now caps
   at phone width and centers; inside the phone frame nothing changes. */
.sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 21;
  max-width: 430px; margin: 0 auto;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 252, 240, .9), rgba(242, 236, 215, 0) 60%),
    linear-gradient(#f2ecd7, #e6dcbd);
  color: #33402a;
  border: 1px solid rgba(90, 96, 70, .5); border-bottom: none;
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -6px 24px rgba(15, 22, 12, .38), inset 0 0 0 4px rgba(163, 122, 74, .16);
  padding: 16px 14px calc(18px + var(--safe-b));
  animation: sheet-rise .22s ease-out; }
@keyframes sheet-rise { from { transform: translateY(26px); opacity: .35; }
  to { transform: none; opacity: 1; } }
.sheet h3 { margin: 2px 4px 12px; font-size: 15px; font-weight: 700; color: #4a4230; }
/* Onboarding info at the point of input (2026-08-16): the veg sheet is
   self-sufficient — what/how under the title, why + the practice link below
   the chips. */
.sheet-note { margin: -8px 4px 12px; font-size: 12.5px; line-height: 1.45; color: #6d6248; }
.sheet-why { margin: 12px 4px 0; font-size: 12.5px; line-height: 1.45; color: #6d6248; }
.sheet-demo { margin: 8px 4px 0; text-align: center; }
.sheet-demo a { color: #8a7c58; font-size: 13px; }

/* ============ The full-screen intro (v0.13, 2026-08-16) ============
   Not a popup over an unmet world: an opaque, gently animated experience of
   its own. Parchment ground, one composed visual per slide, progress dots. */
.intro { position: fixed; inset: 0; z-index: 40;
  /* Sun-covered mist (Oleh 2026-08-16): the world's own weather, not a
     design-system gradient — warm light from above, soft haze below. */
  background:
    radial-gradient(85% 55% at 50% -8%, rgba(255, 236, 178, .85), rgba(255, 236, 178, 0) 70%),
    radial-gradient(120% 80% at 50% 115%, rgba(252, 250, 244, .9), rgba(252, 250, 244, 0) 60%),
    linear-gradient(#f8f2e1 0%, #f1e9d0 55%, #ece2c4 100%);
  color: #33402a;
  display: flex; align-items: center; justify-content: center; overflow: hidden; }
/* Drifting mist bodies — the painted fog's own tufts, huge and soft. */
.intro::before, .intro::after { content: ''; position: absolute; z-index: 0;
  width: 150%; height: 46%; pointer-events: none; opacity: .5;
  background: url('/art/v18/fog/tuft-02.png') no-repeat center / contain;
  filter: blur(7px); }
.intro::before { bottom: -10%; left: -30%; animation: mist-drift-a 26s ease-in-out infinite; }
.intro::after { top: 4%; right: -40%; width: 120%; height: 36%; opacity: .32;
  background-image: url('/art/v18/fog/tuft-04.png');
  animation: mist-drift-b 34s ease-in-out infinite; }
@keyframes mist-drift-a { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(6%); } }
@keyframes mist-drift-b { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-7%); } }
.intro.intro-out { animation: intro-fade-out .6s ease-in forwards; }
@keyframes intro-fade-out { to { opacity: 0; } }
/* Composition grid (Oleh 2026-08-17: "elements could be located and sized
   better relative to the screen edges and to each other"): the slide is a
   FULL-HEIGHT column with a CONSTANT art band, so the title, body, button and
   dots land at the same place on every slide — before this, per-slide art
   heights (360/158/225/172) made the text block wander. The art band centers
   its scene; the button anchors to the bottom via the flexible gap; the body's
   line-count variance is absorbed by that gap, never by the art or the CTA. */
.intro-slide { width: min(84%, 340px); height: 100%; box-sizing: border-box;
  padding: calc(46px + var(--safe-t, 0px)) 0 calc(26px + var(--safe-b, 0px));
  display: flex; flex-direction: column; align-items: center;
  text-align: center; position: relative; z-index: 1;
  animation: intro-slide-in .5s ease-out; }
@keyframes intro-slide-in {
  from { transform: translateY(16px); opacity: 0; }
  to { transform: none; opacity: 1; } }
.intro-slide h2 { flex: none; margin: 10px 0 0; font-size: 21px; color: #4a4230;
  text-wrap: balance; }
.intro-slide p { flex: none; align-self: stretch; margin: 12px 0 0;
  font-size: 14.5px; line-height: 1.55; color: #5a5138; text-align: left; }
.intro-slide .intro-next { flex: none; margin-top: auto; width: 100%; padding: 13px;
  border: none; border-radius: 14px;
  background: #6d8f56; color: #fdfbf2; font-size: 16px; font-weight: 700;
  box-shadow: 0 2px 10px rgba(38, 52, 30, .25); cursor: pointer; }
.intro-dots { flex: none; margin-top: 14px; display: flex; gap: 7px; justify-content: center; }
.intro-dots span { width: 7px; height: 7px; border-radius: 50%; background: #c9bb92; }
.intro-dots span.on { background: #6d8f56; }
/* The art band: constant across slides; shrinks first (before text or CTA)
   on short screens. Scenes with engine-tied pixel geometry (the feed scene)
   keep their fixed size and simply center in the band. */
.intro-art { flex: 0 1 350px; min-height: 235px; align-self: stretch;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px; }
/* Back to the previous slide (Oleh 2026-08-16): quiet chevron, top-left. */
.intro-back { position: fixed; top: calc(26px + var(--safe-t, 0px)); left: 14px;
  width: 40px; height: 40px; border: none; border-radius: 50%;
  background: rgba(163, 122, 74, .14); color: #6d6248;
  font-size: 24px; line-height: 1; cursor: pointer; }
@keyframes intro-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
/* Slide 1 — the rolled gut-world painting (the FIGURE final, Oleh's ruling
   2026-08-17). The melt is TWO layers: the feather is BAKED into the asset's
   alpha (high-core ellipse at 50%,38% — the head stays whole, the edges reach
   true zero before the bounds; a CSS mask alone left the corners whispering a
   square), and a halo of the painting's own edge tone (sampled:
   rgb(230,211,170)) breathes its atmosphere outward into the sun-mist. */
.intro-gutworld { height: 100%; display: flex; align-items: center; justify-content: center; }
/* The asset is the ORIGINAL roll centered on a larger canvas with mist-tone
   margins and the feather baked into the alpha (per-side ramps + high-core
   global feather), reaching true zero well inside every bound. The figure is
   the central 74%, so the box reads smaller than it measures. Scales with
   the art band (square, height-driven). */
.intro-gutworld img { display: block; height: 122%; max-height: 430px;
  aspect-ratio: 1; width: auto; }
/* Slide 2 — the mirror, unframed (Oleh: no buttons, no peepholes): pressed
   veg chips on the left, and on the right a soft-edged glade that slowly
   covers with Mossling's white star flowers, then breathes clear and grows
   again. The glade is a real map crop under a feathered mask — it melts into
   the mist instead of sitting in a frame. */
.intro-mirrorrow { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 16px; }
.intro-chiprow { display: flex; gap: 14px; }
.intro-chiprow img { width: 76px; height: 76px; object-fit: cover; border-radius: 18px;
  border: 2px solid rgba(255, 252, 240, .9); box-shadow: 0 3px 10px rgba(38, 52, 30, .2); }
.intro-chiprow .p1 { animation: chip-press 7.5s ease-in-out .8s infinite; }
.intro-chiprow .p2 { animation: chip-press 7.5s ease-in-out 3.2s infinite; }
.intro-chiprow .p3 { animation: chip-press 7.5s ease-in-out 5.6s infinite; }
@keyframes chip-press {
  0%, 6%, 100% { transform: scale(1); box-shadow: 0 3px 10px rgba(38, 52, 30, .2); }
  3% { transform: scale(.9); box-shadow: 0 1px 4px rgba(38, 52, 30, .35); } }
.intro-glade { position: relative; width: 300px; height: 170px;
  background: url('/art/v18/master.png'); background-size: 743px auto;
  /* The green-heart window (scored programmatically), widened to 380 master
     px for the full-width oval (composition pass 2026-08-17) — the heart
     sits near the master's west edge, which caps the zoom-out. */
  background-position: 0px -1584px;
  -webkit-mask-image: radial-gradient(58% 55% at 50% 50%, #000 55%, transparent 88%);
  mask-image: radial-gradient(58% 55% at 50% 50%, #000 55%, transparent 88%); }
.intro-glade .st { position: absolute; width: 36px; opacity: 0; }
.intro-glade .e1 { left: 24%; top: 30%; animation: star-early 13s ease-in-out infinite; }
.intro-glade .e2 { left: 58%; top: 52%; animation: star-early 13s ease-in-out .3s infinite; }
.intro-glade .m1 { left: 40%; top: 66%; animation: star-mid 13s ease-in-out infinite; }
.intro-glade .m2 { left: 66%; top: 24%; animation: star-mid 13s ease-in-out .4s infinite; }
.intro-glade .l1 { left: 16%; top: 56%; animation: star-late 13s ease-in-out infinite; }
.intro-glade .l2 { left: 46%; top: 14%; animation: star-late 13s ease-in-out .5s infinite; }
@keyframes star-early { 0%, 10% { opacity: 0; transform: scale(.5); }
  18%, 88% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
@keyframes star-mid { 0%, 34% { opacity: 0; transform: scale(.5); }
  42%, 88% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
@keyframes star-late { 0%, 58% { opacity: 0; transform: scale(.5); }
  66%, 88% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
/* Slide 3 — the REAL feeding run (Oleh 2026-08-17): a feather-masked window
   on the actual stump→pool stretch of the master (crop 100,130 560×420 at
   k≈.464 — a whisker over the game's own screen scale), the real painted
   token seated on its real stump, and the game's own FeedConnective flying
   the caravan on a transparent Pixi stage (introFeedScene.ts). Same melting-
   into-mist idiom as the glade and the fogscape — no frame, no peephole. */
.intro-feedscene { position: relative; width: 340px; height: 235px;
  /* The feather must COMPLETE inside the box (outer stop × radius < half-size)
     — an oval whose fade crosses the element edge gets chopped flat at the
     sides (Oleh's callout, 2026-08-17). */
  -webkit-mask-image: radial-gradient(50% 50% at 50% 50%, #000 50%, transparent 90%);
  mask-image: radial-gradient(50% 50% at 50% 50%, #000 50%, transparent 90%); }
.intro-feedscene .scape { position: absolute; inset: 0; z-index: 0;
  /* The DORMANT stump→pool window (baked master + dry-bed overlay) — the pool
     a D1 player really sees; the awake blue was a B4 spoiler. */
  background: url('/art/v18/intro/feedscene-dormant.png');
  background-size: 100% 100%; }
.intro-feedscene .feedstage { position: absolute; inset: 0; z-index: 1; }
/* The token rides its seat exactly as in play — the button over the painted
   cut face — and dips when the loop presses it (the in-game tap answer). */
.intro-feedscene .tok { position: absolute; left: 111px; top: 71px; z-index: 2;
  width: 62px; transform: translate(-50%, -50%);
  filter: drop-shadow(0 3px 8px rgba(38, 52, 30, .3)); }
.intro-feedscene .tok.press { animation: intro-tok-press .7s ease-in-out; }
@keyframes intro-tok-press {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  30% { transform: translate(-50%, -50%) scale(.88); } }
/* Slide 4 — the eye over the fogscape, one composition (no split, no
   peephole): a wide soft-masked stretch of the real map, its lower half
   asleep under breathing mist, the eye resting on it. */
.intro-fogscape { position: relative; width: 324px; height: 200px; }
.intro-fogscape .scape { position: absolute; inset: 0;
  /* The DORMANT stretch (baked master + dry-bed overlay — the awake stream
     was a B4 spoiler); same completed-feather rule as the feed scene. */
  background: url('/art/v18/intro/fogscape-dormant.png');
  background-size: 100% 100%;
  -webkit-mask-image: radial-gradient(50% 50% at 50% 50%, #000 50%, transparent 90%);
  mask-image: radial-gradient(50% 50% at 50% 50%, #000 50%, transparent 90%);
  overflow: hidden; }
.intro-fogscape .veil { position: absolute; inset: 0; background:
  linear-gradient(to top, rgba(252, 250, 244, .98) 12%, rgba(252, 250, 244, .8) 32%, rgba(252, 250, 244, 0) 62%);
  animation: veil-breathe 5.5s ease-in-out infinite; }
@keyframes veil-breathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(16px); } }
.intro-fogscape .eye { position: absolute; left: 50%; top: 58%; width: 62px;
  transform: translateX(-50%);
  filter: drop-shadow(0 0 16px rgba(255, 233, 160, .85));
  animation: intro-float 5s ease-in-out infinite; }
/* Slides 3/4 — the verbs: the painted token in a breathing halo. */
.intro-halo { position: relative; width: 120px; height: 120px;
  display: flex; align-items: center; justify-content: center; }
.intro-halo::before { content: ''; position: absolute; inset: -14px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 233, 160, .55), rgba(255, 233, 160, 0) 70%);
  animation: intro-breathe 3.4s ease-in-out infinite; }
@keyframes intro-breathe { 0%, 100% { transform: scale(.92); opacity: .7; }
  50% { transform: scale(1.08); opacity: 1; } }
.intro-halo img { width: 96px; position: relative; }
.chips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; }
/* Photoreal example image + label (rulebook 3.5). The image decorates the chip;
   the label is ALWAYS present under it, never replaced by it — hue is never the
   sole carrier of a read (art §6 colourblind guard), and a missing image must
   still leave a usable chip. Framed like a specimen plate: thin warm rim
   around the photo, ink label beneath on the paper. */
.chip { display: flex; flex-direction: column; align-items: stretch; gap: 7px;
  background: rgba(255, 253, 244, .55); border: 1px solid rgba(122, 101, 62, .35);
  border-radius: 14px; padding: 6px 6px 8px;
  color: #3d3524; font-size: 11.5px; line-height: 1.15; font-weight: 600; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.chip:active { background: rgba(122, 101, 62, .18); }
.chip .thumb { display: block; position: relative; width: 100%; aspect-ratio: 1;
  border-radius: 10px; overflow: hidden;
  border: 1px solid rgba(122, 101, 62, .4);
  box-shadow: inset 0 0 6px rgba(80, 62, 34, .25); }
.chip .thumb img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; }
.chip .chip-label { display: block; text-align: center; }

.card-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 30;
  display: flex; align-items: center; justify-content: center; padding: 24px; }
/* Story/beat cards sit in the lower third with a soft scrim, so the world moment
   they narrate stays visible above them (Oleh review 2026-08-01). */
.card-scrim--lower { align-items: flex-end; padding-bottom: 96px;
  background: linear-gradient(to bottom, rgba(0,0,0,.12), rgba(0,0,0,.55)); }
/* Cards share the sheet's field-journal parchment (2026-08-14, chrome pass):
   story beats, welcome, naming — one paper, ink text, moss-green confirm.
   The DRAFT ribbon stays until the copy pass strips it. */
.card { max-width: 320px; color: #33402a;
  background:
    radial-gradient(130% 100% at 50% 0%, rgba(255, 252, 240, .9), rgba(242, 236, 215, 0) 62%),
    linear-gradient(#f2ecd7, #e6dcbd);
  border: 1px solid rgba(90, 96, 70, .5);
  border-radius: 18px;
  box-shadow: 0 8px 26px rgba(15, 22, 12, .4), inset 0 0 0 4px rgba(163, 122, 74, .16);
  padding: 20px 20px 18px;
  animation: card-in .22s ease-out; }
@keyframes card-in { from { transform: translateY(14px); opacity: .35; }
  to { transform: none; opacity: 1; } }
.card h3 { margin: 0 0 8px; font-size: 16px; color: #4a4230; }
.card p { margin: 0 0 16px; font-size: 14px; line-height: 1.5; color: #5c5540; }
.card button { width: 100%; padding: 12px; border-radius: 12px;
  border: 1px solid rgba(60, 74, 40, .4);
  background: #66824c; color: #f7f3e4; font-size: 15px; font-weight: 600; cursor: pointer; }
.card button:active { background: #57713f; }
.card input { width: 100%; padding: 10px; border-radius: 10px; font-size: 15px;
  margin-bottom: 12px; border: 1px solid rgba(122, 101, 62, .45);
  background: rgba(255, 253, 244, .7); color: #33402a; }
/* Naming: the creature stands on the card, centred above its name field —
   the player sees exactly who they are naming (Oleh 2026-08-15). */
.card .portrait { display: block; height: 88px; margin: 2px auto 10px;
  filter: drop-shadow(0 3px 6px rgba(15, 22, 12, .25)); }

.gate { position: fixed; inset: 0; display: flex; align-items: center;
  justify-content: center; padding: 32px; text-align: center; background: #141b16; }
/* The gate message rides the family parchment — even the locked door is part
   of the same world (chrome pass, 2026-08-14). */
.gate p { margin: 0; max-width: 300px; color: #33402a; line-height: 1.6;
  background:
    radial-gradient(130% 100% at 50% 0%, rgba(255, 252, 240, .9), rgba(242, 236, 215, 0) 62%),
    linear-gradient(#f2ecd7, #e6dcbd);
  border: 1px solid rgba(90, 96, 70, .5); border-radius: 18px;
  box-shadow: 0 8px 26px rgba(15, 22, 12, .4), inset 0 0 0 4px rgba(163, 122, 74, .16);
  padding: 22px 20px; }

/* The D1 closing banner (2026-08-16): transient, pointer-through, the
   mist-catcher's material — the non-modal close under live judgement. */
.day-toast { position: fixed; left: 0; right: 0; bottom: 120px; z-index: 25;
  display: flex; justify-content: center; pointer-events: none;
  opacity: 0; transform: translateY(8px); transition: all .7s ease; }
.day-toast.show { opacity: 1; transform: none; }
.day-toast > div { max-width: 300px; text-align: center;
  background: rgba(20, 27, 22, .85); color: #efe9d6;
  padding: 11px 18px; border-radius: 14px; font-size: 13.5px; line-height: 1.45; }

/* DEV ONLY (?dev=1 or ?frame=1 adds body.dev-phone): schematic phone frame.
   390×844 logical px = iPhone 6.1" class, the dominant device of the target
   cohort (US health-curious women 25–35; assumption logged in recipes). The frame
   centers in the window while the state-lab panel stays pinned left;
   --dev-phone-scale (set from JS) shrinks the whole frame when the window is
   smaller than the device. The transform also makes position:fixed descendants
   (action bar, sheets, scrims) anchor to the FRAME, so the full UI clips to the
   phone like on a real device. */
/* …and because that transform zeroes env(safe-area-inset-*), the frame supplies
   the device's real insets by hand — otherwise the lab shows the action bar 34px
   lower than a real phone ever would. */
body.dev-phone { --safe-b: 34px; --safe-t: 47px; }
body.dev-phone #game, body.dev-phone #ui {
  position: fixed; inset: auto; left: 50%; top: 50%;
  width: 390px; height: 844px;
  transform: translate(-50%, -50%) scale(var(--dev-phone-scale, 1));
  border-radius: 28px; overflow: hidden;
}
body.dev-phone #game { box-shadow: 0 0 0 12px #2a3132, 0 0 0 14px #454f50,
  0 30px 80px rgba(0, 0, 0, .55); }
body.dev-phone #game canvas { position: absolute; inset: 0; }
body.dev-phone #ui::before { content: ''; position: absolute; top: 10px; left: 50%;
  transform: translateX(-50%); width: 120px; height: 22px; border-radius: 12px;
  background: #10140f; opacity: .85; pointer-events: none; z-index: 5; }
