/* ==========================================================================
   THE CALENDAR TAB, IN CODE — /family-calendar/
   One recreation of the wall's Calendar tab, used by all three animated
   sections on this page. Each section supplies only a camera track; the tab
   itself is authored once by tools/cal-build-tab.py, and styled 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 ships is the `lane-filter` variant (FamilyScheduleUnified/variants.js):
   members down a 56px frozen gutter, each gutter cell washed in that member's
   colour and carrying their busyness ring, and NO legend strip above the grid
   — the strip belongs to the retired `lanes` variant. Every size below is the
   component's own: LANE_GUTTER_W 56, GRID_HEADER_H 32, and the WEEK_CELL
   geometry (label 14, chip 40, gap 14, pad 8) that model/layout.js sizes the
   rows from.

   Palette is the wall's honey theme, which is what the tab actually wears in
   the screenshot this was measured against — and the same warm set the Chores
   page uses, so the two pages depict one wall rather than two products.
   ========================================================================== */

/* ── the tab ────────────────────────────────────────────────────────────── */
.cal {
  width: 1284px;
  padding: 24px;
  background: #fdf9f3;
  font-family: inherit;
  color: #33302c;
}

/* nestory-card: white on the page's cream, a 16px radius and a soft lift. */
.cal-card {
  position: relative;
  padding: 16px;
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.07);
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(160, 100, 0, 0.12);
}

/* ── the widget header ──────────────────────────────────────────────────── */
/* ScheduleHeader.jsx: title, stepper, a flexible gap, then the view toggle and
   the settings gear. */
.cal-head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }

.cal-wtitle {
  font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: #b4772a; white-space: nowrap;
}

.cal-step { display: flex; align-items: center; gap: 4px; }
/* The 18px stepper buttons, which match CalendarMainHeader's so the two
   widgets read as one surface. */
.cal-step i {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 3px;
  background: rgba(255, 255, 255, 0.75); border: 1px solid rgba(0, 0, 0, 0.07);
  font-size: 10px; line-height: 1; font-style: normal; color: #8a7f6d;
}
.cal-step b { min-width: 68px; text-align: center; font-size: 10px; font-weight: 400; color: #9a8f7d; }

.cal-spacer { flex: 1 1 0; min-width: 0; }

.cal-views {
  display: inline-flex; align-items: center; padding: 2px;
  border-radius: 999px; background: #fbf7ec; border: 1px solid rgba(0, 0, 0, 0.07);
}
.cal-views span {
  min-width: 38px; padding: 2px 8px; border-radius: 999px;
  font-size: 11px; font-weight: 600; text-align: center; color: #9a8f7d;
}
.cal-views .is-on { background: #d97706; color: #fff; }

.cal-gear {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 8px; color: #c2762a;
  background: rgba(217, 119, 6, 0.10);
}
.cal-gear svg { width: 14px; height: 14px; }

/* The wall's add button, which floats over the bottom-right of the grid. */
.cal-fab {
  position: absolute; right: 18px; bottom: 18px;
  display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 999px;
  background: #d97706; color: #fff; font-size: 24px; font-weight: 300; line-height: 1;
  box-shadow: 0 8px 20px rgba(160, 100, 0, 0.28);
  /* The tasks card is a later positioned sibling, so without this it paints
     over the button rather than under it. */
  z-index: 2;
}

/* ── the grid ───────────────────────────────────────────────────────────── */
/* The member gutter sits OUTSIDE the day scroller, which is what freezes it
   structurally — WeekGrid.jsx. Here nothing scrolls, but the two-box split is
   kept because it is what makes the gutter's own background and border right. */
.cal-grid { display: flex; width: 100%; min-width: 0; }

.cal-gutter {
  flex: 0 0 56px; width: 56px;
  background: #fff; border-right: 1px solid rgba(0, 0, 0, 0.07);
}
.cal-gutter-head { height: 32px; border-bottom: 1px solid rgba(0, 0, 0, 0.07); }

.cal-gcell {
  display: flex; align-items: center; justify-content: center; padding: 0 4px;
  border-top: 1px solid rgba(0, 0, 0, 0.07);
}

/* LoadRing: a 32px face inside a 44px ring — two readings in one shape, how
   much of the week is booked (the arc) and how heavy that adds up to (its
   colour). The track is neutral because the cell behind it already carries the
   member's colour. */
.cal-ring { position: relative; display: inline-flex; width: 44px; height: 44px; }
.cal-ring svg { position: absolute; inset: 0; width: 44px; height: 44px; }
.cal-ring img {
  width: 32px; height: 32px; margin: 6px; border-radius: 999px; object-fit: cover;
}

.cal-days { flex: 1 1 auto; min-width: 0; }
.cal-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }

/* Weekday, date and forecast on ONE line — GRID_HEADER_H is 32 and the heading
   used to stack those three, spending 56px on "SUN / 20 / ☀". */
.cal-dhead {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  height: 32px; padding: 0 4px; overflow: hidden;
  border-left: 1px solid rgba(0, 0, 0, 0.07);
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
  color: #9a8f7d;
}
.cal-dow { font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; flex: none; }
.cal-dnum { font-size: 12px; font-weight: 700; line-height: 1; padding: 4px 6px; border-radius: 999px; flex: none; }
/* An emoji's glyph runs taller than its font size; left to `normal` it drags
   the whole strip taller. */
.cal-wx { font-size: 12px; line-height: 12px; flex: none; }

.cal-dhead.is-today { background: rgba(217, 119, 6, 0.10); color: #d97706; }
.cal-dhead.is-today .cal-dnum { background: #d97706; color: #fff; }

/* ── a day cell ─────────────────────────────────────────────────────────── */
/* The gap is part of the geometry the row height was computed from, so it is
   stated as the same number WEEK_CELL carries — 14. */
.cal-cell {
  display: flex; flex-direction: column; gap: 14px; padding: 4px; overflow: hidden;
  border-top: 1px solid rgba(0, 0, 0, 0.07);
  border-left: 1px solid rgba(0, 0, 0, 0.07);
}

/* Always drawn, empty or not: every cell in the row reserves this line so the
   chips beneath start at the same height across the week. A ragged top edge
   costs more than the empty line does — the eye reads a week across. */
.cal-lab {
  flex: none; height: 14px; font-size: 10px; line-height: 14px; font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase; text-align: center;
  color: #9a8f7d; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* A discrete event: solid and rounded, so it sits clearly ON the hatching
   rather than blending into it. Opaque, never a 90% fill — a translucent block
   takes its final colour from whatever it lands on, which is the one thing a
   contrast decision cannot be made against. */
.cal-chip {
  flex: none; display: flex; align-items: center; gap: 4px;
  min-height: 40px; padding: 0 8px; border-radius: 6px;
  font-size: 12px; line-height: 16px; font-weight: 600; overflow: hidden;
}
.cal-chip-txt { flex: 1 1 auto; min-width: 0; }
.cal-chip-t { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Deliberately faint and small: it answers "when, roughly" at a glance without
   competing with the thing that is happening. */
.cal-chip-w {
  display: block; font-size: 10px; line-height: 12px; font-weight: 500; opacity: 0.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The OTHER people on a shared event. The owner is deliberately absent — the
   lane already says whose event it is, and repeating their face inside their
   own row would spend the block's scarcest pixels restating the row it is in. */
.cal-faces { display: inline-flex; align-items: center; flex: none; }
.cal-faces img {
  width: 13px; height: 13px; border-radius: 999px; object-fit: cover;
  box-shadow: 0 0 0 1.5px #fff;
}

/* ── the tasks widget below ─────────────────────────────────────────────── */
.cal-tasks { margin-top: 12px; padding: 12px; border-radius: 16px; }
.cal-tasks-head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.cal-tasks-count { margin-left: auto; font-size: 11px; font-weight: 600; color: #9a8f7d; }

.cal-tgroup + .cal-tgroup { margin-top: 12px; }
.cal-tglabel {
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px; padding: 0 2px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: #9a8f7d;
}
.cal-tglabel i { width: 8px; height: 8px; border-radius: 999px; flex: none; }
.cal-tglabel b { font-weight: 400; opacity: 0.6; }

.cal-tlist { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.cal-task {
  display: flex; align-items: center; gap: 8px; min-height: 40px;
  padding: 6px 10px; border-radius: 12px;
  background: #fff; border: 1px solid rgba(0, 0, 0, 0.07);
  box-shadow: 0 1px 3px rgba(160, 100, 0, 0.08);
}
.cal-pri { width: 6px; height: 6px; border-radius: 999px; flex: none; }
.cal-ttitle {
  flex: 1 1 auto; min-width: 0; font-size: 12px; font-weight: 600; color: #33302c;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-tdue { flex: none; font-size: 10px; color: #9a8f7d; }
.cal-tick {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 999px;
  background: #d97706; color: #fff;
}
.cal-tick svg { width: 12px; height: 12px; }

/* ── the family-event countdown tiles ───────────────────────────────────── */
.cal-events { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 12px; }
.cal-event {
  position: relative; height: 160px; border-radius: 16px; border: 1px solid; overflow: hidden;
}
.cal-ev-icon { position: absolute; top: 12px; right: 16px; font-size: 48px; line-height: 1; opacity: 0.25; }
.cal-ev-orb {
  position: absolute; left: -28px; bottom: -32px; width: 96px; height: 96px;
  border-radius: 999px; opacity: 0.12;
}
.cal-ev-body { position: relative; height: 100%; padding: 16px; display: flex; flex-direction: column; }
.cal-ev-body h3 {
  margin: 0; font-size: 17px; font-weight: 600; line-height: 1.2; color: #33302c;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 48px;
}
.cal-ev-foot { margin-top: auto; }
.cal-ev-hero { font-size: 25px; font-weight: 700; line-height: 1.05; }
.cal-ev-detail { margin-top: 2px; font-size: 11px; font-weight: 500; line-height: 1.25; color: #7a7064; }

/* The wash layer, behind everything the cell draws. */
.cal-cell { position: relative; }
.cal-wash { position: absolute; inset: 0; pointer-events: none; }
.cal-cell > .cal-lab,
.cal-cell > .cal-chip { position: relative; z-index: 1; }

/* ==========================================================================
   CAMERA TRACKS
   --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), measured off the rendered tab rather than computed:

     tab            1284 x 1088
     calendar card  x 24-1260,  y 24-621
     grid           x 41-1243,  y 79-604
     member gutter  x 41-97                 <- FROZEN in the app
     day columns    97 261 424 588 752 916 1079, right edge 1243
     day headings   y 79-111
     lanes          Daniel 123-240  Maya 240-357  Ava 357-487  Leo 487-604
     tasks card     y 633-892
     event tiles    y 904-1064

   TWO CONSTRAINTS SHAPE EVERY TRACK BELOW.

   1. The member gutter never leaves the frame. It is `position: sticky` in
      spirit — WeekGrid puts it OUTSIDE the day scroller precisely so it cannot
      move — and a camera that panned it away would be showing a grid of
      coloured blocks with nothing on screen saying whose they are. That is not
      a framing preference, it is the widget's own rule. So --tx is pinned at
      -41 (the grid's left edge) in every zoomed hold, and the week is toured
      DOWN the lanes rather than across the days.

   2. A frame edge may only land on a column boundary. Chips are full-bleed
      inside their cell, so an edge anywhere else cuts one in half. The visible
      width at a zoom is 1284/z, so the usable widths are the boundaries
      themselves — 547 (3 days), 711 (4 days), 875 (5 days) — which is what
      fixes z at 2.347, 1.806 and 1.4674 rather than at round numbers.
   ========================================================================== */

/* --- 1. the hero: establish the object ----------------------------------- */
/* Opens on the tab as the wall shows it, then one long, slow push into the
   week. z=1.4674 is 1284/875: the frame's right edge lands exactly on the
   Thu/Fri column boundary, and at that zoom the visible height is 601.7 —
   which puts the bottom edge on the calendar card's own bottom (621) when the
   top is on its top (24). Both edges on the card, five days and every lane in
   shot.

   The push takes 47% of the cycle. Nothing happens in this section, so a long
   hold would be dead time; the move itself is the pace, and it gets one beat
   at the end rather than a pause in the middle. */
@keyframes cal-week {
  0%, 8%     { --z: 1;      --tx: 0;   --ty: 0; }    /* the tab as it stands */
  55%        { --z: 1.4674; --tx: -41; --ty: -24; }  /* one slow push in */
  68%        { --z: 1.4674; --tx: -41; --ty: -24; }  /* a beat at the end of it */
  92%, 100%  { --z: 1;      --tx: 0;   --ty: 0; }    /* back out; 0% = 100% */
}
.ct-stage[data-track="week"] .ct-cam { animation: cal-week 15s infinite; }

/* --- 2. down the lanes --------------------------------------------------- */
/* z=2.347 is 1284/547: the gutter and three whole days. Visible height is
   376.2, which is more than three lanes and less than four — so the section
   opens with the day headings and the two parents, then drifts down until the
   bottom edge is just past the grid's own (ty -240) and the children are in
   shot. -240 rather than -227.8, which would have landed the bottom edge
   exactly on the grid: the top edge is where the eye enters, and putting it on
   Maya's lane boundary is worth 12px of the card's own padding at the bottom.
   The alternative — both edges on lane boundaries — needs a width that cuts a
   chip in half, and a cut chip is worse than a cut of nothing.

   The drift is the point, not a transition to it: what the first frame cuts off
   is exactly what the second frame reveals, so nothing is left unseen before
   the camera starts going home. */
@keyframes cal-lanes {
  0%, 6%     { --z: 1;     --tx: 0;   --ty: 0; }
  18%        { --z: 2.347; --tx: -41; --ty: -79;
               animation-timing-function: linear; }     /* headings + the parents */
  56%        { --z: 2.347; --tx: -41; --ty: -240; }     /* one steady drift down */
  66%        { --z: 2.347; --tx: -41; --ty: -240; }     /* a beat on the children */
  88%, 100%  { --z: 1;     --tx: 0;   --ty: 0; }
}
.ct-stage[data-track="lanes"] .ct-cam { animation: cal-lanes 14s infinite; }

/* --- 3. a calendar connects ---------------------------------------------- */
/* The one genuine state change on the page, so the camera does the least: in,
   hold while it happens, out. z=1.806 is 1284/711 — the gutter and four whole
   days, which is where three of Maya's five events land. Visible height 489,
   and ty=-115 puts the bottom edge on the grid's bottom (604) with the top in
   the gap between the day headings and the first lane. Both edges clean.

   The hold runs 14%-74%, which is 9.6 seconds: the toast, the routine wash,
   and five events arriving one at a time all have to fit inside it, and the
   camera must not start leaving until the last one has landed. */
@keyframes cal-sync {
  0%, 5%     { --z: 1;     --tx: 0;   --ty: 0; }
  14%, 74%   { --z: 1.806; --tx: -41; --ty: -115; }
  90%, 100%  { --z: 1;     --tx: 0;   --ty: 0; }
}
.ct-stage[data-track="sync"] .ct-cam { animation: cal-sync 16s infinite; }

/* ── a lane filling ─────────────────────────────────────────────────────── */
/* Everything a connection brings arrives the way the widget itself would show
   it: the routine wash first, because it is the backdrop the events sit on,
   then the events in day order. Both are in the DOM from the start and merely
   hidden — the row's height is computed from its full contents, so a lane that
   built itself up element by element would grow under the reader and shove the
   two lanes below it down the wall. `includeEmpty` exists in buildLanes for
   exactly this reason: the grid stays stable while what fills it changes. */
[data-track="sync"] .cal-wash,
[data-track="sync"] .cal-chip {
  transition: opacity 420ms ease, transform 420ms cubic-bezier(.2, .9, .3, 1);
}
[data-track="sync"] .cal-row.is-pending .cal-wash,
[data-track="sync"] .cal-row.is-pending .cal-chip { opacity: 0; transform: translateY(5px); }
[data-track="sync"] .cal-row.is-pending.show-wash .cal-wash { opacity: 1; transform: none; }
[data-track="sync"] .cal-row.is-pending .cal-chip.is-in { opacity: 1; transform: none; }

/* The busyness ring answers the same question the lane does, so it has to move
   with it — a full week beside an empty hoop would be the widget disagreeing
   with itself. The app animates this arc on a 240ms ease; the demo takes
   longer because it is covering five arrivals rather than one. */
[data-track="sync"] .cal-ring .cal-arc { transition: stroke-dasharray 900ms ease; }
/* An empty lane gets a bare hoop, not a hoop with a dot on it. The component
   does this by not rendering the arc at all (`share > 0 &&`, LaneParts.jsx);
   here it has to stay in the DOM to be animated into, so the round cap is
   dropped instead — a zero-length dash with a round cap still paints a disc.

   `stroke-dasharray` is set HERE and nowhere else. A presentation attribute
   loses to a stylesheet, so the JS used to write an attribute this rule then
   overrode: the arc never moved, and a probe reading the attribute back saw
   the value it had just written and called it a pass. The class is the state. */
[data-track="sync"] .cal-gcell.is-pending .cal-arc {
  stroke-dasharray: 0 128.8;
  stroke-linecap: butt;
}

/* With motion off. The camera is stilled by the engine (wall-demo.css); this
   is the one thing on the page it cannot know about — and the resting frame
   has to be the FULL lane, never the empty one, or the section's whole point
   is a calendar that does not work. */
@media (prefers-reduced-motion: reduce) {
  [data-track="sync"] .cal-wash,
  [data-track="sync"] .cal-chip,
  [data-track="sync"] .cal-arc { transition: none !important; }
  [data-track="sync"] .cal-row.is-pending .cal-wash,
  [data-track="sync"] .cal-row.is-pending .cal-chip { opacity: 1; transform: none; }
  [data-track="sync"] .cal-gcell.is-pending .cal-arc {
    stroke-dasharray: 91.4 128.8; stroke-linecap: round;
  }
}

/* ==========================================================================
   THE DAY VIEW — what the widget opens on
   Time across at 110px an hour, one row per member, and two layers in each
   row: the routine wash full-height behind, its label in a reserved 14px line
   at the top, and discrete blocks on top, centred in their slot.
   Sizes are DayGrid.jsx's own — HOUR_W 110, LANE_ROW_MIN_H 68, SLOT_H 46,
   MAX_BLOCK_H 72, BLOCK_GAP 6, MIN_BLOCK_W 96.
   ========================================================================== */

.dv-time { flex: 1 1 auto; min-width: 0; overflow: hidden; }

.dv-hours {
  position: relative; height: 32px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
}
/* Sat just right of the gridline, not centred on it: dayBounds snaps the
   window to a whole hour, so there is always a mark at x=0 with nothing to its
   left, and a centred first label is half-clipped by the scroller's edge. */
.dv-hour {
  position: absolute; top: 50%; transform: translateY(-50%);
  font-size: 9px; font-weight: 600; color: #9a8f7d; white-space: nowrap;
}
/* The now pill DOES stay centred on its line — it is the one label that has to
   line up with a rule drawn through every row. */
.dv-nowpill {
  position: absolute; top: 50%; transform: translate(-50%, -50%); z-index: 25;
  padding: 0 4px; border-radius: 3px;
  background: #ef4444; color: #fff;
  font-size: 9px; font-weight: 700; white-space: nowrap;
}

.dv-row { position: relative; border-top: 1px solid rgba(0, 0, 0, 0.07); overflow: hidden; }
.dv-line { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(0, 0, 0, 0.07); opacity: 0.55; }

/* Layer 1 — where this member IS, full height, behind everything.
   `align-items: flex-start`: the wash fills the row because it is the day's
   texture, but its words live in the line reserved for them at the top, clear
   of every block. */
.dv-wash {
  position: absolute; top: 0; bottom: 0; z-index: 1;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 0 8px; overflow: hidden; color: #9a8f7d;
}
.dv-wash b {
  height: 14px; line-height: 14px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Drawn over the blocks, not under them: it is the one rule on the grid that
   has to be findable at a glance from across a room. */
.dv-now { position: absolute; top: 0; bottom: 0; width: 2px; background: #ef4444; z-index: 15; }

/* Layer 2 — a discrete block. Widened to a 96px floor so a 30-minute
   appointment still holds its title; packRow then treats the width it actually
   occupies as its extent, so a widened block cannot sit on top of the next. */
.dv-block {
  position: absolute; z-index: 5;
  display: flex; align-items: center; overflow: hidden; border-radius: 6px;
}
.dv-txt {
  flex: 1 1 auto; min-width: 0; height: 100%; padding: 0 8px;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
}
/* The height is spent on the title, because the width is what is scarce: two
   lines of 11px hold roughly twice what one truncated line does, at no cost in
   either axis. */
.dv-block b {
  font-size: 11px; font-weight: 600; line-height: 1.2;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.dv-block i {
  font-style: normal; font-size: 9px; line-height: 1.2; opacity: 0.85;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dv-block .cal-faces { padding-right: 6px; }
.dv-block .cal-faces img { width: 16px; height: 16px; }

/* ── chapter 1: the day assembling itself ───────────────────────────────── */
/* The markup ships FULL and the script empties it at t=0 — never the other way
   round. A build that lives in the markup's absence would leave a permanently
   empty calendar on the hero the moment the script fails to run, which is the
   worst thing this page could show. This way no-JS, an error, and
   prefers-reduced-motion all land on the finished board. */
[data-track="day"] .dv-wash,
[data-track="day"] .dv-block {
  transition: opacity 420ms ease, transform 420ms cubic-bezier(.2, .9, .3, 1);
}
[data-track="day"] .dv-wash.is-hidden { opacity: 0; }
/* Used for one frame, when the script empties the board it was handed. */
[data-track="day"] .no-anim,
.cal-swap.no-anim,
.cal-swap.no-anim *,
.ct-switch.no-anim,
.ct-switch.no-anim i { transition: none !important; }
/* Pills drop in rather than fading up: a block arriving is an event landing on
   a day, and downward motion is what that reads as. */
[data-track="day"] .dv-block.is-hidden { opacity: 0; transform: translateY(-9px) scale(0.97); }

/* The busyness ring answers "how full is this person's day", so it cannot be
   full while the day is empty. It arrives with the events, which is what fills
   it — routines alone are baseline and barely move it (load.js). Same rule as
   the sync track: the CLASS is the state, never a written attribute, because a
   presentation attribute loses to a stylesheet. */
[data-track="day"] .cal-ring .cal-arc { transition: stroke-dasharray 700ms ease; }
[data-track="day"] .cal-gcell.is-pending .cal-arc {
  stroke-dasharray: 0 128.8;
  stroke-linecap: butt;
}

/* The camera is LOCKED for the whole build — see the track below — so with
   motion off the resting frame is simply the finished day. */
@media (prefers-reduced-motion: reduce) {
  [data-track="day"] .dv-wash,
  [data-track="day"] .dv-block { transition: none !important; opacity: 1 !important; transform: none !important; }
  [data-track="day"] .cal-arc { transition: none !important; }
}

/* --- chapter 1 camera ---------------------------------------------------- */
/* Locked at z=1.752 for the entire build, which is the whole idea: the subject
   is the board filling in, so a camera moving at the same time would be two
   things happening at once and neither would read.

   1.752 is 883/504, and 504 is exactly the grid's height — the 32px hour strip
   plus four rows at LANE_ROW_MAX_H. So with ty=-79 the frame's top edge sits
   on the hour strip and its bottom edge on the grid's last rule, both on
   content. The visible width that falls out of it is 733, which puts the right
   edge 677px into the timeline: the gutter plus six hours, 8am to just past 2.
   That is the constraint the day's data was written around — everything that
   arrives during the build happens before 2pm, and the afternoon is what the
   pull-back at the end is FOR.

   It opens and closes at z=1 so the loop seam is a match rather than a cut.

   12s, and the length is set by the captions rather than by the motion. Each
   has to be READ, which is about two seconds once the eye has found the pill —
   so the build takes as long as the reading does and the moving parts wait. A
   caption gone before it has been finished is the same as not writing it.

   There is no closing caption. The last line said what the finished board was
   already showing, and a summary of a picture the reader is looking at is the
   one caption that can be cut for free — the pull-back is the ending. */
@keyframes cal-day {
  0%, 3.7%     { --z: 1;     --tx: 0;   --ty: 0; }   /* the tab as it stands */
  8.7%, 68.5%  { --z: 1.752; --tx: -41; --ty: -79; } /* locked, while it builds */
  78.5%, 100%  { --z: 1;     --tx: 0;   --ty: 0; }   /* out — the afternoon appears */
}
.ct-stage[data-track="day"] .ct-cam { animation: cal-day 12s infinite; }

/* ==========================================================================
   CHAPTER 2 — zooming in on a commitment
   Both grids in one cell, two lanes folded away, then one event opened.
   ========================================================================== */

/* Both views share a single grid cell and the hidden one keeps its layout —
   `visibility`, not `display`. A hidden grid still contributes height, so the
   box is as tall as the TALLER of the two and tapping the toggle changes what
   is in the widget rather than how much room it takes (index.jsx). */
.cal-stack { display: grid; grid-template-columns: minmax(0, 1fr); }
.cal-view { grid-area: 1 / 1; min-width: 0; visibility: hidden; }
.cal-view.is-on { visibility: visible; }

/* The stepper carries both labels; only the live one is drawn. */
.cal-step b[data-label] { display: none; }
.cal-step b.is-on { display: inline; }

/* ── folding a lane ─────────────────────────────────────────────────────── */
/* COLLAPSED_LANE_H is 40. A folded lane greys out rather than vanishing: the
   family can see who is being left out, and the face is still there to put
   them back. The height is a variable so the same rule serves both grids. */
.cal-row, .dv-row, .cal-gcell { height: var(--h); transition: height 420ms cubic-bezier(.4, 0, .2, 1); }
.cal-row.is-folded, .dv-row.is-folded, .cal-gcell.is-folded { height: 40px; }

.cal-row.is-folded, .dv-row.is-folded {
  background: #fbf7ec;
  opacity: 0.55;
}
/* Nothing is drawn in a folded lane — the events are gone, not merely faded. */
.cal-row.is-folded .cal-chip,
.cal-row.is-folded .cal-lab,
.cal-row.is-folded .cal-wash,
.dv-row.is-folded .dv-block,
.dv-row.is-folded .dv-wash,
.dv-row.is-folded .dv-now { opacity: 0; }

/* The face stays, greyed and smaller, and loses its ring: a meter for someone
   whose events are hidden is answering a question nobody asked. */
.cal-gcell.is-folded { opacity: 0.4; filter: grayscale(1); background: transparent !important; }
.cal-gcell.is-folded .cal-ring svg { opacity: 0; }
.cal-gcell.is-folded .cal-ring { width: 26px; height: 26px; }
.cal-gcell.is-folded .cal-ring img { width: 26px; height: 26px; margin: 0; }

.cal-row, .dv-row, .cal-gcell,
.cal-chip, .cal-lab, .cal-wash, .dv-block, .dv-wash, .cal-ring, .cal-ring img {
  transition: height 420ms cubic-bezier(.4, 0, .2, 1), opacity 300ms ease,
              filter 300ms ease, background-color 300ms ease, width 420ms ease, margin 420ms ease;
}

/* ── the event dialog ───────────────────────────────────────────────────── */
/* Pinned to the SCREEN, not the tab — so it is the same size whatever the
   camera is doing behind it, exactly as on the wall. */
.ct-modal {
  position: absolute; inset: 0; z-index: 8;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 240ms ease;
}
.ct-modal.is-on { opacity: 1; }
.ct-veil { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.8); }

/* Authored at the component's own 512px and scaled by --fit, so every size
   below is the number the app actually uses. Sizing it as a percentage of the
   stage and then picking px values by eye made the type twice its real weight.
   --fit is a plain custom property on the stage, so it inherits down here. */
/* The panel is ON THE GLASS, so it scales with the camera: when the view
   closes in on the wall, a modal on that wall gets closer too. It did not, and
   the result was a correctly-proportioned dialog whose 9px reminder segments
   rendered at under 4px — unreadable, and quietly inconsistent with everything
   behind it. --mz is the camera's own z for the beats the panel is open. */
.ct-scaler {
  transform: scale(calc(var(--fit, 0.42) * var(--mz, 1)));
  transition: transform 240ms cubic-bezier(.2, .9, .3, 1);
}
.ct-card {
  position: relative;
  width: 512px;
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
  /* NOT overflow:hidden — the reminder popover has to be able to stand above
     the footer without being cut off, exactly as the real one does. */
  transform: scale(0.96) translateY(-8px);
  transition: transform 240ms cubic-bezier(.2, .9, .3, 1);
}
.ct-modal.is-on .ct-card { transform: none; }

.ct-chead { display: flex; align-items: flex-start; gap: 12px; padding: 20px 20px 16px; }
.ct-cicon {
  flex: none; width: 48px; height: 48px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.72);
}
.ct-cicon svg { width: 20px; height: 20px; }
.ct-ctitle { flex: 1 1 auto; min-width: 0; font-size: 16px; font-weight: 600; line-height: 1.25; color: #33302c; padding-top: 2px; }
.ct-cx { flex: none; width: 32px; height: 32px; color: #9a8f7d; display: flex; align-items: center; justify-content: center; }
.ct-cx svg { width: 16px; height: 16px; }

.ct-cbody { padding: 0 20px 16px; display: flex; flex-direction: column; gap: 12px; }
.ct-drow { display: flex; align-items: flex-start; gap: 12px; font-size: 14px; line-height: 1.4; color: #33302c; }
.ct-dicon { flex: none; width: 16px; height: 16px; color: #9a8f7d; margin-top: 2px; }
.ct-dicon svg { width: 16px; height: 16px; }
.ct-dim { color: #8a8070; }
.ct-caldot { display: inline-block; width: 8px; height: 8px; border-radius: 999px; margin-right: 6px; vertical-align: middle; }

.ct-cfoot {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  padding: 12px 20px; border-top: 1px solid rgba(0, 0, 0, 0.07);
}
.ct-bell {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 999px; margin-right: auto;
  background: rgba(217, 119, 6, 0.12); color: #c2762a;
  transition: background-color 200ms ease, color 200ms ease;
}
.ct-bell svg { width: 14px; height: 14px; }
/* Solid once the reminder is set, so on and off stay tellable apart. */
.ct-modal.is-armed .ct-bell { background: #d97706; color: #fff; }

.ct-cbtn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.75); border: 1px solid rgba(0, 0, 0, 0.07);
  font-size: 12px; font-weight: 600; color: #4a4239;
}
.ct-cbtn svg { width: 12px; height: 12px; }
.ct-cbtn--del { color: #b3261e; border-color: rgba(179, 38, 30, 0.22); }

/* ── the reminder popover ───────────────────────────────────────────────── */
/* Sits above the bell it belongs to, as ReminderPopover does. */
.ct-pop {
  position: absolute; left: 50%; bottom: calc(100% + 8px);
  display: flex; align-items: center; gap: 6px;
  padding: 6px 10px; border-radius: 10px;
  background: #fff; border: 1px solid rgba(0, 0, 0, 0.07);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 6px);
  transition: opacity 200ms ease, transform 200ms cubic-bezier(.2, .9, .3, 1);
  pointer-events: none;
}
.ct-modal.is-pop .ct-pop { opacity: 1; transform: translate(-50%, 0); }
.ct-popemoji { font-size: 12px; line-height: 1; }
.ct-popdiv { width: 1px; height: 16px; background: rgba(0, 0, 0, 0.07); }
.ct-segs { display: flex; padding: 2px; border-radius: 99px; background: rgba(217, 119, 6, 0.12); }
.ct-seg {
  padding: 2px 7px; border-radius: 99px;
  font-size: 9px; font-weight: 600; color: #c2762a; white-space: nowrap;
  line-height: 1.5;
}
.ct-seg.is-on { background: #d97706; color: #fff; }
.ct-popvol { display: flex; color: #c2762a; }
.ct-popvol svg { width: 12px; height: 12px; }
.ct-switch {
  position: relative; width: 28px; height: 16px; border-radius: 99px;
  background: rgba(0, 0, 0, 0.12);
}
.ct-switch i {
  position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
  border-radius: 999px; background: #fff;
  transition: transform 180ms ease;
}
.ct-switch.is-on { background: #d97706; }
.ct-switch.is-on i { transform: translateX(12px); }

/* The tap that opens it — the chip presses the way a button does. */
.cal-chip.is-press { animation: ct-press .34s ease; }
.cal-gcell.is-press .cal-ring { animation: ct-press .34s ease; }
.cal-views span.is-press { animation: ct-press .34s ease; }

/* --- chapter 2 camera ---------------------------------------------------- */
/* Landmarks, measured in both fold states (tab space):
 *
 *   open     stack y 79-604   tab 1088   lanes 123 / 240 / 357 / 487
 *   folded   stack y 79-450   tab  934   lanes 123 / 163 / 203 / 333
 *   header   y 41-67          view toggle x 1119-1209, gear 1217-1243
 *
 * Four positions, and each one is a shot rather than a waypoint.
 *
 * 1. WIDE. The tab as it stands, in its Day view.
 *
 * 2. THE TOGGLE, z=4.586 (1284/280). The frame's right edge lands on the
 *    card's own right edge (1260) and its top on the card's top (24), holding
 *    the Day/Week control and the gear beside it. The press happens here and
 *    the camera LEAVES before the grid swaps — the change is revealed on the
 *    way out rather than watched close up, which is the only way a cut this
 *    tight can pay off.
 *
 * 3. WIDE again, for the fold. Two lanes collapsing takes 154px out of the
 *    board; zooming while that happens would hide the one thing it is for.
 *
 * 4. THE LANES, z=2.347 (1284/547) — the gutter and three whole days, with the
 *    frame's right edge on the Tue/Wed boundary and its bottom 5px past the
 *    grid's own. Both folded rows, both open ones and the day headings are in
 *    shot. It used to be 3.35, which magnified two chips and threw the rest of
 *    the week away; at this distance the context survives and a 12px chip
 *    still renders at 11.9px.
 *
 * tx is pinned at -41 in shot 4 for the reason it is pinned in every other
 * track here: the member gutter is frozen in the widget. Shot 2 is free to
 * move because the header is not the grid. */
@keyframes cal-zoom {
  0%, 5.33%      { --z: 1;     --tx: 0;    --ty: 0; }    /* the tab, in Day */
  12%, 17.33%    { --z: 4.586; --tx: -980; --ty: -24; }  /* in on the view toggle */
  /* Wide again at 24% (3.6s) and held to 43.3% (6.5s). The board is still the
     Day for the first 600ms of that — the pause — then becomes the Week, and
     the fold follows with room to finish before the camera moves again. */
  24%, 43.33%    { --z: 1;     --tx: 0;    --ty: 0; }
  50%, 80%       { --z: 2.347; --tx: -41;  --ty: -79; }  /* in on the two open lanes */
  88%, 100%      { --z: 1;     --tx: 0;    --ty: 0; }    /* out, back to the start */
}
.ct-stage[data-track="zoom"] .ct-cam { animation: cal-zoom 15s infinite; }

/* With motion off: the week, everyone showing, nothing opened. The chapter's
   resting frame is the board it starts from, not the state it ends in — a
   still of two folded-away parents and a modal is not what the section is
   about. */
@media (prefers-reduced-motion: reduce) {
  [data-track="zoom"] .cal-row,
  [data-track="zoom"] .dv-row,
  [data-track="zoom"] .cal-gcell,
  [data-track="zoom"] .ct-card,
  [data-track="zoom"] .ct-scaler,
  [data-track="zoom"] .ct-pop { transition: none !important; }
  [data-track="zoom"] .ct-modal { display: none; }
}

/* ==========================================================================
   CHAPTER 3 — the week writes the list
   ========================================================================== */

.cal-tasks-head .cal-wtitle { color: #b4772a; }
/* WidgetTitleButton at `alpha="low"` — the new-task control. */
.cal-tadd {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 6px;
  background: rgba(217, 119, 6, 0.10); color: #c2762a;
  font-size: 15px; font-weight: 400; line-height: 1;
}
.cal-sugtoggle { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
.cal-sugtoggle b { font-size: 11px; font-weight: 600; color: #33302c; }

/* Two numbers, because they are two different things: what the family has
   agreed to, then what is merely proposed. */
.cal-tglabel b { font-weight: 400; opacity: 0.6; }
.cal-tglabel em { font-style: normal; font-weight: 400; opacity: 0.4; }

/* A proposal. Faint enough that the eye sorts agreed from proposed before
   reading a word of either — and the title fades WITH the row rather than
   holding full weight inside a ghost, because a bold dark line in a dashed box
   reads as a real task with an odd border, which is the opposite of what the
   border is for. */
.cal-sug {
  display: flex; align-items: center; gap: 8px; min-height: 40px;
  padding: 6px 10px; border-radius: 12px;
  background: transparent; border: 1px dashed rgba(0, 0, 0, 0.18);
  opacity: 0.55;
  transition: opacity 300ms ease, transform 300ms cubic-bezier(.2, .9, .3, 1);
}
.cal-sugtxt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.cal-sugtitle {
  font-size: 12px; font-weight: 600; color: #33302c;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Why it is being suggested, and for what. Without the event a prep task is an
   instruction from nowhere. */
.cal-sugwhy {
  font-size: 10px; color: #8a8070;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-sugadd {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 999px;
  background: rgba(217, 119, 6, 0.12); color: #c2762a;
  font-size: 14px; line-height: 1;
}

/* Accepting is a crossfade in ONE grid cell, not an insert. Both live in the
   same cell, so the band holds its shape and nothing below it moves. */
.cal-swap { display: grid; grid-template-columns: minmax(0, 1fr); }
.cal-swap > * { grid-area: 1 / 1; }
.cal-swap .cal-task {
  opacity: 0; transform: scale(0.96);
  transition: opacity 300ms ease, transform 300ms cubic-bezier(.2, .9, .3, 1);
}
.cal-swap.is-done .cal-task { opacity: 1; transform: none; }
.cal-swap.is-done .cal-sug { opacity: 0; transform: scale(0.96); }
.cal-sug.is-press .cal-sugadd { animation: ct-press .34s ease; }

/* --- chapter 3 camera ---------------------------------------------------- */
/* Landmarks (tab space):
 *   suggestions OFF  tab 1163 · tasks card 633-967 · every band 60 tall
 *   suggestions ON   tasks card 633-1127 · maya 678-790 · daniel 802-914
 *   header 646-666 · the Suggestions control x 1130-1247
 *   task columns  col1 37-436 · col2 442-841 · col3 847-1247
 *   the proposal that gets accepted: Maya's, col1 row 2
 *
 * One journey in, and no journey back out until the end: down to the widget,
 * across to the control, then a straight PAN LEFT at the same distance. The
 * pan is what makes the reveal land — the camera arrives where the proposals
 * are about to appear, so they arrive in front of it rather than being found
 * afterwards.
 *
 * z=3.109 is 1284/413, and 413 is what it takes to hold ONE task column whole:
 * the control shot sits on col3 (847-1260, its right edge on the card's own),
 * the pan ends on col1 (29-442, its right edge in the col1/col2 gutter).
 * Neither cuts a pill. At that distance a 12px task title renders at 16px and
 * the reason under it at 13px.
 *
 * Both ends share ty=-633 — the card's own top edge — so the pan is horizontal
 * and nothing rises or falls under the reader while it happens. */
@keyframes cal-tasks {
  0%, 4.67%       { --z: 1;     --tx: 0;    --ty: 0; }     /* the tab, suggestions off */
  12%, 17.33%     { --z: 1.572; --tx: -24;  --ty: -601; }  /* down onto the list */
  25.33%, 33.33%  { --z: 3.109; --tx: -847; --ty: -633; }  /* in on the Suggestions switch */
  40.67%, 86%     { --z: 3.109; --tx: -29;  --ty: -633; }  /* pan left — then they arrive */
  94%, 100%       { --z: 1;     --tx: 0;    --ty: 0; }     /* out */
}
.ct-stage[data-track="tasks"] .ct-cam { animation: cal-tasks 15s infinite; }

/* With motion off: the list as it stands, proposals still proposals. The
   resting frame is the offer, not the result — a still of one already accepted
   would be a section about a task list rather than about where it came from. */
@media (prefers-reduced-motion: reduce) {
  [data-track="tasks"] .cal-sug,
  [data-track="tasks"] .cal-task { transition: none !important; animation: none !important; }
}

/* ── suggestions switched off ───────────────────────────────────────────── */
/* The widget builds them either way — they are local and free — but with the
   switch off `group.suggestions` is empty, so the rows are not rendered and
   neither is the band's "+n". The HEADER count stays: switched off it is the
   offer, and a control that only admits to them once they are already on stage
   has nothing to say to the family who turned it off last week.
   `display: none`, not opacity: the cells have to leave the grid, because the
   bands really do shrink and the card really does get shorter. */
.is-sugoff .cal-sug,
.is-sugoff .cal-swap { display: none; }
.is-sugoff .cal-tglabel em { display: none; }
