/* =====================================================================
   THE ILLUSTRATIONS IN MOTION (home page and Council notes)
   Launch rebuild, Sept. 2026: page layout lives in site.css; this file only moves pictures.
   Default styles are the FINISHED state. Only html.moving (set by motion.js when scripts
   run, the page asks for motion, and reduced motion is off) holds things back so they can
   arrive. Every effect plays once by itself; a finished drawing can be replayed with its
   small replay button (see the end of this file). No text waits on anything here.
   Blocks between their own markers are written by scripts in build/ (appscene_css.py,
   treescene_css.py, hydrantscene_css.py, build.py); edit those scripts, not the blocks.
   ===================================================================== */

/* ---------- Stagger indexes (written by build.py) ---------- */
[data-i="0"] { --i: 0; }
[data-i="1"] { --i: 1; }
[data-i="2"] { --i: 2; }
[data-i="3"] { --i: 3; }
[data-i="4"] { --i: 4; }
[data-i="5"] { --i: 5; }
[data-i="6"] { --i: 6; }
[data-i="7"] { --i: 7; }
[data-i="9"] { --i: 9; }
[data-i="10"] { --i: 10; }
[data-i="11"] { --i: 11; }
[data-i="12"] { --i: 12; }
[data-i="13"] { --i: 13; }
[data-i="14"] { --i: 14; }
[data-i="15"] { --i: 15; }
[data-i="16"] { --i: 16; }
[data-i="17"] { --i: 17; }
[data-i="18"] { --i: 18; }
[data-i="19"] { --i: 19; }
[data-i="20"] { --i: 20; }
[data-i="21"] { --i: 21; }
/* ---------- end: stagger indexes ---------- */

/* ---------- Scene: windows lighting ----------
   The dusk street is a scene like the others: it waits, dark, until it's on screen. On desktop
   the home page's street sits below the fold, so on load it would light up unseen. */
.street .win-off { opacity: 0; }
html.moving [data-scene] .street-dusk .win-off { opacity: 1; }
html.moving [data-scene] .street-dusk :is(.win, .moon, .lamp-glow, .lamp-bulb, .t-light, .t-flood) { opacity: 0; }
html.moving [data-scene].in .street-dusk .win-off { animation: lamp-off 0.01s both; animation-delay: calc(900ms + var(--i, 0) * 110ms); }
html.moving [data-scene].in .street-dusk .win { animation: lamp-on 0.9s both; animation-delay: calc(900ms + var(--i, 0) * 110ms); }
html.moving [data-scene].in .street-dusk .moon { animation: fade-in 1.6s 0.2s both; }
/* Street lamps come on first, then the houses. */
html.moving [data-scene].in .street-dusk :is(.lamp-glow, .lamp-bulb) { animation: lamp-on 0.8s both; animation-delay: calc(350ms + var(--i, 0) * 180ms); }
html.moving [data-scene].in .street-dusk .lamp-glow { animation-name: glow-on; }
/* the water tower's floodlights come on with the street lamps, and its beacon just after */
html.moving [data-scene].in .street-dusk .t-flood { animation: lamp-on 0.9s both 0.7s; }
html.moving [data-scene].in .street-dusk .t-light { animation: lamp-on 0.8s both 0.95s; }
/* The sky deepens as the lamps come on: it starts with the last of the daylight low over the
   pines (a lighter green, a trace of gold at the horizon) and settles into the page's own
   evening green. The glow sits behind the drawing and below the headline; finished = no glow,
   the same as without motion. */
.skyline-dusk { position: relative; }
.skyline-dusk .street { position: relative; }
.skyline-dusk::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(180deg, rgba(47, 90, 69, 0) 30%, rgba(47, 90, 69, 0.75) 62%, rgba(92, 110, 62, 0.8) 76%, rgba(239, 181, 87, 0.28) 86%, rgba(239, 181, 87, 0) 94%); }
html.moving .skyline-dusk[data-scene]::before { opacity: 1; }
html.moving .skyline-dusk[data-scene].in::before { animation: dusk-deepens 2.8s ease-in-out 0.15s both; }
@keyframes dusk-deepens { from { opacity: 1; } to { opacity: 0; } }
@keyframes glow-on { from { opacity: 0; } to { opacity: 0.16; } }
@keyframes lamp-on { 0% { opacity: 0; } 20% { opacity: 0.85; } 32% { opacity: 0.35; } 55%, 100% { opacity: 1; } }
@keyframes lamp-off { from { opacity: 1; } to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

html.moving .street-night .win-off { opacity: 1; transition: opacity 0.01s; transition-delay: calc(250ms + var(--i, 0) * 55ms); }
html.moving .street-night .win { opacity: 0; transition: opacity 0.7s var(--ease); transition-delay: calc(250ms + var(--i, 0) * 55ms); }
html.moving [data-scene].in .street-night .win-off { opacity: 0; }
html.moving [data-scene].in .street-night .win { opacity: 1; }
/* the water tower's floodlights flicker on in the middle of the window wave */
html.moving .street-night .t-flood { opacity: 0; }
html.moving [data-scene].in .street-night .t-flood { animation: lamp-on 0.9s both 0.85s; }

/* ---------- Dawn: the neighborhood gets its color back, one sign at a time ----------
   Before: a flat grey pre-dawn street with the moon still up.
   Then the moon sets, the sun rises, and each house regains its color the moment its
   sign goes up (--d is shared by a house and its sign). Default = finished, sunny street. */
.street-day .moon { opacity: 0; }
.street-day .ysign, .street-day .sun, .street-day .moon { transform-box: fill-box; }
.street-day .ysign { transform-origin: 50% 100%; }

html.moving [data-scene] .signs-street { background-color: #4b5651; transition: background-color 1.4s ease 0.1s; }
html.moving [data-scene].in .signs-street { background-color: var(--paper); }

/* moon sets */
html.moving [data-scene] .street-day .moon { opacity: 1; transition: transform 1.6s cubic-bezier(0.5, 0, 0.8, 0.6), opacity 1.2s ease 0.3s; }
html.moving [data-scene].in .street-day .moon { opacity: 0; transform: translateY(80px); }
/* sun rises from behind the pines */
html.moving [data-scene] .street-day .sun { transform: translateY(120px); transition: transform 1.7s cubic-bezier(0.25, 0.7, 0.3, 1) 0.1s; }
html.moving [data-scene].in .street-day .sun { transform: none; }

/* the washed-out "before" palette */
html.moving [data-scene]:not(.in) .street-day .h-body { fill: #a3a7a1; stroke: #7c827e; }
html.moving [data-scene]:not(.in) .street-day .h-roof { fill: #6f7773; }
html.moving [data-scene]:not(.in) .street-day .h-door { fill: #666e6a; }
html.moving [data-scene]:not(.in) .street-day .h-win { fill: #858b87; stroke: #7c827e; }
html.moving [data-scene]:not(.in) .street-day .tree { fill: #66706b; }
html.moving [data-scene]:not(.in) .street-day :is(.h-awn-a, .h-sign) { fill: #8d928e; }
html.moving [data-scene]:not(.in) .street-day :is(.h-awn-b, .h-letters) { fill: #b3b6b1; }
html.moving [data-scene]:not(.in) .street-day .ridge { fill: #5c6661; }
html.moving [data-scene]:not(.in) .street-day .ground { fill: #59625d; }
html.moving [data-scene]:not(.in) .street-day .roadline { stroke: #8d928e; }
html.moving [data-scene]:not(.in) .street-day .t-body { fill: #a3a7a1; stroke: #7c827e; }
html.moving [data-scene]:not(.in) .street-day .t-strut { stroke: #6f7773; }
html.moving [data-scene]:not(.in) .street-day :is(.t-trim, .t-letters) { fill: #6f7773; }
/* Before sunrise the tower is floodlit; the floodlights switch off as the moon starts to set. Its
   beacon stays lit through the day. Finished = floodlights off (the drawing's own opacity 0). */
html.moving [data-scene]:not(.in) .street-day .t-flood { opacity: 1; }
html.moving [data-scene] .street-day .t-flood { transition: opacity 0.35s ease 0.45s; }

/* The water tower catches the first light: the tallest thing in town, its tank warms to gold a
   beat before the sun clears the pines, then settles to cream; the houses follow as their signs
   go up. Finished = the tower's own colors, the same as without motion. */
html.moving [data-scene].in .street-day .t-body { animation: first-light 1.5s ease 0.2s both; }
html.moving [data-scene] .street-day :is(.t-strut, .t-trim, .t-letters) { transition: fill 0.8s ease 0.3s, stroke 0.8s ease 0.3s; }
@keyframes first-light {
  from { fill: #a3a7a1; stroke: #7c827e; }
  40%  { fill: #f3d59a; stroke: #173528; }
  to   { fill: #f6f1e8; stroke: #173528; }
}
/* A sunrise glow passes through the sky while the grey gives way to morning: gold low over the
   pines, warm cream above, then gone as the day settles in. Finished = no glow. */
.signs-street { position: relative; }
.signs-street .street { position: relative; }
.signs-street::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(180deg, rgba(247, 230, 194, 0) 10%, rgba(247, 230, 194, 0.7) 40%, rgba(239, 196, 110, 0.85) 62%, rgba(239, 181, 87, 0.5) 74%, rgba(239, 181, 87, 0) 86%); }
html.moving [data-scene].in .signs-street::before { animation: sunrise-glow 3.2s ease-in-out 0.15s both; }
@keyframes sunrise-glow { from { opacity: 0; } 35% { opacity: 1; } 55% { opacity: 1; } to { opacity: 0; } }

/* houses: color returns with their own sign */
html.moving [data-scene] .street-day .house :is(.h-body, .h-roof, .h-door, .h-win, .h-awn-a, .h-awn-b, .h-sign, .h-letters) { transition: fill 0.9s ease, stroke 0.9s ease; transition-delay: var(--d, 1s); }
/* trees come back with their nearest house; the ground and far pines warm up slowly
   across the whole sequence, so the neighborhood fills in as the signs go up */
html.moving [data-scene] .street-day .tree { transition: fill 1s ease; transition-delay: var(--d, 1.5s); }
html.moving [data-scene] .street-day :is(.ridge, .ground) { transition: fill 2.4s ease 0.6s; }
html.moving [data-scene] .street-day .roadline { transition: stroke 2.4s ease 0.6s; }

/* signs pop up */
html.moving .street-day .ysign { transform: scaleY(0); opacity: 0; transition: transform 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.3), opacity 0.2s; transition-delay: var(--d, 0ms); }
html.moving [data-scene].in .street-day .ysign { transform: none; opacity: 1; }

/* the headline keeps time with the first three signs. It is readable the moment you land (a
   sign link lands right here): it waits in a softer ink (6.6:1 on paper) and each line darkens to
   pine as its sign goes up. Finished = pine, the same as without motion. */
html.moving [data-scene] .rhythm > span { color: var(--ink-soft); transition: color 0.5s var(--ease); }
html.moving [data-scene].in .rhythm > span { color: var(--pine-deep); }
html.moving [data-scene].in .rhythm > span:nth-child(1) { transition-delay: 0.35s; }
html.moving [data-scene].in .rhythm > span:nth-child(2) { transition-delay: 0.9s; }
html.moving [data-scene].in .rhythm > span:nth-child(3) { transition-delay: 1.45s; }
html.moving [data-scene].in .rhythm > span:nth-child(4) { transition-delay: 2s; }

/* ---------- Calendar: circling the 3rd ---------- */
.calendar .circle-draw { stroke-dasharray: 100; stroke-dashoffset: 0; }
html.moving .calendar[data-scene] .circle-draw { stroke-dashoffset: 100; transition: stroke-dashoffset 1.1s 0.4s cubic-bezier(0.6, 0, 0.3, 1); }
html.moving .calendar[data-scene].in .circle-draw { stroke-dashoffset: 0; }

/* =====================================================================
   COUNCIL SCENES: plain pictures of what came up at council meetings (Council notes).
   Sept. 24: each scene is four beats and is done in under five seconds (so no Stop control is
   needed), with no gags; the words live in the numbered caption under the drawing, and the
   drawing's numbered markers match it. Default = the finished picture; html.moving plays it once
   when it scrolls into view. Layout lives in site.css.
   ===================================================================== */
/* ---------- Council scene captions ----------
   Every caption line always shows (layout in site.css). While a scene plays, the line it is on
   is marked and the rest step back a shade (still full contrast); when it is done, every line
   looks the same again, exactly like the page without motion. */
html.moving .beats li { color: var(--ink-soft); transition: color 0.3s var(--ease); }
html.moving .beats li::before { transition: background-color 0.3s var(--ease), color 0.3s var(--ease); }
html.moving .beats li.now:not(.gone) { color: var(--ink); }
html.moving .beats li.now:not(.gone)::before { background: var(--amber); color: var(--pine-deep); }
html.moving .beats.done li, html.moving .beats.done li.now:not(.gone) { color: var(--ink); }
html.moving .beats.done li::before, html.moving .beats.done li.now:not(.gone)::before { background: var(--pine); color: var(--paper); }
/* ---------- end: council scene captions ---------- */
/* ---------- The dandelion and the app ----------
   The loop the council heard about on Sept. 16, told plainly beside a case log (times are
   illustrative). A dandelion pops up out of the tall grass in front of the house and a report
   comes in with its photo; the owner mows (no one is shown); staff visit and close the case in
   compliance, with a closing photo of the mowed lawn; a nearly identical report, the same
   dandelion photo, lands on top of the first. Four beats, done by 4.5s. The seconds
   are the data-at values of the case log in build/pages/council.html; appscene_css.py reads them. */

/* the case log (real text, readable on any screen) */
.scene-row { display: grid; grid-template-columns: minmax(14rem, 21.25rem) minmax(0, 1fr); gap: clamp(1rem, 3vw, 2rem); align-items: center; }
@media (max-width: 48rem) { .scene-row { grid-template-columns: 1fr; } .scene-row .app { order: 2; max-width: 22rem; justify-self: center; width: 100%; } }
.app { background: var(--pine-deep); border-radius: 1.6rem; padding: 0.7rem; box-shadow: var(--shadow-md); font-family: var(--sans); }
.app-screen { background: var(--white); border-radius: 1rem; overflow: hidden; }
.app-head { margin: 0; background: var(--amber); color: var(--pine-deep); font: 800 0.9rem/1 var(--sans); letter-spacing: 0.06em; text-transform: uppercase; padding: 0.75rem 0.9rem; }
.app-case { padding: 0.7rem 0.9rem 0.35rem; }
.app-case b { display: block; font: 800 1.05rem/1.25 var(--sans); color: var(--ink); }
.app-illus { font: 500 0.82rem/1.3 var(--sans); color: var(--ink-soft); }
.app-status { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; padding: 0.2rem 0.9rem 0.6rem; border-bottom: 1px solid var(--rule); min-height: 2.4rem; }
.chip { display: inline-flex; align-items: center; justify-content: center; font: 800 0.82rem/1 var(--sans); letter-spacing: 0.04em; text-transform: uppercase; padding: 0.35rem 0.55rem; border-radius: 999px; }
.chip-open { background: var(--paper-deep); color: var(--pine-deep); border: 1.5px solid var(--pine); }
.chip-closed { background: var(--pine); color: var(--white); }
.chip-alert { background: var(--amber); color: var(--pine-deep); }
.trips { font: 700 0.85rem/1.2 var(--sans); color: var(--ink-soft); }
.app-log { list-style: none; margin: 0; padding: 0.6rem 0.7rem 0.8rem; display: flex; flex-direction: column; gap: 0.4rem; max-height: 19.5rem; overflow: hidden; -webkit-mask-image: linear-gradient(#000 80%, transparent); mask-image: linear-gradient(#000 80%, transparent); }
.app-log li { background: var(--paper); border: 1px solid var(--rule); border-left: 4px solid var(--pine-mid); border-radius: 0.45rem; padding: 0.45rem 0.6rem; }
.app-log li.ok { border-left-color: var(--pine); }
.app-log li.alert { border-left-color: var(--amber); background: #fbf0dc; }
.app-log b { display: block; font: 800 0.95rem/1.25 var(--sans); color: var(--ink); }
.app-log span { font: 500 0.84rem/1.3 var(--sans); color: var(--ink-soft); }
.app-log em { display: block; font: italic 600 0.84rem/1.3 var(--sans); color: var(--pine); margin-top: 0.15rem; }
/* without motion the whole log shows, down to the first report */
html:not(.moving) .app-log { max-height: none; -webkit-mask-image: none; mask-image: none; }
/* finished state: the status row is the case's whole history (OPEN -> CLOSED -> REOPENED) and
   the final count, so every word is there without motion. With motion the chips replay one at
   a time and only the number of trips ticks up. */
.app .trips .t { display: none; }
html:not(.moving) .app-status { gap: 0.45rem 1.15rem; }
html:not(.moving) .app-status .chip { position: relative; }
/* one width for OPEN, CLOSED and REOPENED, so the staff-trips count does not slide as the chip changes */
html.moving .app-status .chip { min-width: 7.4em; }
html:not(.moving) .app-status .chip + .chip::before { content: "→"; position: absolute; left: -0.95rem; color: var(--ink-soft); font-weight: 700; letter-spacing: 0; }
html.moving .app .t { display: none; }
/* after playback, the log keeps its size and scrolls, so every entry can be read */
html.moving .app.done .app-log { overflow-y: auto; -webkit-mask-image: none; mask-image: none; }
html.moving .app [data-at].wait { display: none; }
html.moving .app .t.now:not(.gone) { display: inline-flex; }
html.moving .app .t.now.gone { display: none; }
/* while it plays, the log keeps its full height, so the page below it never jumps */
html.moving .app-log { height: 19.5rem; }
html.moving .app li.now { animation: log-in 0.35s var(--ease) both; }
@keyframes log-in { from { opacity: 0; transform: translateY(-0.4rem); } to { opacity: 1; transform: none; } }
html.moving .app .chip.now { animation: chip-in 0.3s var(--ease) both; }
@keyframes chip-in { from { transform: scale(0.6); } to { transform: none; } }

/* the yard. Finished state (and no motion): the two report photos of the dandelion stacked,
   the closing photo of the mowed lawn beside them, the yard mowed, the truck gone. */
.appyard :is(.patch, .pop) { transform-box: fill-box; transform-origin: 50% 100%; }
.appyard .patch { opacity: 0; transform: scaleY(0.35); }
.appyard .truck-go { transform: translateX(-624px); }

/* before it plays: the long grass (the dandelion not up yet), no cards, the truck off to the right */
html.moving [data-scene] .appyard .patch { opacity: 1; transform: none; }
html.moving [data-scene] .appyard .pop { transform: scale(0); }
html.moving [data-scene] .appyard :is(.card-report, .card-closed) { opacity: 0; transform: translateY(-8px); }
html.moving [data-scene] .appyard .card-again { opacity: 0; transform: translate(-4px, -8px); }
html.moving [data-scene] .appyard .truck { transform: translateX(178px); }
html.moving [data-scene] .appyard .truck-go { transform: none; }

/* Once it plays, each beat is a keyframe animation that holds its first frame through its delay
   (fill: backwards), so the yard never depends on the browser having drawn the "before" first.
   When a beat ends, its element simply has the finished style set here, which is the same as the
   finished state above, so nothing is left on its own layer and the last frame matches the
   no-motion one exactly. */
/* 1. the dandelion pops up out of the tall grass, and a report comes in with its photo */
html.moving [data-scene].in .appyard .pop { transform: none; animation: yard-pop 0.6s cubic-bezier(0.34, 1.45, 0.6, 1) 0s backwards; }
html.moving [data-scene].in .appyard .card-report { opacity: 1; transform: none; animation: yard-card 0.5s var(--ease) 0.4s backwards; }
/* 2. the owner mows before anyone visits (no one is shown); staff visit: the truck pulls up */
html.moving [data-scene].in .appyard .patch { opacity: 0; transform: scaleY(0.35); animation: yard-mow 0.45s var(--ease) 1.5s backwards; }
html.moving [data-scene].in .appyard .truck { transform: none; animation: yard-arrive 0.7s var(--ease) 2s backwards; }
/* 3. closed in compliance, with a closing photo; the truck drives on and clears the frame */
html.moving [data-scene].in .appyard .card-closed { opacity: 1; transform: none; animation: yard-card 0.5s var(--ease) 2.8s backwards; }
html.moving [data-scene].in .appyard .truck-go { transform: translateX(-624px); animation: yard-leave 0.95s cubic-bezier(0.4, 0, 0.8, 0.6) 3.05s backwards; }
/* 4. a nearly identical report (the same dandelion photo) lands on top of the first */
html.moving [data-scene].in .appyard .card-again { opacity: 1; transform: none; animation: yard-again 0.5s var(--ease) 4s backwards; }
@keyframes yard-pop { from { transform: scale(0); } to { transform: none; } }
@keyframes yard-card { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes yard-again { from { opacity: 0; transform: translate(-4px, -8px); } to { opacity: 1; transform: none; } }
@keyframes yard-mow { from { opacity: 1; transform: none; } to { opacity: 0; transform: scaleY(0.35); } }
@keyframes yard-arrive { from { transform: translateX(178px); } to { transform: none; } }
@keyframes yard-leave { from { transform: none; } to { transform: translateX(-624px); } }
/* ---------- end: the app ---------- */
/* ---------- Saturday at Cabina: a table set for two by the front window ----------
   Written by build/cafe_scene.py. The finished drawing is the resting state (no script, reduced
   motion, printing). With motion, one thing happens, once: steam rises from the two mugs, mine
   first and the other a beat later (done in about 2.2 s). */
.cafe .cf-steam path { stroke-dasharray: 100; stroke-dashoffset: 0; }
html.moving [data-scene] .cafe .cf-steam path { stroke-dashoffset: 100; opacity: 0; }
html.moving [data-scene].in .cafe .cf-steam path { animation: cf-rise 1.3s ease-out 0.3s both; }
html.moving [data-scene].in .cafe .cf-steam path + path { animation-delay: 0.5s; }
html.moving [data-scene].in .cafe .cf-steam-2 path { animation-delay: 0.7s; }
html.moving [data-scene].in .cafe .cf-steam-2 path + path { animation-delay: 0.9s; }
@keyframes cf-rise { from { stroke-dashoffset: 100; opacity: 0; } 10% { opacity: 0.5; } to { stroke-dashoffset: 0; opacity: 0.5; } }
/* ---------- end: Saturday at Cabina ---------- */
/* ---------- The forum: Pine City High School auditorium ----------
   0.2s curtains part -> 1.0s spotlight -> 1.4s neighbors fill the seats
   3.0s the livestream camera goes LIVE -> 3.2s the clock ticks to 7:05, done by 4.6s */
.forum :is(.curtain, .person, .live, .hand-hour, .hand-min) { transform-box: fill-box; }
.forum .curtain-l { transform-origin: 0 50%; transform: scaleX(0.12); }
.forum .curtain-r { transform-origin: 100% 50%; transform: scaleX(0.12); }
.forum :is(.hand-hour, .hand-min) { transform-box: view-box; transform-origin: 672px 96px; }
.forum .hand-hour { transform: rotate(212.5deg); }
.forum .hand-min { transform: rotate(30deg); }

html.moving [data-scene] .forum .curtain { transform: none; }
html.moving [data-scene].in .forum .curtain { animation: curtain-open 1.5s cubic-bezier(0.6, 0, 0.3, 1) 0.2s both; }
@keyframes curtain-open { from { transform: none; } to { transform: scaleX(0.12); } }
html.moving [data-scene] .forum .spot { opacity: 0; }
html.moving [data-scene].in .forum .spot { animation: fade-in 1.2s ease 1s both; }
html.moving [data-scene] .forum .person { opacity: 0; transform: translateY(10px); }
html.moving [data-scene].in .forum .person { animation: take-seat 0.4s var(--ease) both; animation-delay: calc(1.4s + var(--i, 0) * 0.1s); }
@keyframes take-seat { to { opacity: 1; transform: none; } }
html.moving [data-scene] .forum .live { opacity: 0; }
html.moving [data-scene].in .forum .live { animation: fade-in 0.3s ease 3s both; }
html.moving [data-scene] .forum .hand-min { transform: rotate(-60deg); }
html.moving [data-scene] .forum .hand-hour { transform: rotate(205deg); }
html.moving [data-scene].in .forum .hand-min { animation: min-hand 1.4s cubic-bezier(0.4, 0, 0.2, 1) 3.2s both; }
html.moving [data-scene].in .forum .hand-hour { animation: hour-hand 1.4s cubic-bezier(0.4, 0, 0.2, 1) 3.2s both; }
@keyframes min-hand { from { transform: rotate(-60deg); } to { transform: rotate(30deg); } }
@keyframes hour-hand { from { transform: rotate(205deg); } to { transform: rotate(212.5deg); } }

/* ---------- How close is too close? (trees near the curb) ----------
   One block seen from the street: a shop, two houses, four trees on the boulevard and one well back
   in a yard. Four beats on the caption's clock (its data-at values: 0, 1.1, 2.3, 3.4 s), done by
   3.9 s: (1) the block; (2) a dimension from the curb back to a dashed line fades in with
   "10 ft", and the line is drawn down the block about ten feet back; (3) an amber tint fills that
   strip and a ring appears on the ground at the base of each street tree in it; (4) a plain folder
   settles in the corner (the Planning Commission will look into it). A question, not a verdict: no
   tree is marked for removal.
   Written by build/treescene_css.py; edit that script, not this block. */
.treeblock :is(.tr-mark, .tr-dash, .tr-tint, .tr-ring, .tr-folder) { transform-box: fill-box; }
.treeblock :is(.tr-mark, .tr-ring) { transform-origin: 50% 50%; }
.treeblock .tr-tint { transform-origin: 50% 100%; }
.treeblock .tr-dash { transform-origin: 0% 50%; }

/* before it plays (the default, above, is the finished picture) */
html.moving [data-scene] .treeblock :is(.tr-mark, .tr-ring) { opacity: 0; transform: scale(0.6); }
html.moving [data-scene] .treeblock :is(.tr-dim, .tr-ft) { opacity: 0; }
html.moving [data-scene] .treeblock .tr-dash { transform: scaleX(0); }
html.moving [data-scene] .treeblock .tr-tint { opacity: 0; transform: scaleY(0); }
html.moving [data-scene] .treeblock .tr-folder { opacity: 0; transform: translateY(-14px); }

/* it plays */
html.moving [data-scene].in .treeblock :is(.tr-mark, .tr-ring, .tr-dash, .tr-tint, .tr-folder) { transform: none; }
html.moving [data-scene].in .treeblock :is(.tr-mark, .tr-ring, .tr-dim, .tr-ft, .tr-tint, .tr-folder) { opacity: 1; }
html.moving [data-scene].in .treeblock .tr-m1 { transition: opacity 0.35s var(--ease) 0.1s, transform 0.35s var(--ease) 0.1s; }
/* beat 2: the question, ten feet back from the curb */
html.moving [data-scene].in .treeblock .tr-m2 { transition: opacity 0.35s var(--ease) 1.1s, transform 0.35s var(--ease) 1.1s; }
html.moving [data-scene].in .treeblock .tr-dim { transition: opacity 0.4s var(--ease) 1.1s; }
html.moving [data-scene].in .treeblock .tr-ft { transition: opacity 0.3s var(--ease) 1.35s; }
html.moving [data-scene].in .treeblock .tr-dash { transition: transform 0.03s linear calc(1.5s + var(--i, 0) * 0.0158s); }
/* beat 3: the strip, and the trees standing in it */
html.moving [data-scene].in .treeblock .tr-m3 { transition: opacity 0.35s var(--ease) 2.3s, transform 0.35s var(--ease) 2.3s; }
html.moving [data-scene].in .treeblock .tr-tint { transition: opacity 0.3s var(--ease) 2.3s, transform 0.5s var(--ease) 2.3s; }
html.moving [data-scene].in .treeblock .tr-r1 { transition: opacity 0.35s var(--ease) 2.6s, transform 0.35s var(--ease) 2.6s; }
html.moving [data-scene].in .treeblock .tr-r2 { transition: opacity 0.35s var(--ease) 2.74s, transform 0.35s var(--ease) 2.74s; }
html.moving [data-scene].in .treeblock .tr-r3 { transition: opacity 0.35s var(--ease) 2.88s, transform 0.35s var(--ease) 2.88s; }
html.moving [data-scene].in .treeblock .tr-r4 { transition: opacity 0.35s var(--ease) 3.02s, transform 0.35s var(--ease) 3.02s; }
/* beat 4: the question goes to the Planning Commission */
html.moving [data-scene].in .treeblock .tr-folder { transition: opacity 0.45s var(--ease) 3.4s, transform 0.45s var(--ease) 3.4s; }
html.moving [data-scene].in .treeblock .tr-m4 { transition: opacity 0.35s var(--ease) 3.5s, transform 0.35s var(--ease) 3.5s; }
/* ---------- end: trees near the curb ---------- */
/* ---------- Broken hydrants and next year's budget ----------
   The can that keeps rolling down the road: a hydrant tagged out of order and a tin can
   rolling past it; this year's parts on order; the 2027 budget page, and the can stops against
   its hydrant line (Sept. 16; treatment A); the final vote in December. The hydrant stays
   tagged: nothing is fixed yet. Written by build/hydrantscene_css.py from the caption's data-at
   values in pages/council.html (0s, 1.1s, 2.3s, 3.4s); done by 3.9s. */
.hydrantscene :is(.hy-tag, .hy-cal, .hy-mark, .hy-line) { transform-box: fill-box; }
.hydrantscene .hy-tag { transform-origin: 100% 0%; }
.hydrantscene .hy-cal { transform-origin: 50% 50%; }
.hydrantscene .hy-line { transform-origin: 0% 50%; }
/* the can is drawn around its own center, so it turns about that point as it rolls */
.hydrantscene .hy-can { transform-box: view-box; transform-origin: 0 0; }
/* each marker grows out of (and shrinks toward) the thing it touches */
.hydrantscene :is(.hy-mark-1, .hy-mark-3) { transform-origin: 100% 50%; }
.hydrantscene .hy-mark-2 { transform-origin: 0% 50%; }
.hydrantscene .hy-mark-4 { transform-origin: 100% 50%; }
/* the markers are drawn at their phone size; where the drawing is wider they shrink so they
   stay small notes (about the caption's own markers). The separate scale property sits on top
   of the transform the beats use, so the finished frame is the same with or without motion. */
.scene-box:has(> .hydrantscene) { container-type: inline-size; }
@container (min-width: 440px) { .hydrantscene .hy-mark { scale: 0.8; } }
@container (min-width: 560px) { .hydrantscene .hy-mark { scale: 0.66; } }
@container (min-width: 760px) { .hydrantscene .hy-mark { scale: 0.52; } }

/* before it plays (without motion, none of this applies: the finished frame shows) */
html.moving [data-scene] .hydrantscene .hy-tag { opacity: 0; transform: translateY(-6px) rotate(7deg); }
html.moving [data-scene] .hydrantscene .hy-mark-1 { opacity: 0; transform: scale(0.6); }
html.moving [data-scene] .hydrantscene .hy-box { opacity: 0; transform: translateX(28px); }
html.moving [data-scene] .hydrantscene .hy-mark-2 { opacity: 0; transform: scale(0.6); }
html.moving [data-scene] .hydrantscene .hy-page { opacity: 0; transform: translateY(-180px); }
html.moving [data-scene] .hydrantscene .hy-line { opacity: 0; }
html.moving [data-scene] .hydrantscene .hy-mark-3 { opacity: 0; transform: scale(0.6); }
html.moving [data-scene] .hydrantscene .hy-cal { opacity: 0; transform: translateY(-14px) rotate(-3deg); }
html.moving [data-scene] .hydrantscene .hy-mark-4 { opacity: 0; transform: scale(0.6); }
html.moving [data-scene] .hydrantscene .hy-can { transform: translateX(-491.0px) rotate(-1080.0deg); }

/* the four beats, on the caption's clock */
html.moving [data-scene].in .hydrantscene :is(.hy-tag, .hy-mark-1, .hy-box, .hy-mark-2, .hy-page, .hy-line, .hy-mark-3, .hy-cal, .hy-mark-4, .hy-can) { opacity: 1; transform: none; }
html.moving [data-scene].in .hydrantscene .hy-tag { transition: opacity 0.35s var(--ease) 0.1s, transform 0.7s var(--ease) 0.1s; }
html.moving [data-scene].in .hydrantscene .hy-mark-1 { transition: opacity 0.3s var(--ease) 0.3s, transform 0.35s var(--ease) 0.3s; }
html.moving [data-scene].in .hydrantscene .hy-box { transition: opacity 0.4s var(--ease) 1.1s, transform 0.6s var(--ease) 1.1s; }
html.moving [data-scene].in .hydrantscene .hy-mark-2 { transition: opacity 0.3s var(--ease) 1.4s, transform 0.35s var(--ease) 1.4s; }
html.moving [data-scene].in .hydrantscene .hy-page { transition: opacity 0.25s var(--ease) 2.3s, transform 0.6s var(--ease) 2.3s; }
html.moving [data-scene].in .hydrantscene .hy-line { transition: opacity 0.4s var(--ease) 2.85s; }
html.moving [data-scene].in .hydrantscene .hy-mark-3 { transition: opacity 0.3s var(--ease) 2.55s, transform 0.35s var(--ease) 2.55s; }
html.moving [data-scene].in .hydrantscene .hy-cal { transition: opacity 0.35s var(--ease) 3.4s, transform 0.5s var(--ease) 3.4s; }
html.moving [data-scene].in .hydrantscene .hy-mark-4 { transition: opacity 0.3s var(--ease) 3.55s, transform 0.35s var(--ease) 3.55s; }
/* the can: rolled in from off the left edge (-491 units), 3 whole turns, touching the page at 2.8s */
html.moving [data-scene].in .hydrantscene .hy-can { animation: hy-can-roll 2.8s linear 0s both; }
@keyframes hy-can-roll {
  0% { transform: translateX(-491.0px) rotate(-1080.0deg); }
  3.57% { transform: translateX(-459.3px) rotate(-1010.3deg); }
  7.14% { transform: translateX(-428.8px) rotate(-943.1deg); }
  10.71% { transform: translateX(-399.4px) rotate(-878.5deg); }
  14.29% { transform: translateX(-371.1px) rotate(-816.3deg); }
  17.86% { transform: translateX(-344.0px) rotate(-756.6deg); }
  21.43% { transform: translateX(-317.9px) rotate(-699.3deg); }
  25% { transform: translateX(-293.0px) rotate(-644.5deg); }
  28.57% { transform: translateX(-269.2px) rotate(-592.0deg); }
  32.14% { transform: translateX(-246.4px) rotate(-542.0deg); }
  35.71% { transform: translateX(-224.7px) rotate(-494.3deg); }
  39.29% { transform: translateX(-204.1px) rotate(-448.9deg); }
  42.86% { transform: translateX(-184.5px) rotate(-405.9deg); }
  46.43% { transform: translateX(-166.0px) rotate(-365.1deg); }
  50% { transform: translateX(-148.5px) rotate(-326.5deg); }
  53.57% { transform: translateX(-131.9px) rotate(-290.2deg); }
  57.14% { transform: translateX(-116.4px) rotate(-256.1deg); }
  60.71% { transform: translateX(-101.9px) rotate(-224.1deg); }
  64.29% { transform: translateX(-88.3px) rotate(-194.2deg); }
  67.86% { transform: translateX(-75.6px) rotate(-166.4deg); }
  71.43% { transform: translateX(-63.9px) rotate(-140.6deg); }
  75% { transform: translateX(-53.1px) rotate(-116.8deg); }
  78.57% { transform: translateX(-43.2px) rotate(-94.9deg); }
  82.14% { transform: translateX(-34.1px) rotate(-74.9deg); }
  85.71% { transform: translateX(-25.8px) rotate(-56.7deg); }
  89.29% { transform: translateX(-18.3px) rotate(-40.3deg); }
  92.86% { transform: translateX(-11.6px) rotate(-25.4deg); }
  96.43% { transform: translateX(-5.5px) rotate(-12.1deg); }
  100% { transform: none; }
}
/* ---------- end: hydrants ---------- */
/* ---------- Logo: the O becomes the check ----------
   Approved Sept. 23 (the penciled-in check that came first was removed Sept. 25). The O fills in as the check disc
   (the name reads with the check as its O); then the disc hops (header) or rolls (sign) to
   its place and the plain O settles back. Resting styles are the finished logo, so without
   motion (or without the script) nothing changes. */
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.lg-x { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 3; pointer-events: none; opacity: 0; }
.lg-y { display: block; }
.lg-disc { display: block; width: var(--d1, 1px); height: var(--d1, 1px); margin: calc(var(--d1, 1px) / -2) 0 0 calc(var(--d1, 1px) / -2); overflow: visible; }
.lg-disc .fill { fill: var(--amber); transform-box: fill-box; transform-origin: 50% 50%; }
.lg-disc .tick { fill: none; stroke: var(--pine-deep); stroke-width: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 100; }
.wordmark[data-logo] { position: relative; }
:is(.wordmark, .sign-live) :is(.o, .sl-o) { display: inline-block; line-height: 1; }
:is(.wordmark, .sign-live) .base { display: inline-block; width: 0; height: 0; vertical-align: baseline; }

/* the live yard sign (browsers without container queries keep the picture) */
.sign-live { display: none; }
@supports (container-type: inline-size) {
  .sign-art .sign-img { display: none; }
  .sign-live { display: grid; place-items: center; position: relative; aspect-ratio: 4 / 3; background: #1b3a2c; color: var(--paper); container-type: inline-size; overflow: hidden; }
}
.sl-frame { position: absolute; inset: 4.2cqw; border: max(2px, 0.6cqw) solid var(--amber); }
.sl-lockup { position: relative; display: grid; justify-items: center; text-align: center; line-height: 1; }
.sl-vote-row { display: flex; align-items: center; gap: 3.4cqw; }
.sl-bar { width: 15cqw; height: max(2px, 0.8cqw); background: var(--amber); }
.sl-bar.l, .sl-rule.l { transform-origin: 100% 50%; }
.sl-bar.r, .sl-rule.r { transform-origin: 0% 50%; }
.sl-vote { font: 700 6.1cqw/1 var(--serif); letter-spacing: 0.12em; margin-right: -0.12em; }
.sl-first { margin-top: 2.2cqw; font: 700 6.3cqw/1 var(--serif); letter-spacing: 0.2em; margin-right: -0.2em; }
.sl-name { margin-top: 0.2cqw; font: 900 18.6cqw/0.9 var(--serif); letter-spacing: 0.005em; white-space: nowrap; }
.sl-row { display: flex; align-items: center; width: 81cqw; gap: 2.2cqw; margin-top: 1.4cqw; }
.sl-rule { flex: 1; height: max(2px, 0.95cqw); background: var(--amber); }
.sl-slot { flex: none; width: 8.2cqw; height: 8.2cqw; }
.sl-slot svg { display: block; width: 100%; height: 100%; }
.sl-slot .fill { fill: var(--amber); }
.sl-slot .tick { fill: none; stroke: var(--pine-deep); stroke-width: 11; stroke-linecap: round; stroke-linejoin: round; }
.sl-council { margin-top: 1.8cqw; font: 800 8.2cqw/1 var(--sans); font-stretch: 66%; letter-spacing: 0.03em; }
.sl-paid { margin-top: 1.7cqw; font: italic 500 1.75cqw/1.2 var(--sans); }

/* timings: when it's inked, when it leaves, how long the trip takes */
.wordmark[data-logo] { --t-ink: 0.9s; --t-go: 1.45s; --t-dur: 0.85s; --t-land: 2.3s; --move-x: lg-head-x; --move-y: lg-head-y; }
.sign-live { --t-ink: 1.28s; --t-go: 1.9s; --t-dur: 1s; --t-land: 2.9s; --move-x: lg-sign-x; --move-y: lg-sign-y; }

/* header: until the script starts it, hold the check back (this expires on its own) */
html.moving .wordmark[data-logo]:not(.play) .check { animation: lg-hold 1.5s linear; }
html.moving .wordmark[data-logo].play .check { animation: lg-hidden var(--t-land) linear backwards; }
html.moving .wordmark[data-logo].play .office { animation: lg-slide 0.5s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--t-go) + var(--t-dur) - 0.3s) backwards; }

/* the sign: waits, blank, until it scrolls into view; then sets in as printed, with a plain O */
html.moving .sign-live:not(.in) :is(.sl-vote-row, .sl-first, .sl-name, .sl-council, .sl-paid, .sl-slot) { opacity: 0; }
html.moving .sign-live:not(.in) :is(.sl-rule, .sl-bar) { transform: scaleX(0); }
html.moving .sign-live.in .sl-bar { animation: lg-draw-out 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.05s backwards; }
html.moving .sign-live.in :is(.sl-vote-row, .sl-first, .sl-name, .sl-council, .sl-paid) { animation: lg-set-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
html.moving .sign-live.in .sl-first { animation-delay: 0.06s; }
html.moving .sign-live.in .sl-name { animation-delay: 0.12s; }
html.moving .sign-live.in :is(.sl-council, .sl-paid) { animation-delay: 0.18s; }
html.moving .sign-live.in .sl-slot { animation: lg-hidden var(--t-land) linear backwards; }
html.moving .sign-live.in .sl-rule { animation: lg-draw-out 0.55s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--t-land) - 0.2s) backwards; }

/* both: the O becoming the disc, and the trip to its place */
html.moving :is(.wordmark[data-logo].play, .sign-live.in) .lg-x { animation: lg-shown var(--t-land) linear, var(--move-x) var(--t-dur) linear var(--t-go) backwards; }
html.moving :is(.wordmark[data-logo].play, .sign-live.in) .lg-y { animation: var(--move-y) var(--t-dur) linear var(--t-go) backwards; }
html.moving :is(.wordmark[data-logo].play, .sign-live.in) .lg-disc { animation: lg-roll var(--t-dur) cubic-bezier(0.62, 0, 0.3, 1) var(--t-go) backwards, lg-resize calc(var(--t-dur) * 0.62) cubic-bezier(0.65, 0, 0.35, 1) var(--t-go) backwards; }
html.moving :is(.wordmark[data-logo].play, .sign-live.in) .lg-disc .fill { animation: lg-fill 0.38s cubic-bezier(0.22, 1, 0.36, 1) var(--t-ink) backwards; }
html.moving :is(.wordmark[data-logo].play, .sign-live.in) .lg-disc .tick { animation: lg-ink 0.3s cubic-bezier(0.55, 0, 0.25, 1) calc(var(--t-ink) + 0.08s) backwards; }
html.moving :is(.wordmark[data-logo].play, .sign-live.in) :is(.o, .sl-o) { animation: lg-o-out 0.2s linear calc(var(--t-ink) + 0.05s) forwards, lg-o-back 0.45s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--t-go) + 0.12s) forwards; }

@keyframes lg-set-in { from { opacity: 0; transform: translateY(0.25em); } }
@keyframes lg-draw-out { from { transform: scaleX(0); } }
@keyframes lg-shown { from { opacity: 1; } to { opacity: 1; } }
@keyframes lg-hidden { from { opacity: 0; } to { opacity: 0; } }
@keyframes lg-hold { from { opacity: 0; } to { opacity: 0; } }
@keyframes lg-slide { from { opacity: 0; transform: translateX(-0.5rem); } }
@keyframes lg-fill { from { opacity: 0; transform: scale(0.45); } }
@keyframes lg-ink { from { stroke-dashoffset: 100; } }
@keyframes lg-o-out { to { opacity: 0; } }
@keyframes lg-o-back { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: none; } }
@keyframes lg-roll { from { rotate: 0deg; } to { rotate: 360deg; } }
@keyframes lg-resize { from { scale: var(--s0); } to { scale: 1; } }
/* the sign: it drops out of the word, rolls along under it, and bounces softly onto the rule line */
@keyframes lg-sign-x { from { transform: translateX(var(--x0)); animation-timing-function: cubic-bezier(0.62, 0, 0.3, 1); } to { transform: translateX(var(--x1)); } }
@keyframes lg-sign-y {
  0%   { transform: translateY(var(--y0)); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 0.4); }
  48%  { transform: translateY(var(--y1)); animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1); }
  66%  { transform: translateY(calc(var(--y1) - var(--d1) * 0.16)); animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0); }
  82%  { transform: translateY(var(--y1)); animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1); }
  91%  { transform: translateY(calc(var(--y1) - var(--d1) * 0.035)); animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0); }
  100% { transform: translateY(var(--y1)); }
}
/* the header: one arc up and over L-S-O-N */
@keyframes lg-head-x { from { transform: translateX(var(--x0)); animation-timing-function: cubic-bezier(0.62, 0, 0.3, 1); } to { transform: translateX(var(--x1)); } }
@keyframes lg-head-y {
  0%   { transform: translateY(var(--y0)); animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1); }
  47%  { transform: translateY(calc(var(--y0) - var(--d0) * 0.75)); animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0); }
  90%  { transform: translateY(var(--y1)); animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1); }
  95%  { transform: translateY(calc(var(--y1) - var(--d1) * 0.07)); animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0); }
  100% { transform: translateY(var(--y1)); }
}
/* ---------- end: logo ---------- */


/* ---------- Replay (Sept. 25) ----------
   story.js puts a small replay button in the corner of each finished drawing on the home page.
   It never shows while the drawing moves: with a mouse it fades in while the pointer is over a
   finished drawing, from the keyboard when it has focus, and on a phone for three seconds after
   a tap on the finished drawing (.replay-peek). A pine disc with an amber ring and arrow reads
   on the dusk, dawn and cream drawings alike. .replay-reset holds every animation and
   transition off for the moment the scene returns to its "before" (the yard sign inside the
   sunrise scene keeps playing, finished, on its own). */
.replay-host { position: relative; }
.replay {
  position: absolute; top: 0.6rem; right: 0.6rem; z-index: 3; display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; padding: 0; border-radius: 50%; cursor: pointer;
  background: rgba(15, 36, 26, 0.78); color: var(--amber); border: 1px solid rgba(239, 181, 87, 0.75);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  opacity: 0; pointer-events: none; transition: opacity 0.25s ease, background-color 0.2s ease;
}
.replay svg { width: 1.2rem; height: 1.2rem; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.replay-ready.replay-peek > .replay, .replay-ready > .replay:focus-visible { opacity: 1; pointer-events: auto; }
@media (hover: hover) {
  .replay-ready:hover > .replay { opacity: 1; pointer-events: auto; }
  .replay:hover { background: var(--pine-deep); }
}
@media (hover: none) { .replay { width: 2.75rem; height: 2.75rem; } }
.replay:focus-visible { outline: 3px solid var(--amber); outline-offset: 2px; }
/* the forum drawing has its clock in the top-right corner: the button takes the top-left */
.forum-art > .replay { right: auto; left: 0.6rem; }
/* the hero street rises behind the words above it on wide screens: keep the button below them */
@media (min-width: 52.01rem) { .hero .skyline > .replay { top: calc(7vw - 0.4rem); } }
html.moving .replay-reset, html.moving .replay-reset::before, html.moving .replay-reset::after,
html.moving .replay-reset :not(.sign-live, .sign-live *), html.moving .replay-reset :not(.sign-live, .sign-live *)::before,
html.moving .replay-reset :not(.sign-live, .sign-live *)::after { animation: none !important; transition: none !important; }
@media print { .replay { display: none; } }
/* ---------- end: replay ---------- */
