/* ==========================================================================
   THE CHORES TAB, IN CODE — /chores/
   One recreation of the wall's Chores tab, used by all three animated sections
   on this page. Each section supplies only a camera track; the tab itself is
   authored once, here.

   The camera, the bezel and the screen are NOT here — they are the shared
   engine in wall-demo.css, which this file requires and which must be linked
   before it. What follows is only what is particular to Chores.

   Values follow MemberChoresWidget/index.jsx — the 120-viewBox ring at r=48
   with a 9px round-capped stroke, the 24px check that fills with the member's
   colour, the amber coin pill, strike-and-fade on a done row — and
   GoalsRewardsWidgets for the shop beneath it.
   ========================================================================== */

/* The wide section's cap belongs on the frame, not the screen inside it. */
.ct-frame:has(.ct-stage[data-track="rewards"]) {
  max-width: 784px;
  margin-inline: auto;
}

/* ── the tab ────────────────────────────────────────────────────────────── */
.ct {
  width: 1284px;
  padding: 20px;
  background: #fdf9f3;
  font-family: inherit;
}

.ct-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

.ct-card {
  --m: #5b8fd4;
  position: relative;
  height: 672px;
  padding: 18px 16px;
  background: #fff;
  border-radius: 20px;
  box-shadow: 0 2px 10px rgba(16, 18, 38, 0.06);
  overflow: hidden;
}

.ct-name { font-size: 15px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--m); }

.ct-ringwrap { position: relative; width: 150px; height: 150px; margin: 14px auto 0; }
.ct-ringwrap svg { width: 100%; height: 100%; display: block; }
.ct-track { fill: none; stroke: #ece6dd; stroke-width: 9; }
.ct-arc {
  fill: none; stroke: var(--m); stroke-width: 9; stroke-linecap: round;
  transform: rotate(-90deg); transform-origin: 60px 60px;
  transition: stroke-dasharray 0.6s ease;
}
.ct-face {
  position: absolute; inset: 16px; border-radius: 999px; overflow: hidden;
  box-shadow: 0 0 0 3px #fff;
}
.ct-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ct-pct { position: absolute; right: -4px; bottom: 14px; font-size: 12px; font-weight: 900; color: var(--m); }

.ct-chips { display: flex; justify-content: center; gap: 8px; margin-top: 10px; }
.ct-chip {
  display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 11px;
  border-radius: 999px; font-size: 13px; font-weight: 800; background: #f3efe8; color: #6c6257;
}
.ct-chip--coins {
  color: #b45309;
  background: linear-gradient(135deg, rgba(251,191,36,0.16), rgba(245,158,11,0.08));
  border: 1px solid rgba(245,158,11,0.18);
}

.ct-tabs { display: flex; gap: 4px; margin: 14px 0 12px; padding: 4px; background: #f7f3ec; border-radius: 999px; }
.ct-tab {
  flex: 1; text-align: center; padding: 6px 0; border-radius: 999px;
  font-size: 12.5px; font-weight: 700; color: #9a8b78;
}
.ct-tab.is-on { background: #fff; color: #4a4239; box-shadow: 0 1px 3px rgba(16,18,38,0.08); }

/* ── a chore row ────────────────────────────────────────────────────────── */
.ct-row {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 12px; margin-bottom: 9px;
  border: 2px solid #e9e3da; border-radius: 14px; background: #fff;
  transition: background 220ms ease, border-color 220ms ease, opacity 220ms ease;
}
.ct-check {
  width: 26px; height: 26px; border-radius: 999px; flex: none; display: grid; place-items: center;
  border: 2px solid #d5ccbf; background: transparent;
  transition: background 220ms ease, border-color 220ms ease, box-shadow 220ms ease;
}
.ct-check svg { width: 13px; height: 13px; color: #fff; opacity: 0; transition: opacity 180ms ease; }
.ct-icon { font-size: 18px; line-height: 1; flex: none; }
.ct-title {
  flex: 1; min-width: 0; font-size: 15px; font-weight: 600; color: #33302c;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-decoration-line: line-through; text-decoration-thickness: 1.5px;
  text-decoration-color: transparent;
  transition: text-decoration-color 220ms ease;
}
.ct-coin {
  display: inline-flex; align-items: center; gap: 3px; flex: none;
  padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 900;
  color: #b45309; background: linear-gradient(135deg, rgba(251,191,36,0.16), rgba(245,158,11,0.08));
  border: 1px solid rgba(245,158,11,0.18);
}

/* A completed row — the state the product settles into. */
.ct-row.is-done { background: #f6f3ee; border-color: #e4ded4; opacity: 0.6; }
.ct-row.is-done .ct-check { background: var(--m); border-color: var(--m); box-shadow: 0 0 7px color-mix(in srgb, var(--m) 40%, transparent); }
.ct-row.is-done .ct-check svg { opacity: 1; }
.ct-row.is-done .ct-title { text-decoration-color: currentColor; }


/* ── the hearts behind a child's meter ──────────────────────────────────── */
/* FloatingHeartsBackground.jsx, which the widget renders for child meters only
   (`isChildMeter && <FloatingHeartsBackground tint={memberColor} />`) — so
   Daniel's card has none, exactly as on the wall.

   Individual glyphs rather than a drifting sheet. Three sheet layers were the
   first attempt and they could not be made to work: small enough not to
   dominate, they only covered the foot of the card; stretched to sweep it,
   they arrived as a clump and left the card empty behind them. The app uses
   single hearts for the same reason — twelve of them, each on its own 92s rise
   with a negative delay a twelfth of a cycle apart, so one is always at every
   height and the card is never bare.

   The path is the app's own (HeartGlyph.jsx), painted as the member's colour
   through a mask so each child's hearts carry their tint. */
.ct-hearts { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }

.ct-hearts i {
  position: absolute;
  left: var(--l);
  bottom: 0;
  width: var(--s);
  height: var(--s);
  background: var(--m);
  opacity: var(--o);
  mask: var(--ct-heart) center / contain no-repeat;
  -webkit-mask: var(--ct-heart) center / contain no-repeat;
  animation: ct-heart-rise 92s linear infinite var(--d);
}

:root {
  --ct-heart: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3.68546 5.43796C8.61936 1.29159 11.8685 7.4309 12.0406 7.4309C12.2126 7.43091 15.4617 1.29159 20.3956 5.43796C26.8941 10.8991 13.5 21.8215 12.0406 21.8215C10.5811 21.8215 -2.81297 10.8991 3.68546 5.43796Z'/%3E%3C/svg%3E");
}

/* No fade at either end: the travel starts below the card and finishes above
   it, and .ct-hearts clips. A heart leaving the frame is simply out of sight,
   which is steadier than watching a dozen of them dissolve together. */
@keyframes ct-heart-rise {
  0%   { transform: translate3d(0, 90px, 0) rotate(var(--r)); }
  100% { transform: translate3d(var(--x), -740px, 0) rotate(calc(var(--r) + 10deg)); }
}

/* Everything the card draws sits above them. */
.ct-card > :not(.ct-hearts) { position: relative; z-index: 1; }

/* ── the reward shop ────────────────────────────────────────────────────── */
.ct-shop { margin-top: 16px; padding: 18px; background: #fff; border-radius: 20px; box-shadow: 0 2px 10px rgba(16,18,38,0.06); }
.ct-shop-head { font-size: 14px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: #c2762a; }

.ct-who { display: flex; gap: 10px; margin: 14px 0 16px; }
.ct-tile {
  display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px 6px 6px;
  border-radius: 999px; border: 2px solid transparent; background: #f7f3ec;
  transition: border-color 260ms ease, background 260ms ease;
}
.ct-tile img { width: 34px; height: 34px; border-radius: 999px; object-fit: cover; }
.ct-tile b { font-size: 14px; font-weight: 700; color: #4a4239; }
.ct-tile span {
  display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 999px;
  font-size: 12px; font-weight: 900; color: #b45309;
  background: linear-gradient(135deg, rgba(251,191,36,0.16), rgba(245,158,11,0.08));
}
.ct-tile.is-on { border-color: #e0a43c; background: #fff7ea; }

.ct-rewards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.ct-rewards[hidden] { display: none; }
.ct-reward {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 12px 12px 14px; border-radius: 14px;
  background: #f9f6f1; border: 1px solid #ece5db;
}
.ct-reward-icon { font-size: 19px; line-height: 1; flex: none; }
.ct-reward b { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; color: #33302c;
               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ct-reward-cost {
  display: inline-flex; align-items: center; gap: 3px; flex: none; padding: 2px 8px;
  border-radius: 999px; font-size: 12px; font-weight: 900; color: #b45309;
  background: linear-gradient(135deg, rgba(251,191,36,0.16), rgba(245,158,11,0.08));
}
.ct-btn {
  flex: none; padding: 5px 13px; border-radius: 999px; font-size: 12.5px; font-weight: 800;
}
.ct-btn--redeem  { color: #fff; background: #2f7d5e; }
.ct-btn--request { color: #fff; background: #c2762a; }

/* ==========================================================================
   CAMERA TRACKS
   Each section moves the same tab differently. --z is a multiple of the fit
   scale, --tx/--ty are pixels in the tab's own 1284-wide space, so a beat
   reads as "put this point at the top-left and zoom in by this much".

   Landmarks (tab space): Leo's meter starts at x=20, Daniel's at x=441,
   Ava's at x=862; each is 401 wide. The shop's top edge is y=708.
   ========================================================================== */

/* --- 1. the guided tour -------------------------------------------------- */
/* Geometry (tab space): the meter cards sit at x=20, 440 and 860 — children
   first, the parent on the right — each 404 wide and 672 tall, spanning
   y=20-692. The shop runs y=708-961. Visible height at a given zoom is 883/z,
   visible width 1284/z.

   z=1.25 is the tightest zoom that still shows a card top to bottom
   (883/1.25 = 706 against a card's 672). The pan ends at tx=-257, putting the
   tab's right edge on the frame's, so the last two cards are whole and no
   empty wall follows them.

   9s in total. The pan is the long move and everything else is brisk: the
   opening hold is under a second, Leo gets a beat rather than a pause before
   the shop steps on, and the way out does not begin until the last person has
   been shown. */
@keyframes ct-tour {
  0%, 6%    { --z: 1;    --tx: 0;    --ty: 0; }      /* the tab as it stands */
  16%       { --z: 1.25; --tx: -8;   --ty: -8;
              animation-timing-function: linear; }   /* in on Leo, card in full */
  39%       { --z: 1.25; --tx: -257; --ty: -8; }     /* one steady pan right */
  44%       { --z: 1.25; --tx: -257; --ty: -8; }     /* and a beat on Daniel */
  /* ty is 981 - 883/1.8: the frame's bottom edge lands exactly on the tab's,
     so the pan stops at the end of the shop instead of running on into empty
     wall below it. */
  56%, 80%  { --z: 1.8;  --tx: -28;  --ty: -490; }   /* down and left, into the shop */
  93%, 100% { --z: 1;    --tx: 0;    --ty: 0; }      /* back up and out */
}
.ct-stage[data-track="tour"] .ct-cam { animation: ct-tour 9.4s infinite; }

/* The shop's member tiles follow the camera: whoever is in frame is selected,
   and the parent's own rewards replace the children's when it reaches him. */
@keyframes ct-tile-leo    { 0%, 64.9% { --on: 1 } 65%, 100% { --on: 0 } }
@keyframes ct-tile-daniel { 0%, 64.9% { --on: 0 } 65%, 76.9% { --on: 1 } 77%, 100% { --on: 0 } }
@keyframes ct-tile-ava    { 0%, 76.9% { --on: 0 } 77%, 88.9% { --on: 1 } 89%, 100% { --on: 0 } }

/* --- 2. ticking ---------------------------------------------------------- */
/* Same framing discipline as the tour. It used to close to z=1.9, which shows
   465 of a card's 672px — the ring answering the tick was outside the frame,
   and a meter cropped in half is not a meter. z=1.25 is the tightest that
   holds a whole card, so the rows, the ring, the counter and the coins are all
   in shot when they move.

   Loops the way the tour does: no crossfade, just the camera arriving back at
   the values it started on. 0% and 100% are the same frame, so there is
   nothing to see at the seam. The fade this used to carry was covering for a
   cut that no longer happens. */
@keyframes ct-tick-cam {
  0%, 6%    { --z: 1;    --tx: 0;  --ty: 0; }
  11%, 84%  { --z: 1.25; --tx: -8; --ty: -8; }
  95%, 100% { --z: 1;    --tx: 0;  --ty: 0; }
}
.ct-stage[data-track="ticking"] .ct-cam { animation: ct-tick-cam 14.7s infinite; }

/* --- 3. the reward shop -------------------------------------------------- */
/* The shop runs the full width of the tab, so it cannot be zoomed sideways at
   all — any magnification pushes a column of rewards out of frame. z=1.55 is
   the compromise: tiles and two columns of rewards, which is where both taps
   happen.

   ty is 981 - 883/1.55, so the frame's bottom edge lands exactly on the tab's
   and no empty wall follows the shop. The wide intermediate step is gone: it
   sat 332px past the end of the tab, and stopping at native scale on the way
   to a zoom is the kind of return-to-neutral that makes a move feel like two.
   Straight in, straight out.

   8s: in, a beat to settle, redeem, request, the toast long enough to read,
   then out. */
@keyframes ct-reward-cam {
  0%, 4%    { --z: 1;    --tx: 0;   --ty: 0; }      /* the tab as it stands */
  12%, 62%  { --z: 1.55; --tx: -22; --ty: -411; }   /* down and in, in one move */
  71%, 100% { --z: 1;    --tx: 0;   --ty: 0; }      /* out, under the celebration */
}
.ct-stage[data-track="rewards"] .ct-cam { animation: ct-reward-cam 17s infinite; }


/* ── the redemption celebration ─────────────────────────────────────────── */
/* RedemptionCelebration.jsx, beat for beat. The card arrives showing its BACK,
   settles out of a blur, wobbles, turns edge-on, swaps face behind a flash,
   turns back to reveal the front, stamps, throws confetti from the centre, and
   wobbles again before it holds. The first version of this had no back face and
   no wobble, so the turn revealed nothing and the whole point of the card — that
   it flips over to show you what you won — was missing.

   Timings are the component's, compressed about 40%: its own run is ~8.5s
   (620ms in, 1.1s settle, 1.6s wobble, 0.38s turn, 500ms edge-on, 0.38s back,
   1.8s wobble, 1.5s hold) and this section is ten seconds long in total.
   The theme is its `screen_time` entry, which is the category of the reward
   being redeemed — its gradients, its border, its stamp colour, its ⚡.

   It also covers the camera's reset: the view pulls back to the opening frame
   while this is over the screen. */
.ct-celebrate {
  position: absolute;
  /* Overshoot the stage by 2px and carry NO radius of its own, so the stage's
     clip-path is the only arc in play. Matching the radius here instead left
     two rounded edges antialiased by different layers: where they disagreed by
     a fraction of a pixel the un-darkened wall showed through, and a dotted
     white arc appeared in each corner while this section played. */
  inset: -2px;
  z-index: 4;
  display: grid;
  place-items: center;
  opacity: 0;
  pointer-events: none;
  perspective: 1000px;
}
/* The card is stated in the component's own pixels — 248 x 330 — but the stage
   is fluid, and at phone width it is shorter than the card is tall. The stack
   scales the whole celebration with the stage so it never outgrows the frame,
   holding at 1 from the width the section is designed at (760px, --fit .592)
   and shrinking below it. Only this wrapper scales: the backdrop above it
   still covers the whole stage. */
.ct-cel-stack {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  transform: scale(min(1, calc(var(--fit, 0.592) * 1.69)));
}

/* The line the component writes above the card — "<name> unlocked a <noun>".
   It is absolutely placed rather than stacked, because the card must stay on
   the centre of the frame; in the app the gap below it is the member cue,
   which has already faded by the time the front face is up but still holds
   its room. */
.ct-cel-intro {
  position: absolute;
  left: 50%;
  /* 43px above the card. The app leaves 83, but that space is the member cue
     it has already faded out of; this frame is 523 tall against the app's 960,
     and at 83 the line ran off the top of the bezel. */
  bottom: calc(50% + 208px);
  margin: 0;
  font-size: 11px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.28em;
  color: rgba(255,255,255,0.45);
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -8px);
}
.ct-celebrate.is-on .ct-cel-intro { animation: ct-cel-intro 9.5s ease-out forwards; }
@keyframes ct-cel-intro {
  0%           { opacity: 0; transform: translate(-50%, -8px); }
  7.4%, 89.8%  { opacity: 1; transform: translate(-50%, 0); }
  100%         { opacity: 0; transform: translate(-50%, 0); }
}

/* The card and its bubbles share one box, as they share the perspective
   wrapper in the component. */
.ct-cel-deck { position: relative; width: 248px; height: 330px; }
.ct-cel-bubbles { position: absolute; inset: 0; pointer-events: none; }
.ct-cel-bub {
  position: absolute;
  bottom: 10px;
  left: 50%;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.32);
  opacity: 0;
}
/* rc-bubble. They start when the component starts them: after the second
   wobble, 6.38s in, which is 67.2% of the card's 9.5s. */
.ct-celebrate.is-on .ct-cel-bub {
  animation-name: ct-cel-bubble;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
@keyframes ct-cel-bubble {
  0%   { transform: translateY(0) scale(1); opacity: var(--bop); }
  80%  { opacity: var(--bop); }
  100% { transform: translateY(-100px) scale(0.2); opacity: 0; }
}
.ct-celebrate.is-on .ct-cel-bubbles { animation: ct-cel-bubbles-in 9.5s steps(1, end) forwards; }
@keyframes ct-cel-bubbles-in { 0%, 67.1% { opacity: 0 } 67.2%, 89.8% { opacity: 1 } 100% { opacity: 0 } }
.ct-celebrate.is-on { animation: ct-cel-back-drop 9.5s cubic-bezier(.22, 1, .36, 1) forwards; }
/* The backdrop takes the component's 1.4s to arrive, which is why the whole
   thing opens slowly rather than snapping on. */
@keyframes ct-cel-back-drop {
  0%     { opacity: 0; background: rgba(6,4,18,0);    backdrop-filter: blur(0px);  -webkit-backdrop-filter: blur(0px); }
  14.7%  { opacity: 1; background: rgba(6,4,18,0.72); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
  89.8%  { opacity: 1; background: rgba(6,4,18,0.72); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
  100%   { opacity: 0; background: rgba(6,4,18,0);    backdrop-filter: blur(0px);  -webkit-backdrop-filter: blur(0px); }
}

.ct-celebrate-card {
  /* 248 x 330 — the component's own box. This was 296x208 before, landscape,
     which is the wrong object entirely: the card is a ticket, and a ticket
     stands up. Radius, border weight and both shadows are its values too. */
  position: absolute;
  inset: 0;
  width: 248px;
  min-height: 330px;
  border-radius: 22px;
  transform-style: preserve-3d;
  opacity: 0;
}
.ct-celebrate.is-on .ct-celebrate-card { animation: ct-cel-card 9.5s linear forwards; }

/* The component's own clock, uncompressed. Every percentage below is one of
   its waits converted against a 9.5s run: 620ms before anything moves, a 1.1s
   settle out of blur, a 1.6s wobble, 380ms to turn edge-on, half a second held
   there while the face swaps behind a flash, 380ms back, then an 1.8s wobble
   and a long hold. Compressing this to 5s is what made it read as quick rather
   than cinematic — the pauses ARE the thing. */
@keyframes ct-cel-card {
  0%,
  6.5%   { opacity: 0; filter: blur(10px); transform: translateY(28px) scale(.72) rotate(0deg) rotateY(0deg); }
  18.1%  { opacity: 1; filter: blur(0px);  transform: translateY(0)    scale(1)   rotate(0deg) rotateY(0deg); }
  20.5%  { transform: rotate(2.5deg)  rotateY(0deg); }
  23.0%  { transform: rotate(-2deg)   rotateY(0deg); }
  25.4%  { transform: rotate(1.8deg)  rotateY(0deg); }
  27.8%  { transform: rotate(-1.5deg) rotateY(0deg); }
  30.2%  { transform: rotate(1deg)    rotateY(0deg); }
  32.6%  { transform: rotate(-.8deg)  rotateY(0deg); }
  34.9%  { transform: rotate(0deg)    rotateY(0deg); }
  /* Edge-on, and HELD there. The component turns to 90 (380ms), waits while
     the flash fires and the face is swapped behind it — 120ms then 380ms — and
     only then turns back. Without that hold the rotation ran straight through
     and the content changed while the card was visibly half-turned, which is
     the one moment it must not: the swap is meant to be hidden inside the
     edge. */
  38.9%  { transform: rotate(0deg)    rotateY(90deg); }   /* edge-on */
  44.2%  { transform: rotate(0deg)    rotateY(90deg); }   /* held while it swaps */
  48.2%  { transform: rotate(0deg)    rotateY(0deg); }    /* back, front up */
  51.0%  { transform: rotate(1.8deg)  rotateY(0deg); }
  53.7%  { transform: rotate(-1.5deg) rotateY(0deg); }
  56.4%  { transform: rotate(1.2deg)  rotateY(0deg); }
  59.1%  { transform: rotate(-1deg)   rotateY(0deg); }
  61.8%  { transform: rotate(.6deg)   rotateY(0deg); }
  64.5%  { transform: rotate(-.4deg)  rotateY(0deg); }
  67.2%  { opacity: 1; transform: rotate(0deg) rotateY(0deg) scale(1); }
  89.8%  { opacity: 1; transform: rotate(0deg) rotateY(0deg) scale(1); }
  100%   { opacity: 0; transform: rotate(0deg) rotateY(0deg) scale(.96); }
}

.ct-cel-face {
  position: absolute;
  inset: 0;
  border-radius: 22px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 28px;
  text-align: center;
}
.ct-cel-back {
  background: linear-gradient(145deg, #0f172a 0%, #0e7490 34%, #2563eb 64%, #020617 100%);
  border: 1.5px solid rgba(34, 211, 238, 0.55);
  box-shadow: 0 28px 72px rgba(0,0,0,0.6), 0 0 0 1px rgba(34,211,238,0.55),
              inset 0 1px 0 rgba(255,255,255,0.06);
}
.ct-cel-front {
  background: linear-gradient(160deg, #ecfeff 0%, #eff6ff 48%, #f8fafc 100%);
  border: 1.5px solid rgba(34, 211, 238, 0.55);
  box-shadow: 0 28px 72px rgba(0,0,0,0.55), 0 0 0 1px rgba(34,211,238,0.55),
              inset 0 1px 0 rgba(255,255,255,0.78);
  opacity: 0;
}
.ct-celebrate.is-on .ct-cel-back  { animation: ct-cel-hide 9.5s steps(1, end) forwards; }
.ct-celebrate.is-on .ct-cel-front { animation: ct-cel-show 9.5s steps(1, end) forwards, ct-cel-breathe 3.5s 4.58s ease-in-out infinite; }
@keyframes ct-cel-hide { 0%, 40.1% { opacity: 1 } 40.2%, 100% { opacity: 0 } }
@keyframes ct-cel-show { 0%, 40.1% { opacity: 0 } 40.2%, 100% { opacity: 1 } }
/* rc-breathe, which the component runs on the card once the front is up. */
@keyframes ct-cel-breathe { 0%, 100% { transform: rotate(0deg) scale(1); } 50% { transform: rotate(.8deg) scale(1.008); } }

/* The back's embroidery: a spinning glow behind a 28px repeat of the theme's
   motif, at the component's 0.62. */
/* The component's back-face SVG, verbatim: the 28px `circuit` repeat, the four
   ticket corner marks, and the compass rosette in the middle, all through its
   diagonal gradient at its 0.62. This was four CSS gradients standing in for
   it, which tiled dots and a diagonal rule — the right texture but not the
   right object; a ticket back has corners and a centre mark, and this card is
   face-down for the first 3.7 seconds of the run. */
.ct-cel-weave {
  position: absolute;
  inset: 0;
  opacity: 0.62;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20248%20330%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22l%22%20x1%3D%220%25%22%20y1%3D%220%25%22%20x2%3D%22100%25%22%20y2%3D%22100%25%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%2393c5fd%22%20stop-opacity%3D%220.95%22%2F%3E%3Cstop%20offset%3D%2252%25%22%20stop-color%3D%22%2367e8f9%22%20stop-opacity%3D%220.76%22%2F%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%2393c5fd%22%20stop-opacity%3D%220.9%22%2F%3E%3C%2FlinearGradient%3E%3Cpattern%20id%3D%22p%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2228%22%20height%3D%2228%22%20patternUnits%3D%22userSpaceOnUse%22%3E%3Cg%20stroke%3D%22url%28%23l%29%22%20stroke-width%3D%220.75%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M4%208%20H14%20V20%20H24%22%2F%3E%3Cpath%20d%3D%22M10%204%20V14%20H20%22%2F%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%2220%22%20r%3D%221.5%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3Ccircle%20cx%3D%2220%22%20cy%3D%2214%22%20r%3D%221.2%22%20fill%3D%22%2367e8f9%22%20stroke%3D%22none%22%2F%3E%3C%2Fg%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%2214%22%20r%3D%221.1%22%20fill%3D%22%2393c5fd%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%220%22%20r%3D%220.7%22%20fill%3D%22%2367e8f9%22%2F%3E%3Ccircle%20cx%3D%2228%22%20cy%3D%2228%22%20r%3D%220.7%22%20fill%3D%22%2367e8f9%22%2F%3E%3C%2Fpattern%3E%3C%2Fdefs%3E%3Crect%20width%3D%22248%22%20height%3D%22330%22%20fill%3D%22url%28%23p%29%22%2F%3E%3Cg%20stroke%3D%22url%28%23l%29%22%20stroke-width%3D%220.9%22%20fill%3D%22none%22%20opacity%3D%220.86%22%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%2214%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%2214%22%20r%3D%221.3%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M14%2014%20L14%2038%20M14%2014%20L38%2014%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22url%28%23l%29%22%20stroke-width%3D%220.9%22%20fill%3D%22none%22%20opacity%3D%220.86%22%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%2214%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%2214%22%20r%3D%221.3%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M234%2014%20L234%2038%20M234%2014%20L210%2014%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22url%28%23l%29%22%20stroke-width%3D%220.9%22%20fill%3D%22none%22%20opacity%3D%220.86%22%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%22316%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%22316%22%20r%3D%221.3%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M14%20316%20L14%20292%20M14%20316%20L38%20316%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22url%28%23l%29%22%20stroke-width%3D%220.9%22%20fill%3D%22none%22%20opacity%3D%220.86%22%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%22316%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%22234%22%20cy%3D%22316%22%20r%3D%221.3%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M234%20316%20L234%20292%20M234%20316%20L210%20316%22%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%22translate%28124%2C165%29%22%20stroke%3D%22url%28%23l%29%22%20fill%3D%22none%22%3E%3Ccircle%20r%3D%2232%22%20stroke-width%3D%220.8%22%20opacity%3D%220.55%22%2F%3E%3Ccircle%20r%3D%2222%22%20stroke-width%3D%220.6%22%20opacity%3D%220.4%22%2F%3E%3Cpath%20d%3D%22M-30%200%20L30%200%20M0%20-30%20L0%2030%20M-21%20-21%20L21%2021%20M21%20-21%20L-21%2021%22%20stroke-width%3D%220.5%22%20opacity%3D%220.35%22%2F%3E%3Ccircle%20r%3D%224%22%20stroke-width%3D%221%22%20opacity%3D%220.72%22%2F%3E%3Ccircle%20r%3D%221.5%22%20fill%3D%22%2393c5fd%22%20stroke%3D%22none%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}
/* The back carries the reward's own icon, blurred almost out of sight, over
   the word the component prints there. */
.ct-cel-ghost {
  position: relative;
  font-size: 54px; line-height: 1;
  filter: blur(5px);
  opacity: 0.22;
}
.ct-cel-ach {
  position: relative;
  font-size: 8px; font-weight: 900; letter-spacing: 0.3em;
  text-transform: uppercase; color: #67e8f9;
}

.ct-cel-back::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 340px; height: 340px;
  margin: -170px 0 0 -170px;
  background: radial-gradient(circle, rgba(6,182,212,0.20) 0%, transparent 65%);
  filter: blur(24px);
  animation: ct-cel-spin 12s linear infinite;
}
@keyframes ct-cel-spin { to { transform: rotate(360deg); } }

/* The turn's flash is NOT part of the card. In the component it is a separate
   320px burst on the overlay, which is the whole point: it fires while the
   card is edge-on and therefore invisible, and it is what the eye watches
   during the swap. Built as a child of the card it turned edge-on too, and
   the most important beat of the section played out on a blank screen. */
.ct-cel-flash {
  position: absolute;
  top: 50%; left: 50%;
  width: 320px; height: 320px;
  margin: -160px 0 0 -160px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255,250,220,1) 0%, #93c5fd 30%, rgba(103,232,249,0.4) 60%, transparent 80%);
  filter: blur(8px);
  opacity: 0;
  transform: scale(.3);
  pointer-events: none;
}
/* 0.55s from the moment the card reaches 90deg (38.9%), so it is done before
   the card starts back at 44.2%. */
.ct-celebrate.is-on .ct-cel-flash { animation: ct-cel-flash 9.5s ease-out forwards; }
@keyframes ct-cel-flash {
  0%, 38.9% { opacity: 0;  transform: scale(.3);  }
  40.8%     { opacity: 1;  transform: scale(1.8); }
  42.8%     { opacity: .6; transform: scale(3.5); }
  44.7%,
  100%      { opacity: 0;  transform: scale(5);   }
}

/* The 14 sparks the burst throws outwards with it. */
.ct-cel-sparks { position: absolute; inset: 0; pointer-events: none; }
.ct-cel-spark {
  position: absolute;
  top: 50%; left: 50%;
  border-radius: 50%;
  opacity: 0;
}
.ct-celebrate.is-on .ct-cel-spark { animation: ct-cel-spark 9.5s ease-out forwards; }
/* They do not exist until the turn: in the component they mount with the
   flash. Held at opacity 1 from 0% they sat stacked on the centre of the
   face-down card as one white dot for three and a half seconds. */
@keyframes ct-cel-spark {
  0%, 38.8% { opacity: 0; transform: translate(0, 0) scale(1); }
  38.9%     { opacity: 1; transform: translate(0, 0) scale(1); }
  44.7%,
  100%      { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(.2); }
}
.ct-cel-spark:nth-child(1 ) { width: 4px; height: 4px; margin: -2px 0 0 -2px; background: #67e8f9; --tx: 80.0px; --ty: 0.0px; animation-delay: 0.00s; }
.ct-cel-spark:nth-child(2 ) { width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; background: #60a5fa; --tx: 92.8px; --ty: 44.7px; animation-delay: 0.03s; }
.ct-cel-spark:nth-child(3 ) { width: 10px; height: 10px; margin: -5px 0 0 -5px; background: #a78bfa; --tx: 78.6px; --ty: 98.5px; animation-delay: 0.06s; }
.ct-cel-spark:nth-child(4 ) { width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; background: #ffffff; --tx: 33.2px; --ty: 145.3px; animation-delay: 0.09s; }
.ct-cel-spark:nth-child(5 ) { width: 8px; height: 8px; margin: -4px 0 0 -4px; background: #bae6fd; --tx: -18.2px; --ty: 79.9px; animation-delay: 0.12s; }
.ct-cel-spark:nth-child(6 ) { width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; background: #67e8f9; --tx: -65.5px; --ty: 82.1px; animation-delay: 0.15s; }
.ct-cel-spark:nth-child(7 ) { width: 6px; height: 6px; margin: -3px 0 0 -3px; background: #60a5fa; --tx: -115.3px; --ty: 55.5px; animation-delay: 0.18s; }
.ct-cel-spark:nth-child(8 ) { width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; background: #a78bfa; --tx: -151.0px; --ty: 0.0px; animation-delay: 0.21s; }
.ct-cel-spark:nth-child(9 ) { width: 4px; height: 4px; margin: -2px 0 0 -2px; background: #ffffff; --tx: -75.7px; --ty: -36.4px; animation-delay: 0.24s; }
.ct-cel-spark:nth-child(10) { width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; background: #bae6fd; --tx: -66.7px; --ty: -83.7px; animation-delay: 0.27s; }
.ct-cel-spark:nth-child(11) { width: 10px; height: 10px; margin: -5px 0 0 -5px; background: #67e8f9; --tx: -28.9px; --ty: -126.7px; animation-delay: 0.30s; }
.ct-cel-spark:nth-child(12) { width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; background: #60a5fa; --tx: 34.0px; --ty: -149.2px; animation-delay: 0.33s; }
.ct-cel-spark:nth-child(13) { width: 8px; height: 8px; margin: -4px 0 0 -4px; background: #a78bfa; --tx: 53.6px; --ty: -67.2px; animation-delay: 0.36s; }
.ct-cel-spark:nth-child(14) { width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; background: #ffffff; --tx: 98.2px; --ty: -47.3px; animation-delay: 0.39s; }

/* The front face's own frame: a second border inset 10px, and a lit dot in
   each corner. Both are the component's; without them the card was a plain
   rounded rectangle and read as a generic modal rather than a ticket. */
.ct-cel-rule {
  position: absolute; inset: 10px; border-radius: 14px;
  border: 1px solid rgba(34, 211, 238, 0.55); pointer-events: none;
}
.ct-cel-pip {
  position: absolute; width: 6px; height: 6px; border-radius: 50%;
  background: #93c5fd; box-shadow: 0 0 7px #93c5fd;
}
.ct-cel-pip:nth-of-type(1) { top: 12px;    left: 12px;  }
.ct-cel-pip:nth-of-type(2) { top: 12px;    right: 12px; }
.ct-cel-pip:nth-of-type(3) { bottom: 12px; left: 12px;  }
.ct-cel-pip:nth-of-type(4) { bottom: 12px; right: 12px; }

/* The hero is the REWARD's icon at 68px, not the theme's bolt — the bolt
   belongs in the label line, and it was standing in for the wrong thing. */
.ct-cel-hero {
  position: relative;
  font-size: 68px; line-height: 1;
  filter: drop-shadow(0 6px 22px rgba(6,182,212,0.20)) drop-shadow(0 2px 6px #93c5fd);
}
/* Its entrance runs 1.2s from the moment the card lands face-up (48.2%), and
   for screen_time it is a hop with a small sideways drift. */
.ct-celebrate.is-on .ct-cel-hero { animation: ct-cel-hero 9.5s ease-out forwards; }
@keyframes ct-cel-hero {
  0%, 48.2% { transform: translate(0, 0) scale(1); }
  52.4%     { transform: translate(-2px, -6px) scale(1.09); }
  54.5%     { transform: translate(-1px, -8px) scale(1.12); }
  56.6%     { transform: translate(2px, -4px)  scale(1.06); }
  60.8%,
  100%      { transform: translate(0, 0) scale(1); }
}

.ct-cel-text {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: 8px; text-align: center;
}
/* 9px at 0.24em with the theme's gradient running through the text — the
   shine is what makes the line read as foil on a ticket. */
.ct-celebrate-label {
  font-size: 9px; font-weight: 900; letter-spacing: 0.24em;
  text-transform: uppercase;
  background: linear-gradient(90deg, #0891b2, #93c5fd, #67e8f9, #93c5fd, #0891b2);
  background-size: 200% auto;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  animation: ct-cel-shine 2.6s linear infinite;
}
@keyframes ct-cel-shine { 0% { background-position: -200% 0 } 100% { background-position: 200% 0 } }

/* The component's 16 bubbles, each with its own offset, size, colour, speed
   and start — the arithmetic in RC_BUBBLES, resolved. */
.ct-cel-bub:nth-child(1 ) { margin-left:   -20px; width:  4px; height:  4px; background: #67e8f9; --bop: 0.30; animation-duration: 1.60s; animation-delay: 0.00s; }
.ct-cel-bub:nth-child(2 ) { margin-left:    32px; width: 10px; height: 10px; background: #60a5fa; --bop: 0.35; animation-duration: 1.81s; animation-delay: 0.15s; }
.ct-cel-bub:nth-child(3 ) { margin-left:   -59px; width:  6px; height:  6px; background: #a78bfa; --bop: 0.40; animation-duration: 2.02s; animation-delay: 0.30s; }
.ct-cel-bub:nth-child(4 ) { margin-left:    69px; width: 12px; height: 12px; background: #ffffff; --bop: 0.45; animation-duration: 2.23s; animation-delay: 0.45s; }
.ct-cel-bub:nth-child(5 ) { margin-left:   -98px; width:  8px; height:  8px; background: #bae6fd; --bop: 0.50; animation-duration: 2.44s; animation-delay: 0.60s; }
.ct-cel-bub:nth-child(6 ) { margin-left:    31px; width:  4px; height:  4px; background: #67e8f9; --bop: 0.55; animation-duration: 2.65s; animation-delay: 0.75s; }
.ct-cel-bub:nth-child(7 ) { margin-left:   -57px; width: 10px; height: 10px; background: #60a5fa; --bop: 0.60; animation-duration: 2.86s; animation-delay: 0.90s; }
.ct-cel-bub:nth-child(8 ) { margin-left:    68px; width:  6px; height:  6px; background: #a78bfa; --bop: 0.65; animation-duration: 1.67s; animation-delay: 1.05s; }
.ct-cel-bub:nth-child(9 ) { margin-left:   -96px; width: 12px; height: 12px; background: #ffffff; --bop: 0.30; animation-duration: 1.88s; animation-delay: 1.20s; }
.ct-cel-bub:nth-child(10) { margin-left:    25px; width:  8px; height:  8px; background: #bae6fd; --bop: 0.35; animation-duration: 2.09s; animation-delay: 1.35s; }
.ct-cel-bub:nth-child(11) { margin-left:   -50px; width:  4px; height:  4px; background: #67e8f9; --bop: 0.40; animation-duration: 2.30s; animation-delay: 1.50s; }
.ct-cel-bub:nth-child(12) { margin-left:    62px; width: 10px; height: 10px; background: #60a5fa; --bop: 0.45; animation-duration: 2.51s; animation-delay: 0.05s; }
.ct-cel-bub:nth-child(13) { margin-left:   -89px; width:  6px; height:  6px; background: #a78bfa; --bop: 0.50; animation-duration: 2.72s; animation-delay: 0.20s; }
.ct-cel-bub:nth-child(14) { margin-left:    19px; width: 12px; height: 12px; background: #ffffff; --bop: 0.55; animation-duration: 2.93s; animation-delay: 0.35s; }
.ct-cel-bub:nth-child(15) { margin-left:   -48px; width:  8px; height:  8px; background: #bae6fd; --bop: 0.60; animation-duration: 1.74s; animation-delay: 0.50s; }
.ct-cel-bub:nth-child(16) { margin-left:    61px; width:  4px; height:  4px; background: #67e8f9; --bop: 0.65; animation-duration: 1.95s; animation-delay: 0.65s; }
.ct-celebrate-name { font-size: 21px; font-weight: 900; color: #0f172a; line-height: 1.2; }
/* "for Leo" — the card names who it is for. It said "is unlocked", which is
   the theme's verb, not a line the card ever carries. */
.ct-celebrate-sub  { font-size: 12px; color: #155e75; letter-spacing: 0.02em; }

/* A rounded rectangle, not a pill: the component stamps at radius 10. */
.ct-celebrate-stamp {
  position: absolute; right: 18px; bottom: 18px;
  padding: 6px 10px; border-radius: 10px;
  background: #0891b2; color: #fff;
  font-size: 10px; font-weight: 950; letter-spacing: 0.14em; text-transform: uppercase;
  box-shadow: 0 12px 28px rgba(0,0,0,0.22);
  opacity: 0;
}
.ct-celebrate.is-on .ct-celebrate-stamp { animation: ct-cel-stamp 9.5s cubic-bezier(.18,.9,.22,1) forwards; }
/* 0.55s from the turn landing: in oversized and tilted back, past the mark,
   and settles at -4deg. */
@keyframes ct-cel-stamp {
  0%, 48.2% { opacity: 0; transform: rotate(-12deg) scale(1.8);  }
  51.1%     { opacity: 1; transform: rotate(3deg)   scale(.86); }
  54.0%     { opacity: 1; transform: rotate(-4deg)  scale(1);   }
  89.8%     { opacity: 1; transform: rotate(-4deg)  scale(1);   }
  100%      { opacity: 0; transform: rotate(-4deg)  scale(1);   }
}

/* The tap that starts it. *//* The tap that starts it. *//* The tap that starts it. */
.ct-reward.is-redeeming { animation: ct-redeem .7s cubic-bezier(.2,.9,.3,1); }
@keyframes ct-redeem {
  0%   { transform: scale(1);   }
  35%  { transform: scale(.95); }
  100% { transform: scale(1);   }
}
.ct-btn.is-press { animation: ct-press .34s ease; }

/* With motion off. The camera is stilled by the engine (wall-demo.css); these
   are the animations particular to this page, which it cannot know about. */
@media (prefers-reduced-motion: reduce) {
  .ct-reward.is-redeeming,
  .ct-celebrate, .ct-celebrate .ct-celebrate-card,
  .ct-cel-intro, .ct-cel-hero, .ct-celebrate-label,
  .ct-celebrate-stamp, .ct-cel-bub, .ct-cel-bubbles { animation: none !important; }
  .ct-cel-bub, .ct-cel-flash, .ct-cel-sparks { display: none; }
  .ct-celebrate-label { -webkit-text-fill-color: #0891b2; color: #0891b2; }
  /* The hearts drift whether or not anything else moves, so they have to be
     stopped by name — they are the one animation here not on the camera. Held
     where they are rather than hidden: the card is meant to have them. */
  .ct-hearts i { animation: none !important; }
}

/* The rewards section replaced a full-width card grid, so it inherited the
   whole 1180px wrap and stood 811px tall — it dominated the page and the
   zoomed-out frame was bigger than the wall it depicts. The cap now lives on
   .ct-frame above, which is the outer box. */

/* ── the selection ring ─────────────────────────────────────────────────── */
/* What the meals page does, brought here: the thing that was pressed rings,
   briefly. The app gives none of these controls feedback of its own — a tap is
   instantaneous there and the RESULT is the feedback — but on a screen with no
   finger in it, a row that simply becomes done reads as having done it itself.

   ON A PSEUDO-ELEMENT, for two separate reasons, both of which bit on the
   meals page first:
     · `animation` is one property, and `.ct-reward.is-redeeming` is already
       using it. At equal specificity the later rule just wins and the ring
       never runs.
     · a ring drawn ON the row would be faded by `.ct-row.is-done`'s own
       opacity: 0.6, arriving at 60% strength exactly when it is meant to be
       read.
   Inset, so it is clipped to the element's own 14px radius and cannot overlap
   its neighbours. #d97706 is the same accent the meals rings use: one colour
   across the site for "this is the thing that was pressed". */
.ct-row, .ct-reward { position: relative; }
.ct-row::after, .ct-reward::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  border-radius: 14px;
  pointer-events: none;
  box-shadow: inset 0 0 0 0 rgba(217, 119, 6, 0);
}
@keyframes ct-pick {
  0%   { box-shadow: inset 0 0 0 0 rgba(217, 119, 6, 0); }
  12%  { box-shadow: inset 0 0 0 3px rgba(217, 119, 6, 1); }
  62%  { box-shadow: inset 0 0 0 3px rgba(217, 119, 6, 1); }
  100% { box-shadow: inset 0 0 0 3px rgba(217, 119, 6, 0); }
}
.ct-row.is-picked::after, .ct-reward.is-picked::after {
  animation: ct-pick 1200ms ease forwards;
}

/* A ticked row holds full strength while it is ringing, then settles into the
   done state as the ring goes. The tick's own timing is untouched — the
   counter, the arc, the coins and the confetti all still fire in the same
   frame they always did; only the row's dimming waits for the ring. */
.ct-row.is-done.is-picked { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .ct-row.is-picked::after, .ct-reward.is-picked::after { animation: none !important; }
}
