/* _ds_components.css — GENERATED by scripts/build-ds-bundle.mjs. DO NOT EDIT.
 * Concatenation of src/wc/css/*.css — per-element classes (token-based) that
 * extend global.css's kg-* utilities. Linked by ParkBase (live) + preview (#151). */

/* ── park-admin-actions.css ── */
/* park-admin-actions (anton/forrest#1248) — a small button cluster attached to
   one operator row or form, admin-shell#FR-7 ("one primary action per group
   with destructive actions styled as dangerous"). Lifted from .gm-moves/
   .gm-move/.gm-delete (per-row) and .gm-addform-actions/.gm-addform-cancel/
   .gm-addform-confirm (add-photo form) in GalleryManager — one `kind=`
   vocabulary now covers both call sites.

   Board vernacular (anton/forrest#1422, FR-24/FR-25): the controls sit on the
   board's card and take its ink, and the primary action wears the board's dark
   pill — the same form the public side's buttons took. The DESTRUCTIVE variant
   keeps `--c-admin-danger`: "this deletes something" is a distinction the
   operator must perceive, and repainting it green to match would be a
   regression, not consistency. */
park-admin-actions {
	display: inline-flex;
}
.pa-actions {
	display: flex;
	align-items: center;
	gap: 4px;
}
.pa-action {
	font-family: inherit;
	font-size: var(--fs-tiny);
	line-height: 1;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	color: var(--c-body);
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	cursor: pointer;
}
/* Compact icon square — the move-up/move-down/delete cluster. */
.pa-action--plain,
.pa-action--destructive {
	width: 28px;
	height: 28px;
	border-radius: 7px;
}
.pa-action--plain:hover {
	border-color: var(--c-board-title);
	color: var(--c-board-title);
}
.pa-action--destructive:hover {
	border-color: var(--c-admin-danger);
	color: var(--c-admin-danger);
}
/* Text pill — the cancel/confirm cluster. */
.pa-action--primary,
.pa-action--cancel {
	font-size: var(--fs-tiny);
	font-weight: var(--fw-semibold);
	border-radius: var(--r-pill);
	padding: 6px 14px;
}
.pa-action--primary {
	border-color: var(--c-board-header-bg);
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
}
.pa-action--primary:disabled {
	opacity: 0.6;
	cursor: default;
}
.pa-action:focus-visible {
	outline: 2px solid var(--c-board-title);
	outline-offset: 1px;
}


/* ── park-admin-caption.css ── */
/* park-admin-caption (anton/forrest#1248) — the labelled caption/alt-text
   editor, admin-shell#FR-8. Lifted 1:1 from .gm-caplabel/.gm-caphead/.gm-caption
   in GalleryManager (both the add-photo form and the per-row editor).

   Board vernacular (anton/forrest#1422, FR-24): the field label takes the
   board's shouted title treatment and the input sits on the board's card with
   its hairline — the same shape the public forms took in anton/forrest#1407. */
park-admin-caption {
	display: block;
}
.pa-caption {
	display: flex;
	flex-direction: column;
	gap: 4px;
}
.pa-caption-head {
	font-family: var(--ff-display);
	font-size: var(--fs-tiny);
	font-weight: var(--fw-bold);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--c-board-title);
}
.pa-caption-field {
	width: 100%;
	box-sizing: border-box;
	font-family: inherit;
	font-size: var(--fs-tiny);
	line-height: 1.45;
	color: var(--c-body);
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	border-radius: 8px;
	padding: 6px 8px;
	resize: vertical;
}
.pa-caption-field:focus-visible {
	outline: 2px solid var(--c-board-title);
	outline-offset: 1px;
	border-color: var(--c-board-title);
}


/* ── park-admin-empty.css ── */
/* park-admin-empty (anton/forrest#1021) — the directive (never blank) empty
   state, admin-shell#FR-7. Lifted from .gm-empty in GalleryManager.

   Board vernacular (anton/forrest#1422, FR-24): this text TELLS the operator
   what to do next, so it reads in body ink rather than the muted grey that
   fails AA on the board's cream (FR-15). */
park-admin-empty {
	display: block;
	margin-top: 24px;
	color: var(--c-body);
	font-size: var(--fs-tiny);
}

/* anton/forrest#1209 — the rule above beats the UA `[hidden] { display: none }`
   rule in the cascade (author origin always wins over UA origin, regardless
   of selector specificity), so `<park-admin-empty hidden>` — the element's
   documented normal "surface has content" state — stayed visible. Scope the
   override explicitly instead of relying on the UA default. */
park-admin-empty[hidden] {
	display: none;
}


/* ── park-admin-page-header.css ── */
/* <park-admin-page-header> — lifted 1:1 from AdminShell.astro's .adm-back /
   .adm-pagehead scoped styles (anton/forrest#1000). The page CHROME
   (.adm-body / .adm-wrap) stays in AdminShell: it is the document shell, not
   the header pattern. Token-only: no colour literals (check:brand-tokens).

   Board vernacular (anton/forrest#1422, FR-24): the operator's page title reads
   in the board's deepest green, its back link and lead in the board's palette —
   the same ink the public pages took in anton/forrest#1405/#1407. */

.pa-back {
	display: inline-block;
	font-size: var(--fs-tiny);
	font-weight: var(--fw-semibold);
	color: var(--c-board-title);
	text-decoration: none;
	margin-bottom: 8px;
}

.pa-back:hover {
	text-decoration: underline;
}

.pa-back:focus-visible {
	outline: 2px solid var(--c-board-title);
	outline-offset: 2px;
}

.pa-pagehead h1 {
	font-family: var(--ff-display);
	font-weight: var(--fw-medium);
	text-transform: uppercase;
	letter-spacing: 0.02em;
	color: var(--c-board-ink);
	font-size: clamp(28px, 4vw, 38px);
	line-height: 1.1;
	margin: 0 0 6px;
	text-wrap: balance;
}

.pa-pagehead p {
	margin: 0;
	max-width: 62ch;
	/* The page lead explains what this screen is for — body ink, not muted grey. */
	color: var(--c-body);
}


/* ── park-admin-row.css ── */
/* <park-admin-list> / <park-admin-row> — lifted 1:1 from AdminLanding.astro's
   .yp-oplist / .yp-oprow scoped styles (anton/forrest#1000). Token-only: no
   colour literals (check:brand-tokens).

   Board vernacular (anton/forrest#1422, FR-24): an operator row is a small board
   card — cream card, board hairline, deep-green label — and its description is
   body ink rather than the muted grey that fails AA on cream (FR-15). */

.pa-list {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
	gap: 10px;
}

.pa-row {
	display: flex;
	align-items: baseline;
	gap: 10px;
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	border-radius: 12px;
	padding: 10px 14px;
	text-decoration: none;
	color: var(--c-body);
	font-size: var(--fs-tiny);
}

.pa-row:hover {
	border-color: var(--c-board-title);
}

.pa-row:focus-visible {
	outline: 2px solid var(--c-board-title);
	outline-offset: 2px;
}

.pa-row b {
	color: var(--c-board-ink);
	font-weight: var(--fw-semibold);
	white-space: nowrap;
}

.pa-row-desc {
	color: var(--c-body);
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.pa-arrow {
	color: var(--c-board-title);
	font-weight: var(--fw-semibold);
}


/* ── park-admin-section.css ── */
/* <park-admin-section> — lifted 1:1 from AdminLanding.astro's .yp-section /
   .yp-sechead scoped styles (anton/forrest#1000). Token-only: no colour
   literals (check:brand-tokens).

   Board vernacular (anton/forrest#1422, FR-24): a section heading takes the
   board's PILL form — dark green, rounded, uppercase display type in light ink —
   so the operator's screens announce their groups the way every public surface
   does. The pill carries no circular icon badge here: `park-admin-section` has
   no icon in its API, and inventing one would mean asking every call site to
   choose artwork for a heading that already reads clearly. The rule under the
   heading is the board's dotted separator rather than a solid hairline (FR-15). */

.pa-section {
	margin-top: 44px;
}

.pa-sechead {
	display: flex;
	align-items: center;
	gap: 14px;
	border-bottom: 2px dotted var(--c-board-sep);
	padding-bottom: 10px;
	margin-bottom: 18px;
	flex-wrap: wrap;
}

.pa-sechead h2 {
	font-family: var(--ff-display);
	font-weight: var(--fw-medium);
	text-transform: uppercase;
	letter-spacing: 0.03em;
	font-size: var(--fs-upper);
	color: var(--c-board-header-ink);
	background: var(--c-board-header-bg);
	border-radius: var(--r-pill);
	padding: 7px 18px;
	margin: 0;
	white-space: nowrap;
}

.pa-hint {
	margin-left: auto;
	font-size: var(--fs-tiny);
	/* Section hints tell the operator what a group is for — meaning-carrying
	   text, so body ink rather than the muted grey that fails AA on cream. */
	color: var(--c-body);
	text-align: right;
}


/* ── park-admin-status.css ── */
/* park-admin-status (anton/forrest#1021) — the operator action-result banner,
   admin-shell#FR-9. Lifted from .gm-status / .gm-addstatus in GalleryManager.
   Default variant = the per-card, right-aligned case; --toolbar = the
   left-aligned, slightly larger toolbar case (the old .gm-addstatus).

   Board vernacular (anton/forrest#1422, FR-24/FR-25): "saved" speaks in the
   board's own ink, but the THREE states stay distinguishable from one another —
   an error keeps `--c-admin-danger` and in-flight work stays quieter than a
   settled result. Flattening them to one green would cost the operator the one
   thing this banner exists to tell them. */
park-admin-status {
	display: inline-block;
	font-size: var(--fs-tiny);
	color: var(--c-body);
	min-height: 1em;
	text-align: right;
}
park-admin-status.pa-status--toolbar {
	font-size: var(--fs-tiny);
	text-align: left;
}
park-admin-status[data-kind="saving"] {
	color: var(--c-body);
}
park-admin-status[data-kind="saved"] {
	color: var(--c-board-title);
}
park-admin-status[data-kind="error"] {
	color: var(--c-admin-danger);
}


/* ── park-admin-tile.css ── */
/* <park-admin-tile-grid> / <park-admin-tile> — lifted 1:1 from
   AdminLanding.astro's .yp-grid / .yp-tile / .yp-chip / .yp-note / .yp-go
   scoped styles (anton/forrest#1000). Token-only: no colour literals
   (check:brand-tokens).

   Board vernacular (anton/forrest#1422, FR-24): a tile is a board CARD — the
   board's cream card on its hairline, deep-green heading, and the board's own
   ink for the affordance that leads onward. The operator note keeps its warm
   hue: it is a warning, and FR-25 forbids flattening a meaning-carrying
   distinction into the board's green. */

.pa-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
	gap: 14px;
}

.pa-tile {
	display: flex;
	flex-direction: column;
	gap: 10px;
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	border-radius: 14px;
	padding: 16px 16px 14px;
	text-decoration: none;
	color: var(--c-body);
	transition:
		box-shadow 120ms ease,
		border-color 120ms ease,
		transform 120ms ease;
}

.pa-tile:hover {
	border-color: var(--c-board-title);
	box-shadow: var(--shadow-sm);
	transform: translateY(-1px);
}

@media (prefers-reduced-motion: reduce) {
	.pa-tile,
	.pa-tile:hover {
		transition: none;
		transform: none;
	}
}

.pa-tile:focus-visible {
	outline: 2px solid var(--c-board-title);
	outline-offset: 2px;
}

.pa-tile h3 {
	margin: 0;
	font-size: var(--fs-xs);
	font-weight: var(--fw-semibold);
	color: var(--c-board-ink);
	line-height: 1.3;
}

.pa-tile-what {
	margin: 0;
	font-size: var(--fs-tiny);
	line-height: 1.55;
	flex: 1;
}

.pa-chips {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
}

.pa-chip {
	font-size: var(--fs-tiny);
	background: var(--c-chip-bg);
	border-radius: var(--r-pill);
	padding: 2px 10px;
	color: var(--c-body);
	white-space: nowrap;
}

/* A note on an operator tile is a caution — it stays warm on purpose (FR-25). */
.pa-note {
	font-size: var(--fs-tiny);
	line-height: 1.5;
	background: var(--c-notice-warm-bg);
	color: var(--c-notice-warm-ink);
	border-radius: 8px;
	padding: 6px 10px;
}

.pa-note--wide {
	display: block;
	margin: 12px 0 0;
}

.pa-go {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	border-top: 2px dotted var(--c-board-sep);
	padding-top: 9px;
	font-size: var(--fs-tiny);
	font-weight: var(--fw-semibold);
	color: var(--c-board-title);
}

.pa-tile:hover .pa-go {
	color: var(--c-board-ink);
}

.pa-path {
	font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
	font-weight: var(--fw-regular);
	font-size: var(--fs-tiny);
	/* A route path is reference detail, not prose — it may stay quiet, but the
	   board's title ink keeps it legible where the muted grey did not. */
	color: var(--c-board-title);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}


/* ── park-booking-calendar.css ── */
/* park-booking-calendar (#146) — calendar + hour-grid + summary styles lifted
 * 1:1 from varaa.astro (de-dup, not redesign). Includes the JS-generated node
 * classes (.cal-cell*, .hour-chip*) so the inline script's injected cells/chips
 * are styled from the bundle. Auto-bundled into _ds_components.css. */

/* Board vernacular (anton/forrest#1409, spec `board-visual-language` FR-16):
 * the picker sits on the board's card, its chrome in the board's deep green
 * instead of the generic grey palette — grey on cream drops below AA (NFR-1).
 * Every state distinction the customer must perceive keeps a non-colour
 * carrier: an unavailable day stays faded AND muted, a taken hour keeps its
 * strikethrough, the selected day keeps its ring (FR-18). */
.cal {
	border: 1px solid var(--c-board-card-border);
	border-radius: 18px;
	padding: 1rem 1.1rem 1.2rem;
	background: var(--c-board-card);
}
.cal-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	margin-bottom: 0.8rem;
}
.cal-title {
	font-family: var(--ff-display);
	font-size: var(--fs-md);
	margin: 0;
	color: var(--c-board-ink);
	text-transform: capitalize;
}
.cal-nav {
	width: 2.4rem;
	height: 2.4rem;
	border-radius: var(--r-circle);
	border: 2px solid var(--c-board-header-bg);
	background: var(--c-board-card);
	font-size: var(--fs-content-heading);
	line-height: 1;
	cursor: pointer;
	color: var(--c-board-ink);
}
.cal-nav:hover {
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
}
/* FR-13a — ONE horizontal slice of consecutive days, not a month grid.
 * Seven equal columns, so the strip never scrolls horizontally and the page
 * body cannot either (NFR-4). Seven is also exactly what the month grid was
 * already sized for, so the per-day width at 320px is no worse than shipped. */
.cal-strip {
	display: grid;
	grid-template-columns: repeat(7, 1fr);
	gap: 4px;
}

/* FR-13f — the FULL MONTH view, kept as an opt-in expansion of the strip
 * rather than deleted. The strip is the compact default; scanning a whole
 * month at once is the thing a strip is genuinely worse at, so the grid stays
 * one control away instead of being replaced outright. Both render the same
 * `.cal-cell` chips, so state styling below serves both. */
.cal-weekdays,
.cal-grid {
	display: grid;
	grid-template-columns: repeat(7, 1fr);
	gap: 4px;
}
.cal-weekdays {
	margin-bottom: 4px;
}
/* anton/forrest#1392 (same trap as anton/forrest#1209, see park-admin-empty.css)
 * — an author `display` declaration beats the UA `[hidden] { display: none }`
 * rule regardless of specificity (author origin always wins over UA origin),
 * so `setMonthOpen()` setting `.hidden` on these three containers left BOTH
 * the strip and the grid rendering at once instead of exactly one. Scope the
 * override explicitly rather than relying on the UA default. */
.cal-strip[hidden],
.cal-weekdays[hidden],
.cal-grid[hidden] {
	display: none;
}
/* a11y (anton/forrest#339): the page script wraps the 42 day cells in 6
 * `role="row"` weeks so the grid has valid grid/row/gridcell structure.
 * `display: contents` dissolves the row wrappers from layout so the cells stay
 * direct grid items of `.cal-grid` (7-col template + gap unchanged). */
.cal-grid > [role="row"] {
	display: contents;
}
.cal-weekdays span {
	text-align: center;
	font-size: var(--fs-tiny);
	color: var(--c-board-title);
	text-transform: capitalize;
	padding: 2px 0;
}
/* In the month grid the weekday is the column header, so the per-chip weekday
 * line is redundant — hide it there and let the chip sit square again. */
.cal-grid .cal-day-wd {
	display: none;
}
.cal-grid .cal-cell {
	min-height: 40px;
}
/* FR-13b — the escape hatch from blind paging. Hidden until the script finds a
 * reachable free day, so it never offers a jump that goes nowhere. */
.cal-tools {
	margin: 0.5rem 0 0;
	min-height: 1.75rem;
}
.cal-nextfree {
	font: inherit;
	font-size: var(--fs-tiny);
	border: 2px solid var(--c-board-header-bg);
	border-radius: var(--r-pill);
	background: var(--c-board-card);
	color: var(--c-board-ink);
	padding: 0.25rem 0.75rem;
	cursor: pointer;
}
.cal-nextfree:hover {
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
}
/* The day/month view switcher is now a nested <park-choice variant="segmented">
 * (anton/forrest#1392) rather than a bespoke .cal-expand button — its own
 * look lives in park-choice.css. Only the placement next to #cal-nextfree is
 * this file's concern. */
.cal-tools .choice-group {
	display: inline-block;
	margin-inline-start: 0.4rem;
	vertical-align: middle;
}
/* A day chip stacks weekday / date / state-glyph. `aspect-ratio` is gone: the
 * chip is now taller than wide, which is what buys room for all three lines at
 * a 320px width (AC-8.2). */
.cal-cell {
	border: 1px solid var(--c-board-card-border);
	border-radius: 10px;
	background: var(--c-board-surface);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	color: var(--c-board-ink);
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1px;
	position: relative;
	min-height: 56px;
	padding: 4px 0;
}
.cal-day-wd {
	font-size: var(--fs-tiny);
	font-weight: 500;
	text-transform: uppercase;
	letter-spacing: 0.02em;
	opacity: 0.75;
}
.cal-day-num {
	font-size: var(--fs-tiny);
	line-height: 1;
}
/* FR-13d — "today" must not be colour-only either. */
.cal-cell[aria-current="date"] .cal-day-num {
	text-decoration: underline;
	text-underline-offset: 2px;
	text-decoration-thickness: 2px;
}
.cal-cell--out {
	opacity: 0.35;
}
/* fix-stage-island-ux (booking-flow-ux FR-13/FR-13d) — the calendar collapsed
 * from three availability states to two: a bookable day is selectable
 * regardless of how full it is, so free/partial deliberately get NO modifier
 * here and just inherit the base `.cal-cell` look (this is what makes them
 * "look normal"). A light hover affordance marks them interactive. */
.cal-cell:hover:not([aria-disabled="true"]) {
	border-color: var(--c-board-ink);
}
/* Unavailable — full, past, or beyond the booking horizon — reads as ONE
 * "not available" treatment: same sunken background as a normal cell, but
 * muted ink AND reduced opacity stacked together, so the difference is a
 * strong lightness contrast rather than a subtle tint (never colour alone,
 * and gray-on-gray needed the extra opacity step to read as "faded" rather
 * than merely "slightly different"). */
.cal-cell--full,
.cal-cell--past,
.cal-cell--beyond {
	background: var(--c-board-surface);
	color: var(--c-avail-disabled-ink);
	border-style: dotted;
	opacity: 0.6;
	cursor: not-allowed;
}
/* The chosen day is FILLED in the board's ink, not merely ringed: on a card of
 * cream chips the fill is the one treatment nothing else on the surface wears,
 * so the customer's own choice is unmistakable at a glance. */
.cal-cell.is-selected {
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
	border-color: var(--c-board-header-bg);
	outline: 3px solid var(--c-board-ink);
	outline-offset: -1px;
}
/* a11y (anton/forrest#339): visible keyboard focus for roving arrow-key
 * navigation — distinct offset so it reads even on a selected cell. */
.cal-cell:focus-visible {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 2px;
	z-index: 1;
}
/* The one-row legend was REMOVED (anton/forrest#1447). Under the day/month
 * switcher its lone muted swatch read as an unchecked checkbox — the owner saw
 * "Полный" with a box beside it and asked what he was being asked to tick. It
 * cost nothing to remove: the state it explained already has a text
 * alternative ON the cell itself (`aria-disabled` + the day's
 * available/unavailable label), so assistive tech loses nothing here. */

/* Hour grid */
.hour-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
	gap: 6px;
}
/* Same latent trap as .cal-strip/.cal-weekdays/.cal-grid above
 * (anton/forrest#1392) — varaa.astro toggles `#hour-grid`'s `hidden` attribute
 * and this rule's explicit `display: grid` would otherwise beat it. Harmless
 * today only because `renderHours()` always clears the grid's content before
 * hiding it (an empty grid has no visible height either way) — fixed anyway
 * so the container doesn't silently depend on that ordering. */
.hour-grid[hidden] {
	display: none;
}
/* a11y (anton/forrest#339): same grid/row/gridcell + roving pattern as the
 * calendar — the single `role="row"` wrapper is dissolved so chips stay direct
 * grid items. */
.hour-grid > [role="row"] {
	display: contents;
}
.hour-chip:focus-visible {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 2px;
	z-index: 1;
}
/* Inside the band the offset ring reads as a stray rectangle: a banded chip has
 * SQUARE corners on the side it joins its neighbour, so an outline drawn 2px
 * outside traces a shape that matches neither the chip nor the track. An inset
 * ring follows the chip's own asymmetric radius exactly, and stays just as
 * visible — the indicator is kept, only its geometry changes. */
.hour-chip.is-selected:focus-visible,
.hour-chip.is-preview:focus-visible {
	outline: none;
	box-shadow: inset 0 0 0 3px var(--c-board-ink);
}
/* a11y (anton/forrest#339): unavailable cells/chips are now aria-disabled (kept
 * focusable for arrow-key roving) rather than the `disabled` attribute — so
 * re-suppress the interactive hover affordance and keep the not-allowed cursor. */
.hour-chip--taken:hover {
	border-color: var(--c-board-sep);
}
.cal-cell[aria-disabled="true"],
.hour-chip[aria-disabled="true"] {
	cursor: not-allowed;
}
.hour-chip {
	position: relative;
	padding: 0.55rem 0;
	/* 2px at ALL times. Every state below changes only the border's COLOUR and
	   STYLE, never its width — a state that thickened the border changed the
	   border box, and the whole grid shifted as the cursor crossed a chip
	   (anton/forrest#1905). Geometry must not depend on state. */
	border: 2px solid var(--c-board-card-border);
	border-radius: var(--r-pill);
	background: var(--c-board-surface);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	color: var(--c-board-ink);
	text-align: center;
	transition:
		border-color 0.15s ease,
		background 0.15s ease,
		color 0.15s ease,
		border-radius 0.15s ease;
}
/* The plain hover affordance, and ONLY on a chip in no other state. It used to
 * be `.hour-chip:hover:not([aria-disabled])` — specificity (0,3,1), which beats
 * `.hour-chip.is-preview` at (0,2,0), so hovering the cell in play repainted its
 * amber border green while the dashed STYLE survived: a dark dashed ring that
 * belonged to neither state (anton/forrest#1905). */
.hour-chip:hover:not([aria-disabled="true"]):not(.is-preview):not(
	.is-selected
) {
	border-color: var(--c-board-title);
	background: var(--c-board-card);
	color: var(--c-board-ink);
}
/* The cell the cursor would act on stays UNDER the band in both directions — the
 * band never retreats out from under the cursor. Only its dress differs, and
 * where the amber grip lands (anton/forrest#1905). */
.hour-chip.is-preview {
	border: 2px dashed var(--c-accent-dark);
	background: var(--c-hour-preview-fill);
	color: var(--c-board-title);
	text-decoration: none !important;
	opacity: 1 !important;
	z-index: 2;
}
/* Already held AND in play — the customer is about to give this hour back. It
 * keeps the full track fill, because it genuinely is still his, struck through
 * with amber so it reads as leaving rather than as gone. */
.hour-chip.is-selected.is-preview-out {
	background:
		repeating-linear-gradient(
			45deg,
			var(--c-hour-evict-stripe) 0 5px,
			transparent 5px 10px
		),
		var(--c-board-sep);
	border: 2px solid var(--c-board-ink);
	/* colour, not width — see the note on .hour-chip */
	border-left-color: transparent;
	border-right-color: transparent;
}
/* The cell being given back carries the amber grip on the side the edge
 * retreats TO. A dark cap left standing at its other end contradicts it: one
 * mark says "the edge lands here in a moment", the other says "this is the edge
 * now", on a chip that is about to leave the band entirely.
 *
 * Hiding the cap alone would open a hole, because `.is-selected` paints only
 * top and bottom and the CAP is what closes an end — so the border it was
 * standing in for is restored on exactly the side it occupied. Which side that
 * is comes from the committed band position already on the chip. */
.hour-chip.is-preview-out > .hour-chip__cap {
	display: none;
}
.hour-chip.is-selected.is-preview-out.is-selected-start {
	border-left-color: var(--c-board-ink);
}
.hour-chip.is-selected.is-preview-out.is-selected-end {
	border-right-color: var(--c-board-ink);
}
.hour-chip.is-preview-start {
	border-top-right-radius: 0;
	border-bottom-right-radius: 0;
}
.hour-chip.is-preview-mid {
	border-radius: 0;
	border-left-color: transparent;
	border-right-color: transparent;
}
.hour-chip.is-preview-end {
	border-top-left-radius: 0;
	border-bottom-left-radius: 0;
	border-left-color: transparent;
}
.hour-chip.is-preview.is-connected-right::after {
	content: "–";
	position: absolute;
	/* Same gutter arithmetic as the committed bridge above — the preview band
	   joins its cells the same way and had the same 1px shortfall. */
	top: -1px;
	bottom: -1px;
	right: -9px;
	width: 11px;
	background: var(--c-hour-preview-fill);
	border-top: 2px dashed var(--c-accent-dark);
	border-bottom: 2px dashed var(--c-accent-dark);
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 0.75rem;
	font-weight: 700;
	color: transparent;
	z-index: 1;
	pointer-events: none;
}
/* anton/forrest#1438 — the grid now offers every quarter-hour start, not 24
 * whole hours, so it holds ~4x the chips. The on-the-hour ones keep the full
 * ink weight and the quarter-hours step back, giving the eye an hourly rhythm
 * to scan by. This is emphasis only: every chip stays the same size, the same
 * target, and carries its own state through the existing non-colour carriers
 * (strikethrough + dotted edge when taken), so nothing here is the sole
 * bearer of any information. */
.hour-chip:not(.hour-chip--onhour) {
	font-weight: var(--fw-medium);
	color: var(--c-muted);
}
/* Taken keeps BOTH non-colour carriers it always had — the strikethrough and
 * the faded, muted ink — so dropping the alarm-red (which the board's palette
 * has no place for) costs no distinguishability, and adds a dotted edge. */
/* An hour SOMEBODY BOOKED. It was drawn at 0.6 opacity, which under the night
 * rate made the one real booking on screen fainter than the gap cells beside it
 * — the customer could not see what was stopping his block (anton/forrest#1905).
 * The strike-through and the dotted border already say "not yours"; the ghosting
 * only made it hard to find. */
.hour-chip--taken {
	background: var(--c-board-surface);
	color: var(--c-avail-disabled-ink);
	border-color: var(--c-board-sep);
	border-style: dotted;
	cursor: not-allowed;
	text-decoration: line-through;
}
/* An hour NOBODY booked, that simply has no whole block left before the window
 * shuts. Not occupied, so no strike-through — it is merely out of reach, and
 * takes the faint weight `--taken` used to wear. */
.hour-chip--closed {
	background: var(--c-board-surface);
	color: var(--c-avail-disabled-ink);
	border-color: var(--c-board-sep);
	border-style: dotted;
	opacity: 0.55;
	cursor: not-allowed;
}
/* The 15-minute separation, made visible (anton/forrest#1905).
 *
 * An hour nobody booked that the gap alone makes unusable used to wear
 * `--taken`, so the grid said "someone has this" about an hour that is simply
 * breathing room — an unexplained refusal. It is NOT struck through: a
 * strikethrough claims the hour is spoken for, and it is not. The hatch says
 * "held apart"; the solid edge (against `--taken`'s dotted one) keeps the two
 * apart without relying on colour, and it stays unclickable either way. */
/* THE REMAINDER CELL. At whole-hour granularity the separation eats a whole
 * hour on each side of a booking, so the minutes between a held range and the
 * rest fitted nothing and were drawn for nobody. The cell now offers them, and
 * it needs two faces: the hour it begins with, and — under the pointer — what
 * it actually adds.
 *
 * The second face is a CSS swap over a data attribute, deliberately not a
 * textContent write. The grid repaints on every mousemove and restores each
 * chip's `originalLabel`, so a written label flickered back mid-hover; and the
 * chips' labels are meant to be immutable (anton/forrest#1905). An attribute
 * survives every repaint because nothing in the paint path touches it. */
.hour-chip--partial {
	position: relative;
}
/* The hour does not move aside, it goes transparent — the chip must not change
 * width under the pointer or the whole grid reflows on hover. */
.hour-chip--partial:hover {
	color: transparent;
}
.hour-chip--partial:hover::after {
	content: attr(data-add);
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	color: var(--c-board-ink);
	font-size: 0.86em;
	font-variant-numeric: tabular-nums;
	pointer-events: none;
}

/* The cell is a FIELD, and it plays the handover (anton/forrest#1905): the
 * guest leaving sets a ball down and goes, the grass stands empty for a beat,
 * the next guest comes and takes it. The empty beat IS the gap — stated rather
 * than labelled, which is what the hatch could never do.
 *
 * A NOTE ON WIDTH, because the comment that stood here was wrong. It claimed
 * the cell was "deliberately NARROW" so it could not read as a bookable hour,
 * via `flex: 0 0 auto; min-width: 2.75rem`. Both were INERT: `.hour-grid` is a
 * grid and `[role="row"]` is `display: contents`, so this is a grid item and
 * flex properties do not apply to it. Measured live, the cell was already
 * 78.9x60.1 — byte-identical to a chip. Nothing was ever narrow. What actually
 * keeps it from being taken for a slot is structural and still holds: it is not
 * a `.hour-chip`, not a `<button>`, and carries `role="separator"`. */
.hour-gap {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	/* The sign hangs ABOVE the top rail and must not be clipped. Nothing in the
	   ancestor chain clips today and that is not an accident — the anti-reflow
	   `overflow: hidden` that used to live on a chip was removed when the range
	   text moved out of it (anton/forrest#1913). Do not put one back. */
	overflow: visible;
	cursor: default;
	user-select: none;
}
/* The field does the clipping the cell must not: the dogs run in from off-cell
   on both sides and have to be cut at the rail, while the sign above it stays
   whole. That division is why there are two boxes here and not one. */
.hour-gap .gap-field {
	position: absolute;
	inset: 0;
	overflow: hidden;
	border: 2px solid var(--c-gap-grass-edge);
	border-radius: var(--r-pill);
	background: linear-gradient(
		var(--c-gap-grass-lit) 0 30%,
		var(--c-gap-grass-mid) 30% 46%,
		var(--c-gap-grass) 46% 72%,
		var(--c-gap-grass-deep) 72% 100%
	);
}
/* Clumps along the near rail, so the ground has a front edge. */
.hour-gap .gap-field::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: -2px;
	height: 34%;
	pointer-events: none;
	opacity: 0.34;
	background:
		radial-gradient(6px 4px at 18% 100%, var(--c-gap-tuft) 55%, transparent 56%),
		radial-gradient(
			7px 5px at 47% 100%,
			var(--c-gap-blade) 55%,
			transparent 56%
		),
		radial-gradient(5px 4px at 78% 100%, var(--c-gap-tuft) 55%, transparent 56%);
}
/* Three rulings at slightly different angles and periods. One period reads as
   corduroy; three read as grass. Masked out toward the top so the texture fades
   into the lit band instead of stopping on a line. */
.hour-gap .gap-blades {
	position: absolute;
	inset: 34% 0 0;
	pointer-events: none;
	opacity: 0.3;
	background:
		repeating-linear-gradient(
			88deg,
			var(--c-gap-blade) 0 0.5px,
			transparent 0.5px 7px
		),
		repeating-linear-gradient(
			95deg,
			var(--c-gap-tuft) 0 0.5px,
			transparent 0.5px 11px
		),
		repeating-linear-gradient(
			81deg,
			var(--c-gap-blade) 0 0.5px,
			transparent 0.5px 15px
		);
	mask-image: linear-gradient(transparent, var(--c-mask-opaque) 55%);
}

/* ── the sign on the rail ──────────────────────────────────────────────── */
/* Two tiers sharing a hairline edge: the word, and the duration beneath it in a
   quieter voice. It hangs off the top of the cell, which is what makes it read
   as fixed TO the field rather than drawn on it. */
.hour-gap .gap-sign {
	position: absolute;
	top: -9px;
	left: 50%;
	transform: translateX(-50%);
	z-index: 4;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	pointer-events: none;
}
.hour-gap .gap-plate {
	position: relative;
	border: 1px solid var(--c-gap-sign-edge);
	border-bottom: none;
	border-radius: 7px 7px 0 0;
	background: linear-gradient(var(--c-gap-sign-face), var(--c-gap-sign-face-2));
	color: var(--c-gap-sign-ink);
	font-size: 0.46rem;
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	white-space: nowrap;
	padding: 0.05rem 0.34rem 0.07rem;
	text-align: center;
}
/* Two fixings, so the plate is bolted on rather than floating. */
.hour-gap .gap-plate::before,
.hour-gap .gap-plate::after {
	content: "";
	position: absolute;
	top: 50%;
	margin-top: -1px;
	width: 2px;
	height: 2px;
	border-radius: 50%;
	background: var(--c-gap-sign-stud);
}
.hour-gap .gap-plate::before {
	left: 3.5px;
}
.hour-gap .gap-plate::after {
	right: 3.5px;
}
.hour-gap .gap-mins {
	border: 1px solid var(--c-gap-sign-edge);
	border-radius: 0 0 7px 7px;
	background: var(--c-gap-sign-lower);
	color: var(--c-gap-sign-lower-ink);
	font-size: 0.44rem;
	font-weight: 700;
	letter-spacing: 0.02em;
	white-space: nowrap;
	padding: 0.01rem 0.24rem 0.03rem;
	text-align: center;
}

/* ── the ball ──────────────────────────────────────────────────────────── */
.hour-gap .gap-ball {
	position: absolute;
	bottom: 15%;
	width: 6px;
	height: 6px;
	z-index: 1;
	border-radius: 50%;
	background: var(--c-gap-ball);
	opacity: 0;
}
/* A seam, so the ball reads as ROTATING rather than sliding. */
.hour-gap .gap-ball::after {
	content: "";
	position: absolute;
	inset: 1.4px;
	border-radius: 50%;
	background: linear-gradient(
		90deg,
		transparent 44%,
		var(--c-gap-ball-stripe) 44% 56%,
		transparent 56%
	);
}

/* ── the dogs ──────────────────────────────────────────────────────────── */
/* ONE base colour per breed; every other tone is DERIVED from it here. Giving a
   breed an independent limb colour is how an earlier table produced a dark body
   on four white sticks that read as a blob on stilts rather than as a dog. */
.hour-gap .gap-dog {
	position: absolute;
	bottom: 6%;
	width: 25px;
	height: 21px;
	overflow: visible;
	--gap-coat: var(--c-dog-golden);
	--gap-limb: color-mix(in srgb, var(--gap-coat) 87%, var(--c-dog-shade));
	--gap-mark: color-mix(in srgb, var(--gap-coat) 74%, var(--c-dog-shade));
	--gap-far: color-mix(in srgb, var(--gap-coat) 62%, var(--c-dog-far));
}
.hour-gap .gap-dog--golden {
	--gap-coat: var(--c-dog-golden);
}
.hour-gap .gap-dog--choc {
	--gap-coat: var(--c-dog-choc);
}
.hour-gap .gap-dog--cream {
	--gap-coat: var(--c-dog-cream);
}
.hour-gap .gap-dog--slate {
	--gap-coat: var(--c-dog-slate);
}
.hour-gap .gap-dog--coal {
	--gap-coat: var(--c-dog-coal);
}
.hour-gap .gap-dog--rust {
	--gap-coat: var(--c-dog-rust);
}
/* The SVG carries no fill attributes: a colour literal there would be a second
   home for the brand (spec FR-1/FR-2). The repo's other SVGs use
   `currentColor` + `color:` on the container, which cannot carry the four
   simultaneous tones a dog needs — hence class-bound fills here. */
.hour-gap .gap-dog .coat {
	fill: var(--gap-coat);
}
.hour-gap .gap-dog .limb {
	fill: var(--gap-limb);
}
.hour-gap .gap-dog .limb-far {
	fill: var(--gap-far);
}
.hour-gap .gap-dog .coat-mark {
	fill: var(--gap-mark);
}
.hour-gap .gap-dog .belly {
	opacity: 0.35;
}
.hour-gap .gap-dog .ink {
	fill: var(--c-dog-ink);
}
.hour-gap .gap-dog .gleam {
	fill: var(--c-dog-gleam);
	opacity: 0.85;
}
.hour-gap .gap-dog .tail {
	fill: none;
	stroke: var(--gap-coat);
	stroke-linecap: round;
}
.hour-gap .gap-dog .mouth {
	fill: none;
	stroke: var(--c-dog-ink);
	stroke-width: 0.7;
	stroke-linecap: round;
	opacity: 0.75;
}
.hour-gap .gap-dog .ball-mouth {
	fill: var(--c-gap-ball);
	opacity: 0;
}
.hour-gap .gap-dog .ball-mouth--lit {
	fill: var(--c-gap-ball-stripe);
}
.hour-gap .gap-dog.has-ball .ball-mouth {
	opacity: 1;
}
.hour-gap .gap-dog.has-ball .ball-mouth--lit {
	opacity: 0.75;
}

/* JOINT ORIGINS ARE PERCENTAGES, and that is load-bearing. With
   `transform-box: fill-box` an origin in user-space px is measured from the
   part's OWN bbox, which put each hip several px below its own leg — the legs
   then pivoted about a point in the empty grass. That was the "legs flying
   around" fault. */
.hour-gap .gap-dog g,
.hour-gap .gap-dog path {
	transform-box: fill-box;
}
.hour-gap .gap-dog .leg-f,
.hour-gap .gap-dog .leg-h,
.hour-gap .gap-dog .knee-f,
.hour-gap .gap-dog .knee-h,
.hour-gap .gap-dog .foot-f,
.hour-gap .gap-dog .foot-h {
	transform-origin: 50% 0%;
}
.hour-gap .gap-dog .rig {
	transform-origin: 50% 78%;
}
.hour-gap .gap-dog .head {
	transform-origin: 18% 88%;
}
.hour-gap .gap-dog .tail {
	transform-origin: 100% 100%;
}
.hour-gap .gap-dog .ear {
	transform-origin: 20% 100%;
}
.hour-gap .gap-dog .face {
	transform-origin: 50% 50%;
	animation: none;
}

/* The stride duration is SET BY THE SCENE (`--gap-gait`), never chosen here: it
   is derived from how fast the dog is actually crossing THIS cell. A fixed
   number is wrong at every size — wide field and the dog skates, narrow cell
   and the same number makes it treadmill. */
.hour-gap .gap-dog .rig,
.hour-gap .gap-dog .leg-f,
.hour-gap .gap-dog .leg-h,
.hour-gap .gap-dog .knee-f,
.hour-gap .gap-dog .knee-h,
.hour-gap .gap-dog .foot-f,
.hour-gap .gap-dog .foot-h,
.hour-gap .gap-dog .head,
.hour-gap .gap-dog .ear {
	animation-duration: var(--gap-gait, 0.44s);
	animation-play-state: paused;
}
/* The gait runs only while the dog is TRAVELLING. Legs that keep cycling under
   a sitting dog are the tell that the pose and the animation are two different
   opinions. */
.hour-gap .gap-dog.is-running .rig,
.hour-gap .gap-dog.is-running .leg-f,
.hour-gap .gap-dog.is-running .leg-h,
.hour-gap .gap-dog.is-running .knee-f,
.hour-gap .gap-dog.is-running .knee-h,
.hour-gap .gap-dog.is-running .foot-f,
.hour-gap .gap-dog.is-running .foot-h,
.hour-gap .gap-dog.is-running .head,
.hour-gap .gap-dog.is-running .ear {
	animation-play-state: running;
}

/* A BOUND, not a trot: front pair together, hind pair together, with a moment
   of suspension between. It is what a dog does when it is pleased rather than
   travelling — and at this size it reads far better than a trot, whose diagonal
   pairs just blur into each other. */
.hour-gap .gap-dog .rig {
	animation: gapBound 0.44s ease-in-out infinite;
}
.hour-gap .gap-dog .leg-f {
	animation: gapLegFore 0.44s linear infinite;
}
.hour-gap .gap-dog .knee-f {
	animation: gapKneeFore 0.44s linear infinite;
}
.hour-gap .gap-dog .foot-f {
	animation: gapFootFore 0.44s linear infinite;
}
.hour-gap .gap-dog .leg-h {
	animation: gapLegHind 0.44s linear infinite;
}
.hour-gap .gap-dog .knee-h {
	animation: gapKneeHind 0.44s linear infinite;
}
.hour-gap .gap-dog .foot-h {
	animation: gapFootHind 0.44s linear infinite;
}
.hour-gap .gap-dog .head {
	animation: gapHeadBob 0.44s ease-in-out infinite;
	animation-delay: -0.06s;
}
.hour-gap .gap-dog .ear {
	animation: gapEarFlop 0.44s ease-in-out infinite;
	animation-delay: -0.1s;
}
/* Deliberately NOT paused with the rest: a sitting dog still wags, and it is
   the cheapest way to keep the pose from reading as a freeze-frame. */
.hour-gap .gap-dog .tail {
	animation: gapTailWag 0.22s ease-in-out infinite;
}
/* Far pair a fifth of a stride behind the near pair. Too small an offset and
   the four legs collapse into two; too large and the dog comes apart. */
.hour-gap .gap-dog .far {
	animation-delay: -0.088s;
}

@keyframes gapBound {
	0% {
		transform: translateY(1.1px) rotate(-2deg);
	} /* gather, haunches down */
	30% {
		transform: translateY(-2px) rotate(3deg);
	} /* push off, nose up */
	50% {
		transform: translateY(-2.6px) rotate(1deg);
	} /* suspension */
	72% {
		transform: translateY(0.3px) rotate(-3deg);
	} /* forefeet land */
	100% {
		transform: translateY(1.1px) rotate(-2deg);
	}
}
/* STANCE vs SWING, and the difference between them is the whole point. 0-40% is
   stance: the foot is planted and the limb stays nearly STRAIGHT while the hip
   sweeps it back — a bent leg here looks like it is collapsing under the dog.
   40-100% is swing: the limb leaves the ground and FOLDS hard. Rotating hip and
   knee in step gave a pendulum; the fold has to be concentrated where the foot
   is in the air. `linear` on purpose — the easing lives in the keyframe
   spacing, and an ease-in-out on top added a hitch at every joint. */
@keyframes gapLegFore {
	0% {
		transform: rotate(-24deg);
	} /* touchdown, reaching ahead */
	18% {
		transform: rotate(-6deg);
	}
	40% {
		transform: rotate(20deg);
	} /* end of stance, pushing back */
	52% {
		transform: rotate(12deg);
	}
	70% {
		transform: rotate(-14deg);
	} /* folded, swinging through */
	86% {
		transform: rotate(-30deg);
	}
	100% {
		transform: rotate(-24deg);
	}
}
@keyframes gapKneeFore {
	0% {
		transform: rotate(6deg);
	}
	18% {
		transform: rotate(2deg);
	} /* straight under load */
	40% {
		transform: rotate(8deg);
	}
	52% {
		transform: rotate(46deg);
	} /* snaps up the moment it leaves the ground */
	70% {
		transform: rotate(58deg);
	} /* tucked tight at mid-swing */
	86% {
		transform: rotate(20deg);
	}
	100% {
		transform: rotate(6deg);
	}
}
@keyframes gapFootFore {
	0% {
		transform: rotate(14deg);
	} /* toes up, heel leads the landing */
	18% {
		transform: rotate(0deg);
	} /* flat on the ground */
	40% {
		transform: rotate(-16deg);
	} /* toes push off last */
	52% {
		transform: rotate(-30deg);
	}
	70% {
		transform: rotate(-8deg);
	}
	86% {
		transform: rotate(16deg);
	}
	100% {
		transform: rotate(14deg);
	}
}
/* The hind limb folds FORWARD and much further — it is the engine, and its tuck
   is the most visible thing in a bound. Opposite sign to the foreleg, which is
   what stops all four legs looking like one mechanism repeated. */
@keyframes gapLegHind {
	0% {
		transform: rotate(-18deg);
	}
	18% {
		transform: rotate(2deg);
	}
	40% {
		transform: rotate(26deg);
	} /* full extension of the drive */
	52% {
		transform: rotate(16deg);
	}
	70% {
		transform: rotate(-22deg);
	} /* tucked under the belly */
	86% {
		transform: rotate(-26deg);
	}
	100% {
		transform: rotate(-18deg);
	}
}
@keyframes gapKneeHind {
	0% {
		transform: rotate(-10deg);
	}
	18% {
		transform: rotate(-4deg);
	}
	40% {
		transform: rotate(-12deg);
	}
	52% {
		transform: rotate(-52deg);
	} /* hock snaps closed */
	70% {
		transform: rotate(-64deg);
	}
	86% {
		transform: rotate(-24deg);
	}
	100% {
		transform: rotate(-10deg);
	}
}
@keyframes gapFootHind {
	0% {
		transform: rotate(12deg);
	}
	18% {
		transform: rotate(0deg);
	}
	40% {
		transform: rotate(-20deg);
	}
	52% {
		transform: rotate(28deg);
	}
	70% {
		transform: rotate(40deg);
	}
	86% {
		transform: rotate(18deg);
	}
	100% {
		transform: rotate(12deg);
	}
}
/* Head and tail FOLLOW the body rather than keeping their own beat — a head
   nodding on its own schedule is the single thing that made it read as a
   puppet. */
@keyframes gapHeadBob {
	0% {
		transform: rotate(4deg);
	}
	30% {
		transform: rotate(-6deg);
	}
	50% {
		transform: rotate(-3deg);
	}
	72% {
		transform: rotate(7deg);
	}
	100% {
		transform: rotate(4deg);
	}
}
@keyframes gapTailWag {
	0%,
	100% {
		transform: rotate(-20deg);
	}
	50% {
		transform: rotate(26deg);
	}
}
@keyframes gapEarFlop {
	0% {
		transform: rotate(-10deg);
	}
	32% {
		transform: rotate(16deg);
	}
	60% {
		transform: rotate(-6deg);
	}
	100% {
		transform: rotate(-10deg);
	}
}

/* ── the poses ─────────────────────────────────────────────────────────── */
/* A real sit, not a collapse: haunches folded onto the grass, chest LIFTED,
   forelegs straight and VERTICAL. The forelegs are children of `.rig`, so
   tilting the body tilts them with it; they are counter-rotated by the same
   angle to stand upright in the world rather than lean forward with the chest,
   which is what made an earlier pose read as a dog falling onto its face.

   The sit pivots at the SHOULDER, not at the rig's default origin. Rotating
   about the default swings the whole dog and leaves the rump hovering ~6px
   above the front paw — measured; the dog read as perching. The origin below is
   the foreleg's own attachment (x=26.6, y=15.2 in the viewBox) expressed
   against the rig's fill-box, so the shoulder stays put while the rear swings
   DOWN onto the grass. Measured after: rump, hock and front paw within 1.5px,
   where they had been 6.1px apart. */
.hour-gap .gap-dog.is-sitting .rig,
.hour-gap .gap-dog.is-sitting .leg-f,
.hour-gap .gap-dog.is-sitting .leg-h,
.hour-gap .gap-dog.is-sitting .knee-f,
.hour-gap .gap-dog.is-sitting .knee-h,
.hour-gap .gap-dog.is-sitting .foot-f,
.hour-gap .gap-dog.is-sitting .foot-h,
.hour-gap .gap-dog.is-sitting .head {
	animation: none;
}
.hour-gap .gap-dog.is-sitting .rig {
	transform-origin: 26.2px 12.19px;
	transform: translateY(2.5px) rotate(-34deg);
}
.hour-gap .gap-dog.is-sitting .leg-h {
	transform: rotate(-14deg);
}
.hour-gap .gap-dog.is-sitting .knee-h {
	transform: rotate(-74deg);
}
.hour-gap .gap-dog.is-sitting .foot-h {
	transform: rotate(30deg);
}
.hour-gap .gap-dog.is-sitting .leg-f {
	transform: rotate(34deg);
} /* cancels the body's -34 */
.hour-gap .gap-dog.is-sitting .knee-f {
	transform: rotate(0deg);
}
.hour-gap .gap-dog.is-sitting .foot-f {
	transform: rotate(-9deg);
}
/* +19 against the body's -34 keeps the muzzle at the same lift it had before
   the tilt got steeper; without it the dog stares at the sky. */
.hour-gap .gap-dog.is-sitting .head {
	transform: rotate(19deg);
}

/* The play-bow — front end down, nose to the grass, haunches still up. Without
   it a dog "took" the ball by standing beside it while the ball blinked out:
   the one moment the whole scene turns on had no gesture at all. */
.hour-gap .gap-dog.is-bowing .rig,
.hour-gap .gap-dog.is-bowing .leg-f,
.hour-gap .gap-dog.is-bowing .leg-h,
.hour-gap .gap-dog.is-bowing .knee-f,
.hour-gap .gap-dog.is-bowing .knee-h,
.hour-gap .gap-dog.is-bowing .foot-f,
.hour-gap .gap-dog.is-bowing .foot-h,
.hour-gap .gap-dog.is-bowing .head {
	animation: none;
}
.hour-gap .gap-dog.is-bowing .rig {
	transform: translateY(2px) rotate(7deg);
}
.hour-gap .gap-dog.is-bowing .head {
	transform: rotate(32deg);
}
.hour-gap .gap-dog.is-bowing .leg-f {
	transform: rotate(20deg);
}
.hour-gap .gap-dog.is-bowing .knee-f {
	transform: rotate(-18deg);
}
.hour-gap .gap-dog.is-bowing .foot-f {
	transform: rotate(-6deg);
}
.hour-gap .gap-dog.is-bowing .leg-h {
	transform: rotate(-12deg);
}
.hour-gap .gap-dog.is-bowing .knee-h {
	transform: rotate(-16deg);
}
.hour-gap .gap-dog.is-bowing .foot-h {
	transform: rotate(14deg);
}

/* The transition is what makes a pose change a MOVEMENT rather than a swap.
   NOTE when measuring: a computed transform read inside this window reports the
   PREVIOUS pose. Wait it out before believing a reading. */
.hour-gap .gap-dog .rig,
.hour-gap .gap-dog .head,
.hour-gap .gap-dog .leg-f,
.hour-gap .gap-dog .leg-h {
	transition: transform 180ms ease-out;
}
.hour-gap .gap-dog.is-running .rig,
.hour-gap .gap-dog.is-running .head,
.hour-gap .gap-dog.is-running .leg-f,
.hour-gap .gap-dog.is-running .leg-h {
	transition: none;
}

/* Motion here is decoration and nothing depends on it. The scene ALSO checks
   this in JS and never starts its loop — a reader who asked for less motion
   should not pay for a timer running behind a stilled picture. */
@media (prefers-reduced-motion: reduce) {
	.hour-gap .gap-dog,
	.hour-gap .gap-dog .rig,
	.hour-gap .gap-dog .tail,
	.hour-gap .gap-dog .ear,
	.hour-gap .gap-dog .head,
	.hour-gap .gap-dog .leg-f,
	.hour-gap .gap-dog .leg-h,
	.hour-gap .gap-dog .knee-f,
	.hour-gap .gap-dog .knee-h,
	.hour-gap .gap-dog .foot-f,
	.hour-gap .gap-dog .foot-h,
	.hour-gap .gap-dog .face {
		animation: none;
		transition: none;
	}
	.hour-gap .gap-ball {
		opacity: 1;
	}
}

/* The visitor's OWN unpaid hold. It is no longer an unavailable chip wearing a
 * friendlier face (anton/forrest#1900) but a SELECTABLE one he can edit
 * (anton/forrest#1907) — so it must not inherit `--taken`'s strikethrough or
 * not-allowed cursor. Full opacity, board ink, solid edge: present and his.
 * The distinction never rests on colour alone — the chip carries its own
 * aria-label and the banner above states it in words. When it also carries
 * `--taken` (the hold lapsed and is genuinely gone) the strikethrough returns
 * from that class, which is the correct reading in that one case. */
.hour-chip--own {
	opacity: 1;
	color: var(--c-board-ink);
	border-style: solid;
	border-color: var(--c-board-ink);
}
.hour-chip--own:not(.hour-chip--taken) {
	cursor: pointer;
	text-decoration: none;
}
/* anton/forrest#1906 — a selected hour that is not an edge. It is part of the
 * range and must keep reading as chosen, so it is deliberately NOT dimmed the
 * way a taken chip is: the only thing it lacks is an action, and that is carried
 * by the cursor and the accessible name, never by looking unavailable. */
.hour-chip--locked {
	cursor: default;
}
/* An adjacent slot that cannot be added, because the merged range would break
 * the required separation from the next booking. Distinguished from a plain
 * taken chip: nothing is booked here, it simply cannot be reached from the
 * current selection — so it keeps its own time and gains no strikethrough. */
.hour-chip--blocked {
	cursor: not-allowed;
	opacity: 0.55;
	border-style: dashed;
}
/* One row: what he has chosen on the left, the way onward on the right — the
 * statement and the act on it read together instead of stacking. `space-between`
 * keeps the button hard right even while the hint is hidden (before a selection
 * exists), so the control does not jump sideways when the text appears. It wraps
 * on a narrow phone rather than squeezing either half (FR-14). */
.time-actions {
	margin-top: 1rem;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.6rem 1rem;
}
/* What he has chosen, stated ABOVE the grid it describes. It used to sit in the
 * action row beneath, where it read after the thing it was about and made that
 * row change height as it appeared.
 *
 * Three indicators rather than one sentence: start, end, and how long. Each is a
 * separate fact the customer checks separately — "does it start when I meant?",
 * "how many hours am I paying for?" — and a sentence makes him parse the whole
 * of it to answer either. The panel is rendered whenever the grid is, with an em
 * dash in each value until a legal span exists, so the grid does not shift down
 * the moment a first hour is picked. */
.time-span {
	display: block;
	margin: 0 0 0.8rem;
}
/* Which of the two readouts this is. Without it two identical rows of the same
 * three labels are a riddle — especially before a hover, when the lower row is
 * three em dashes and nothing on screen says why. */
.time-span__caption {
	margin: 0 0 0.25rem;
	font-size: 0.78rem;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--c-board-title);
}
.time-span__row + .time-span__caption {
	margin-top: 0.55rem;
}
.time-span__row {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 0.5rem;
}
/* Same card the booking summary uses, so the readout reads as part of the board
 * rather than as a new kind of surface. */
.time-span__cell {
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
	padding: 1rem 1.15rem 1.1rem;
	border: 1px solid var(--c-board-card-border);
	border-radius: 18px;
	background: var(--c-board-card);
	min-width: 0;
}
.time-span__label {
	font-size: var(--fs-tiny);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--c-board-title);
}
/* The value carries the display face at the board's deepest ink: it is the
 * answer, and it should be readable at a glance from further away than the
 * label naming it. `tabular-nums` keeps 09:00 and 21:00 the same width, so the
 * three cells do not twitch as the customer grows the span. */
.time-span__value {
	font-family: var(--ff-display);
	font-size: clamp(1.75rem, 4.5vw, 2.5rem);
	line-height: 1.05;
	color: var(--c-board-ink);
	font-variant-numeric: tabular-nums;
}
/* A duration is no longer always "1 год": a remainder makes it "1 год 45 хв",
 * which wrapped onto a second line and made the cell taller than its two
 * neighbours.
 *
 * The fix is to stop it breaking, NOT to add column sizing. Each track here is
 * `1fr`, i.e. `minmax(auto, 1fr)`, and an `auto` minimum is min-content — so a
 * column ALREADY takes more than its third when its content demands it, and the
 * others give way down to their own min-content (two short times, with room to
 * spare). While the value could break, its min-content was one word and the
 * track never grew. Unbreakable, the grid does the rest by itself. */
.time-span__cell:last-child .time-span__value {
	white-space: nowrap;
}
/* ...and the track has to be ALLOWED to hear about it. `.time-span__cell` sets
 * `min-width: 0`, which replaces a grid item's automatic min-content minimum
 * with zero — so an unbreakable value simply overflowed its own cell while all
 * three columns stayed exactly equal (measured: at a 460px row, 148/148/148
 * with 64px spilling out). Restoring the floor on the LAST cell only is what
 * makes the duration column grow and the other two give way; they keep
 * `min-width: 0` and shrink, which is the whole point. */
.time-span__cell:last-child {
	min-width: min-content;
}
/* Author `display: grid` beats the UA `[hidden] { display: none }` on cascade
 * origin — the same trap already documented for .bk-auth-widget
 * (anton/forrest#1472). Without this the panel would be visible before a day is
 * chosen. */
.time-span[hidden] {
	display: none;
}
/* The lower row: what the chip under the cursor WOULD give. Dashed edge and the
 * board's lighter ink, permanently — this row is never a statement of fact, so
 * it is never dressed as one. (The range used to be written into the hovered
 * chip itself, where the anti-reflow `overflow: hidden` clipped it to
 * "10:00–11:" — anton/forrest#1913; a cell is simply too small to hold a range.)
 * Its columns line up with the row above so the two spans can be read off
 * against each other without moving the eye sideways. */
.time-span__row--preview .time-span__cell {
	border-style: dashed;
	background: transparent;
}
.time-span__row--preview .time-span__value {
	color: var(--c-board-title);
}

/* ── the hint balloon (anton/forrest#1905) ─────────────────────────────
 * Follows the cursor rather than anchoring to the cell: the cell already carries
 * two signals (its dress and the grip) and a third mark on it would crowd them.
 * `pointer-events: none` is load-bearing — a panel under the cursor would steal
 * the hover it exists to explain and flicker.
 *
 * It reinforces, it does not inform: a mouse-following panel is invisible to a
 * keyboard user, so every fact it states is also in the chip's `aria-label`. */
.hour-hint {
	position: fixed;
	z-index: 60;
	pointer-events: none;
	display: flex;
	align-items: center;
	gap: 0.7rem;
	padding: 0.7rem 1.05rem 0.7rem 0.7rem;
	border-radius: 14px;
	border: 2px solid var(--c-hint-border);
	background: var(--c-board-ink);
	color: var(--c-board-header-ink);
	font-size: 1.06rem;
	font-weight: 700;
	line-height: 1.3;
	white-space: nowrap;
	box-shadow: 0 10px 30px var(--c-hint-shadow);
	opacity: 0;
	transform: translateY(8px) scale(0.94);
	transform-origin: top left;
	transition:
		opacity 0.12s ease,
		transform 0.12s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.hour-hint.is-on {
	opacity: 1;
	transform: none;
}
.hour-hint::before {
	content: "";
	position: absolute;
	width: 13px;
	height: 13px;
	background: inherit;
	border-left: 2px solid var(--c-hint-border);
	border-top: 2px solid var(--c-hint-border);
	transform: rotate(45deg);
	top: -8px;
	left: 15px;
}
.hour-hint.flip-y::before {
	top: auto;
	bottom: -8px;
	transform: rotate(225deg);
}
.hour-hint.flip-x::before {
	left: auto;
	right: 15px;
}
.hour-hint.flip-x {
	transform-origin: top right;
}
.hour-hint.flip-y {
	transform-origin: bottom left;
}
.hour-hint.flip-x.flip-y {
	transform-origin: bottom right;
}
.hour-hint__body {
	display: flex;
	flex-direction: column;
	gap: 0.18rem;
}
.hour-hint__result {
	font-size: 0.9rem;
	font-weight: 500;
	opacity: 0.85;
	font-variant-numeric: tabular-nums;
}
.hour-hint__key {
	flex: none;
	padding: 0.28rem 0.55rem;
	border-radius: 8px;
	background: var(--c-hint-key-bg);
	border: 1px solid var(--c-hint-key-border);
	font-size: 0.82rem;
	font-weight: 700;
	letter-spacing: 0.05em;
	text-transform: uppercase;
}
/* Giving an hour back is an act of a different kind from taking one, so it takes
 * the amber the grip already speaks in. */
.hour-hint.is-evict {
	background: var(--c-accent-dark);
	color: var(--c-bg);
	border-color: var(--c-hint-evict-border);
}
.hour-hint.is-evict::before {
	border-color: var(--c-hint-evict-border);
}
/* A refusal is not an offer: no amber, and no keybind, because there is no click
 * to make. */
.hour-hint.is-refuse {
	background: var(--c-hint-refuse-bg);
	color: var(--c-bg);
	border-color: var(--c-hint-refuse-border);
}
.hour-hint.is-refuse::before {
	border-color: var(--c-hint-refuse-border);
}
/* A rest between guests is neither an offer nor a refusal, so it takes neither
 * voice. */
.hour-hint.is-rest {
	background: var(--c-hint-rest-bg);
	color: var(--c-bg);
	border-color: var(--c-hint-rest-border);
}
.hour-hint.is-rest::before {
	border-color: var(--c-hint-rest-border);
}
@media (prefers-reduced-motion: reduce) {
	.hour-hint {
		transition: opacity 0.12s ease;
		transform: none;
	}
	.hour-hint.is-on {
		transform: none;
	}
}
/* A cursor-following panel means nothing without a cursor. */
@media (hover: none) {
	.hour-hint {
		display: none;
	}
}

/* The "if you click" readout is a HOVER affordance, and a touch screen has no
 * hover — so on phones and tablets it can never be anything but three em dashes
 * taking a third of the panel. Hidden there, on both the row and the caption
 * that introduces it (`.time-span__row + .time-span__caption` is exactly the
 * second caption, the preview one). anton/forrest#1905. */
@media (max-width: 900px) {
	.time-span__row + .time-span__caption,
	.time-span__row--preview {
		display: none;
	}
}

/* Three cells side by side squeeze the values — and the LABELS — onto two lines
 * each; on a phone "ПОЧАТОК" was clipping to "ПОЧАТО". Start and end take half
 * the device each (so together the full width) and duration drops to its own
 * full-width line, which keeps every value on one line (FR-14).
 *
 * This rule used to fire only below 380px, which is narrower than any phone the
 * owner actually holds — the clipping was live at 534px. */
@media (max-width: 768px) {
	.time-span__row {
		grid-template-columns: repeat(2, 1fr);
		gap: 0.4rem;
	}
	.time-span__cell:last-child {
		grid-column: 1 / -1;
	}
	/* A cell pinned to the top is spending the phone's scarcest resource —
	 * vertical room the hour grid needs. At the desktop padding and display size
	 * the readout took roughly a QUARTER of an 844px viewport, which is the
	 * opposite of the point: it was pinned so the customer could see the answer
	 * WHILE picking, and it left him almost nothing to pick from. Tightened here
	 * only; the desktop panel is unchanged (anton/forrest#1905). */
	.time-span__cell {
		padding: 0.5rem 0.7rem 0.55rem;
		border-radius: 14px;
		gap: 0.1rem;
	}
	.time-span__value {
		font-size: clamp(1.3rem, 6vw, 1.7rem);
	}
	.time-span__caption {
		margin-bottom: 0.2rem;
	}
	/* The whole point of picking hours is comparing them against what you have
	 * already got, and on a phone the grid is long enough to scroll BOTH the
	 * stage's own header and the readout off the top — so the customer loses
	 * which step he is on AND what he has chosen, exactly while choosing. The
	 * header pins first and the readout pins directly beneath it, so section 2
	 * keeps its whole identity on screen.
	 *
	 * Scoped THREE ways on purpose:
	 *   · `:has(#time-block)` — only the time stage. `.stage-headrow` is shared
	 *     by all three steps, and pinning them all would stack three bars.
	 *   · `[data-state="open"]` — only while the section is ACTIVE. A collapsed
	 *     stage has nothing to keep in view and would just pin a dead bar.
	 *   · the 768px query — desktop has the room and is unchanged.
	 *
	 * Both need their own BACKGROUND or the chips scroll through them (the
	 * headrow's own is transparent — the dark pill is on the button inside it),
	 * and the readout needs a bottom EDGE: against the panel's own surface a
	 * pinned block reads as part of the scrolling content until a chip slides
	 * under it. `top: 3.45rem` is the measured headrow height at this width
	 * (55px) and the header therefore takes NO extra padding: any it took made it
	 * taller than that offset, and the chips showed through the few pixels
	 * between the two pinned blocks. The pair must tile exactly. */
	.stage[data-state="open"]:has(#time-block) > .stage-headrow {
		position: sticky;
		top: 0;
		/* Clear of the GRID's whole stacking band, not merely above one part of
		 * it. The chips and their caps/grips already run 1-4, and they come later
		 * in the DOM, so an equal z-index loses on tie-break: the gap cells drew
		 * straight over the pinned readout (anton/forrest#1905). The hint balloon
		 * stays above everything at 60. */
		z-index: 31;
		background: var(--c-board-surface);
	}
	.stage[data-state="open"]:has(#time-block) .time-span {
		position: sticky;
		top: 3.45rem;
		z-index: 30;
		background: var(--c-board-surface);
		/* The gutter STAYS, but the pinned block's own surface runs through it.
		 * `.stage-body` insets this by 1.4rem a side, and a background stopping at
		 * that inset left two vertical strips of bare panel beside the readout —
		 * strips the chips scrolled through. Cancel the inset with a negative
		 * margin and hand it straight back as padding: the box paints edge to edge
		 * while the cells stay exactly where they were (anton/forrest#1905). */
		margin-inline: -1.4rem;
		padding: 0.45rem 1.4rem 0.5rem;
		margin-bottom: 0.55rem;
		border-bottom: 1px solid var(--c-board-card-border);
	}
}
/* `margin-left: auto` rather than `space-between`: the row now holds the button
 * alone, so there is nothing for space-between to push against. Pushing from the
 * button itself keeps it hard right regardless of what else lands in the row. */
.time-actions > .bk-btn {
	margin-left: auto;
}
/* The band is a TRACK the chips sit on, not a repaint of the chips
 * (anton/forrest#1905). The ink is never inverted: #14371f on the sage track is
 * 9.4:1, so a held hour is no harder to read than a free one — and every hour
 * keeps its own time, which is what removes the blank `—` middles entirely. */
.hour-chip.is-selected {
	background: var(--c-board-sep);
	color: var(--c-board-ink);
	border-color: transparent;
	border-top: 2px solid var(--c-board-ink);
	border-bottom: 2px solid var(--c-board-ink);
	text-decoration: none !important;
	opacity: 1 !important;
	border-style: solid !important;
	z-index: 2;
}
.hour-chip.is-selected-start {
	border-top-right-radius: 0;
	border-bottom-right-radius: 0;
}
.hour-chip.is-selected-mid {
	border-radius: 0;
}
.hour-chip.is-selected-end {
	border-top-left-radius: 0;
	border-bottom-left-radius: 0;
}
/* The two hours that can be given back — the first and last of the span. They
 * are one STATE, not a hover effect: the customer can see which cells he may
 * click before he goes looking with the cursor, and both look the same because
 * both do the same thing. The inert middles and the boundary marker deliberately
 * do NOT get this. Inset rather than outset so the band's silhouette is
 * unchanged and only the handles read as separable. */
/* The track's ENDS and the amber grip are REAL ELEMENTS, appended by the
 * renderer, not pseudo-elements and not inset shadows (anton/forrest#1905).
 *
 * Inset shadows were the first attempt and looked wrong: the chip is a pill with
 * a `--r-pill` radius, and an inset 10px shadow is eaten by that curvature, so a
 * solid grip survived only as a thin crescent at the widest point.
 *
 * Pseudo-elements were the second and could not work either: an element has ONE
 * `::before` and ONE `::after`, and the connector that bridges the gutter
 * already owns `::after` on every chip joined to its neighbour — which is
 * exactly the chip a shrink-at-the-start puts a grip on.
 *
 * A child element has neither limit: it sits outside the fill, keeps its own
 * shape whatever the chip's radius, and can overlap the gutter the way the
 * prototype's cap did. */
.hour-chip__cap,
.hour-chip__grip {
	position: absolute;
	top: -2px;
	bottom: -2px;
	width: 10px;
	border-radius: var(--r-pill);
	z-index: 3;
	pointer-events: none;
}
.hour-chip__cap {
	background: var(--c-board-ink);
}
/* Where the edge lands after the click — the one amber mark in the grid. */
.hour-chip__grip {
	background: var(--c-accent-dark);
	z-index: 4;
	box-shadow: 0 0 0 2px var(--c-board-card);
}
.hour-chip__cap--l,
.hour-chip__grip--l {
	left: -1px;
}
.hour-chip__cap--r,
.hour-chip__grip--r {
	right: -1px;
}
/* A cap marks the edge of the band NOW. While a preview is up, a side that the
 * previewed band runs straight through is no longer an edge, and a dark bar
 * left standing there cuts the resulting track in half — most visibly when a
 * one-cell band grows, where `solo` had put a cap on both sides and the click
 * is about to make one of them interior.
 *
 * The preview's own band position is already on the chip (`paintBand` stamps it
 * from `pv.cells`), so this needs no JS and no second opinion about which cell
 * is an end — and it reverts by itself when the preview classes are swept. A
 * committed cell OUTSIDE the previewed band gets none of these classes and so
 * keeps both caps, which is what a restart must look like. */
.hour-chip.is-preview-start > .hour-chip__cap--r,
.hour-chip.is-preview-mid > .hour-chip__cap--l,
.hour-chip.is-preview-mid > .hour-chip__cap--r,
.hour-chip.is-preview-end > .hour-chip__cap--l {
	display: none;
}
.hour-chip.is-selected-edge:hover {
	filter: brightness(0.97);
}
.hour-chip.is-selected.is-connected-right::after {
	content: "";
	position: absolute;
	/* -2px, not -1px: the chip's own top/bottom borders are 2px, and the bridge
	   has to line up with the BORDER box or the join shows a notch. */
	top: -2px;
	bottom: -2px;
	/* The horizontal pair needs the SAME correction the vertical one got, and
	   never had it. An absolute offset resolves against the PADDING box, so with
	   a 2px border `right: -7px; width: 8px` reached only 5px past the border
	   edge into a 6px gutter — the last pixel of every joint went unpainted and
	   read as a hairline seam. -9/11 covers the whole gutter and spills 2px onto
	   the next chip, which paints over it (anton/forrest#1905). */
	right: -9px;
	width: 11px;
	background: var(--c-board-sep);
	border-top: 2px solid var(--c-board-ink);
	border-bottom: 2px solid var(--c-board-ink);
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 0.75rem;
	font-weight: 700;
	color: transparent;
	z-index: 1;
	pointer-events: none;
}
.hour-chip--conflict {
	border-color: var(--c-danger-border-strong) !important;
	color: var(--c-danger-ink) !important;
	background: var(--c-danger-bg) !important;
}
.hour-chip--conflict:hover,
.hour-chip--conflict.is-preview,
.hour-chip--conflict.is-selected {
	background: var(--c-danger-ink) !important;
	color: var(--c-bg) !important;
	border-color: var(--c-danger-ink) !important;
}
.hour-chip--conflict.is-connected-right::after {
	content: "–";
	background: var(--c-danger-ink) !important;
	border-top-color: var(--c-danger-ink) !important;
	border-bottom-color: var(--c-danger-ink) !important;
	color: var(--c-bg) !important;
}

.bk-summary {
	border: 1px solid var(--c-board-card-border);
	border-radius: 18px;
	padding: 1rem 1.1rem;
	background: var(--c-board-card);
}
.bk-summary p {
	margin: 0.15rem 0;
}


/* ── park-choice.css ── */
/* <park-choice> — a single-select group of native radios styled as buttons
 * (fix-stage-island-ux). Replaces the /varaa island <select>.
 *
 * The radio input itself is visually hidden but stays in the tab order and
 * receives real focus (a11y: `.choice-input:focus-visible + .choice-face`
 * draws the ring) — the `<label>` is what reads as the button. Selection is
 * NEVER colour-only: the checked face gets an inset shadow, a heavier
 * border, bold text AND a checkmark glyph, so the "highly visible push
 * effect" survives colour-blindness and grayscale alike.
 */

.choice-group {
	border: 0;
	margin: 0;
	padding: 0;
}

.choice-row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.6rem;
}

.choice-input {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.choice-btn {
	display: inline-flex;
	cursor: pointer;
}
/* The author `display` above beats the UA's `[hidden] { display: none }`, so an
 * option a page hides at runtime would stay visible — the same trap already hit
 * by park-admin-empty (anton/forrest#1209) and by this component's own
 * day/month switcher (anton/forrest#1392). `/varaa` hides the night-block
 * option on a section that does not offer one (anton/forrest#1436), which is
 * exactly a runtime hide, so re-assert it here rather than at the call site. */
.choice-btn[hidden] {
	display: none;
}

/* Board vernacular (anton/forrest#1409, spec `board-visual-language` FR-16):
 * the option is already a pill — it takes the board's pill, ink and card so it
 * belongs to the same artefact as the group headers above it. All four
 * non-colour "pressed" signals below are untouched (FR-18). */
.choice-face {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.75rem 1.1rem;
	border: 2px solid var(--c-board-header-bg);
	border-radius: var(--r-pill);
	background: var(--c-board-card);
	color: var(--c-board-ink);
	font: inherit;
	font-weight: 600;
	line-height: 1.2;
	transition:
		transform 0.1s ease,
		box-shadow 0.1s ease;
}

.choice-btn:hover .choice-face {
	border-color: var(--c-board-ink);
}

/* The checkmark exists in the DOM for every option but is only shown once
 * checked — an icon differentiator, not a colour one. */
.choice-check {
	display: none;
	font-weight: 700;
}

/* Highly visible "pushed" state: inset shadow (reads as physically pressed),
 * a filled/heavier border, bold weight already set above, and the checkmark
 * — four non-colour signals stacked on top of the colour change. */
.choice-input:checked + .choice-face {
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
	border-color: var(--c-board-header-bg);
	border-width: 3px;
	box-shadow: var(--shadow-inset-sm);
	transform: translateY(1px);
}
.choice-input:checked + .choice-face .choice-check {
	display: inline;
}

.choice-input:focus-visible + .choice-face {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 2px;
}

/* Variant "segmented" (anton/forrest#1392) — the day/month view switcher.
 * Same markup, same radio semantics, different shell: the row gets ONE
 * shared outer border/radius and its overflow clipped, each face loses its
 * own border/radius entirely, and a divider line marks the seam — so two
 * options read as one button split in half rather than two pills next to
 * each other. The checked segment still gets the inset-shadow "pressed" look
 * and the checkmark from the rules above; only the per-face border/transform
 * (which don't make sense once the border moved to the shared row) are
 * overridden here. */
.choice-group--segmented .choice-row {
	display: inline-flex;
	flex-wrap: nowrap;
	gap: 0;
	border: 2px solid var(--c-board-header-bg);
	border-radius: var(--r-pill);
	overflow: hidden;
}
.choice-group--segmented .choice-face {
	border: 0;
	border-radius: 0;
	padding: 0.55rem 1rem;
}
.choice-group--segmented .choice-btn:not(:first-child) .choice-face {
	border-inline-start: 2px solid var(--c-board-header-bg);
}
.choice-group--segmented .choice-input:checked + .choice-face {
	border: 0;
	transform: none;
}
.choice-group--segmented
	.choice-btn:not(:first-child)
	.choice-input:checked
	+ .choice-face {
	border-inline-start: 2px solid var(--c-board-header-bg);
}


/* ── park-cta.css ── */
/* park-cta (#188) — closing call-to-action band. Reproduces the live
 * HomePage.astro `.home-cta` styles 1:1 (de-dup, not redesign). Colours are
 * --c-* tokens with NO literal fallback (anton/forrest#865). `.park-wrap` is shared (park.css) — not duplicated.
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

/* Board vernacular (anton/forrest#1405, FR-11): the closing band is a board
 * CARD on the cream surface it now sits on — a pale panel would read as a
 * foreign block cut into the board. */
.home-cta {
	margin-top: 3.5rem;
	padding: 0;
	text-align: center;
}
/* The card is the inner wrap, not the section: a full-bleed rounded band would
   run its corners off the viewport edge. */
.home-cta > .park-wrap {
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	border-radius: 22px;
	padding-block: clamp(2.5rem, 6vw, 4rem);
}
.home-cta h2 {
	font-size: var(--fs-h2);
	color: var(--c-board-ink);
	margin: 0 0 0.75rem;
}
.home-cta p {
	color: var(--c-body);
	max-width: 50ch;
	margin: 0 auto 1.5rem;
}


/* ── park-field.css ── */
/* park-field / park-notice (#147) — form primitives. Lifted 1:1 from
 * varaa.astro's scoped styles (de-dup, not redesign). Includes .bk-check and
 * .bk-btn (sibling form classes the booking/contact forms use) so a migrated
 * page can drop its scoped copies. Auto-bundled into _ds_components.css. */

.bk-field {
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
}
.bk-field--inline {
	max-width: 220px;
}
/* Board vernacular (anton/forrest#1409, spec `board-visual-language`
 * FR-16/FR-15): labels, hints and inputs take the board's ink and card. The
 * grey `--c-muted` these carried is below AA on the cream surface (NFR-1), and
 * it is the same grey FR-15 already retired from listed content. */
.bk-field > span {
	font-family: var(--ff-display);
	font-weight: var(--fw-bold);
	letter-spacing: 0.02em;
	text-transform: uppercase;
	font-size: var(--fs-upper);
	color: var(--c-board-title);
}
.bk-field input,
.bk-field select {
	padding: 0.6rem 0.7rem;
	border: 1px solid var(--c-board-card-border);
	border-radius: 10px;
	font: inherit;
	color: var(--c-board-ink);
	background: var(--c-board-card);
}
.bk-field input:focus-visible,
.bk-field select:focus-visible {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 1px;
}
.bk-hint,
.bk-muted {
	color: var(--c-board-title);
	font-size: var(--fs-tiny);
}

.bk-check {
	display: flex;
	gap: 0.6rem;
	align-items: flex-start;
	color: var(--c-body);
}
.bk-check input {
	flex: none;
	margin-top: 0.7rem;
}
.bk-check a {
	color: var(--c-board-title);
}

/* The board has no square corners: the call to action is a filled pill in the
 * board's own ink, the same form its group headers wear. */
.bk-btn {
	display: inline-block;
	padding: 0.8rem 1.6rem;
	border-radius: var(--r-pill);
	align-self: flex-start;
	font-family: var(--ff-display);
	font-size: var(--fs-upper);
	font-weight: var(--fw-bold);
	letter-spacing: 0.02em;
	text-transform: uppercase;
	cursor: pointer;
	border: 2px solid var(--c-board-header-bg);
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
}
.bk-btn:disabled {
	opacity: 0.5;
	cursor: default;
}
.bk-btn:focus-visible {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 2px;
}
.bk-notice {
	margin: 0;
	padding: 0.7rem 0.9rem;
	border-radius: 10px;
	background: var(--c-notice-bg);
	color: var(--c-body);
}
.bk-notice--error {
	background: var(--c-danger-bg);
	color: var(--c-primary);
}


/* ── park-gallery.css ── */
/* park-gallery-grid + park-gallery-card (#149) — the public photo gallery.
 * Reproduces the live GalleryPage.astro styles 1:1 (de-dup, not redesign).
 * Shadows come from the --shadow-* tokens (tokens.css) with NO literal
 * fallback — the ds bundle always ships tokens.css ahead of this file, and a
 * duplicated literal can silently drift from the token (anton/forrest#416).
 * Only the grid/card/empty rules belong here; the .gallery-head /
 * .gallery-intro page header is handled separately.
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

.gallery-grid {
	columns: 3 280px;
	column-gap: 0.85rem;
}
.gallery-item {
	position: relative;
	display: block;
	break-inside: avoid;
	margin: 0 0 0.85rem;
	border-radius: 12px;
	overflow: hidden;
	box-shadow: var(--shadow-sm);
	transition:
		transform 0.15s ease,
		box-shadow 0.15s ease;
}
.gallery-item:hover {
	transform: translateY(-2px);
	box-shadow: var(--shadow-lg);
}
.gallery-item img {
	display: block;
	width: 100%;
	height: auto;
}
.gallery-empty {
	color: var(--c-body);
	font-size: var(--fs-sm);
	text-align: center;
	padding: 2rem 0 3rem;
}


/* ── park-hero.css ── */
/* park-hero (#143) — home page hero band. Reproduces the live HomePage.astro
 * .home-hero* scoped styles 1:1 (de-dup, not redesign). Every colour is a
 * --c-* token from park.css — NO literals, NO var() fallbacks (anton/forrest#865). Auto-concatenated into public/_ds_components.css by
 * scripts/build-ds-bundle.mjs. */

.home-hero {
	position: relative;
	overflow: hidden;
	background: var(--c-hero-bg);
	padding: clamp(3rem, 8vw, 5.5rem) 0;
	/* This band paints itself dark, so the magnifier's readability lift (park.css,
	   THE MAGNIFIER) has to resolve to hero ink here — otherwise the lead and the
	   trust items go near-black on dark green when hovered (anton/forrest#1880).
	   The lead/trust sit at 0.92/0.9 alpha, so lifting to the opaque hero ink is
	   the same "a little brighter" the light surface gets. */
	--c-hover-ink: var(--c-on-hero);
}
/* Forest layer kept separate so we can sharpen ONLY the photo (contrast +
   saturation) without touching the text/card on top. */
.home-hero::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	background:
		linear-gradient(
			120deg,
			var(--c-hero-scrim-from) 0%,
			var(--c-hero-scrim-mid) 52%,
			var(--c-hero-scrim-to) 100%
		),
		url("/park/hero-bg.jpg") center / cover no-repeat;
	filter: contrast(1.12) saturate(1.18);
}
.home-hero .home-hero__inner {
	display: grid;
	grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
	gap: clamp(1.5rem, 4vw, 3rem);
	align-items: center;
	max-width: 1160px;
}
.home-hero__media img {
	display: block;
	width: 100%;
	height: auto;
	border-radius: 18px;
	box-shadow: var(--shadow-xl);
}
.home-hero::after {
	content: "";
	position: absolute;
	inset: 0;
	background: radial-gradient(
		circle at 82% 18%,
		var(--c-hero-glow),
		transparent 45%
	);
	pointer-events: none;
}
.home-hero .park-wrap {
	position: relative;
	z-index: 1;
}
.home-hero__badge {
	display: inline-flex;
	align-items: center;
	gap: 0.45rem;
	background: var(--c-hero-badge-bg);
	border: 1px solid var(--c-hero-badge-border);
	color: var(--c-on-hero);
	font-weight: 700;
	font-size: var(--fs-tiny);
	padding: 0.4rem 0.9rem;
	border-radius: 999px;
	margin-bottom: 1.1rem;
}
.home-hero h1 {
	font-size: var(--fs-hero);
	line-height: 1.05;
	color: var(--c-on-hero);
	margin: 0 0 1rem;
}
.home-hero__lead {
	color: var(--c-on-hero-lead);
	max-width: 52ch;
	font-size: var(--fs-md);
	line-height: 1.5;
	margin: 0 0 1.75rem;
}
.home-hero__cta {
	display: flex;
	gap: 0.8rem;
	flex-wrap: wrap;
}
.home-hero__trust {
	list-style: none;
	margin: 1.5rem 0 0;
	padding: 0;
	display: flex;
	gap: 1.2rem;
	flex-wrap: wrap;
	color: var(--c-on-hero-trust);
	font-size: var(--fs-tiny);
	font-weight: 700;
}
.home-hero__trust li {
	white-space: nowrap;
}

@media (max-width: 900px) {
	/* --bp-lg (was 860px; collapsed per anton/forrest#367) */
	.home-hero .home-hero__inner {
		grid-template-columns: 1fr;
	}
	.home-hero__media {
		order: -1;
		max-width: var(--bp-sm); /* was 540px; collapsed per anton/forrest#367 */
		margin-inline: auto;
	}
}


/* ── park-maintenance-banner.css ── */
/* park-maintenance-banner (#148) — public maintenance notice. Reproduces the
 * live MaintenanceBanner.astro styles 1:1 (de-dup, not redesign). The four
 * banner colours are now --c-maint-* tokens in park.css (anton/forrest#865).
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

.park-maint {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem 0.9rem;
	padding: 0.6rem 1rem;
	font-family: var(--ff-body);
	font-size: var(--fs-tiny);
	line-height: 1.4;
}
.park-maint--active {
	background: var(--c-maint-active-bg);
	color: var(--c-maint-active-ink);
}
.park-maint--soon {
	background: var(--c-maint-soon-bg);
	color: var(--c-maint-soon-ink);
	border-bottom: 1px solid var(--c-maint-soon-border);
}
.park-maint__title {
	font-weight: 700;
}
.park-maint__detail {
	opacity: 0.95;
}


/* ── park-page-layout.css ── */
/* Page-composition layout primitives (anton/forrest#1001, spec
   design-system#FR-17).

   WHY THIS FILE EXISTS. A page card composes several elements into a whole
   page, and FR-17 forbids a page fragment from carrying markup or brand values
   of its own. But the live pages' section spacing and card-grid rules
   (`.home-block`, `.sec-grid`) are declared INSIDE `components/HomePage.astro`'s
   Astro-scoped <style> — scoped CSS is rewritten with a per-component attribute
   selector, so it can never reach a mirror. Without a design-system home for
   them, a page card would either ship unstyled or smuggle page-local CSS into
   the fragment; both are wrong.

   So the layout primitives live here, in the design system, token-only. They
   are deliberately minimal — spacing and grid, nothing branded — because their
   job is to ARRANGE elements that already carry the brand.

   NOTE (follow-up): the live pages still declare their own equivalents
   page-scoped, so the card and the page can drift. Making the public pages
   compose these same primitives is tracked separately — it is a change to
   shipping pages and needs its own browser acceptance. */

/* A vertical page section: spacing plus its heading. Mirrors `.home-block`. */
.pa-block {
	margin-top: 3rem;
}

.pa-block > h2 {
	font-size: var(--fs-h2, 1.8rem);
	color: var(--c-heading);
	margin: 0 0 1.25rem;
}

/* Responsive card grid for section/island cards. Mirrors `.sec-grid`. */
.pa-card-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
	gap: 1rem;
}


/* ── park-rule-board.css ── */
/* park-rule-group + park-rule (anton/forrest#1402) — the park's information-board
 * vernacular: grouped cards on a warm cream surface, each item announced by a
 * pictogram in a filled dark-green circle, dotted separators between items.
 * Reproduces the printed board that stands at the park gate (spec
 * `board-visual-language`, FR-1). Colours are --c-* tokens with NO literal
 * fallback (anton/forrest#865) — the board palette lives in styles/park.css.
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

/* ── The board: groups side by side on wide screens, stacked on a phone ──── */
.board {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
	gap: 1.75rem;
	align-items: start;
}
/* A trailing group that would sit alone in its row spans the full width instead
   of leaving a half-empty line — the printed board does exactly this with
   "Ilmoitathan puutteista". Harmless in the single-column (phone) layout, where
   every card is already full width. */
.board > park-rule-group:last-child:nth-child(odd) {
	grid-column: 1 / -1;
}

/* ── A group card ───────────────────────────────────────────────────────── */
park-rule-group {
	display: block;
}
.board-group,
park-rule-group {
	position: relative;
	background: var(--c-board-card);
	border: 1px solid var(--c-board-card-border);
	border-radius: 22px;
	padding: 1.5rem 1.4rem 1.35rem;
	/* Room for the header pill, which rides up over the card's top edge. */
	margin-top: 1.4rem;
}

/* The header is a dark pill that overhangs the card's top-left corner, with a
   circular icon badge overlapping its left end — the board's signature. */
.board-group__header {
	display: flex;
	align-items: center;
	gap: 0.85rem;
	background: var(--c-board-header-bg);
	border-radius: var(--r-pill);
	padding: 0.55rem 1.4rem 0.55rem 0.6rem;
	margin: -2.6rem 0 1.25rem -0.5rem;
	width: fit-content;
	max-width: calc(100% + 0.5rem);
	box-shadow: var(--shadow-sm);
}
.board-group__badge {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 46px;
	height: 46px;
	border-radius: var(--r-circle);
	background: var(--c-board-badge-bg);
	color: var(--c-board-badge-ink);
	border: 2px solid var(--c-board-header-ink);
}
.board-group__badge .board-icon {
	width: 26px;
	height: 26px;
}
.board-group__title {
	margin: 0;
	color: var(--c-board-header-ink);
	font-family: var(--ff-display);
	font-size: var(--fs-h5);
	line-height: 1.15;
	letter-spacing: 0.02em;
	text-transform: uppercase;
}

/* ── The same pill, standing alone (anton/forrest#1405, FR-11) ───────────── */
/* `park-board-heading` announces a page section that is NOT a rules card, so the
   pill sits in the flow instead of overhanging a card's top edge. Everything
   inside it (.board-group__badge / .board-group__title) is the group header's
   own definition — only the positioning differs. */
park-board-heading {
	display: block;
	margin: 0 0 1.25rem;
}
.board-heading {
	display: inline-flex;
	align-items: center;
	gap: 0.85rem;
	background: var(--c-board-header-bg);
	border-radius: var(--r-pill);
	padding: 0.5rem 1.4rem 0.5rem 0.5rem;
	max-width: 100%;
	box-shadow: var(--shadow-sm);
}

/* ── A dotted-separated list on a board card (anton/forrest#1407, FR-15) ──── */
/* `park-rule + park-rule` gives the rules board its dotted rhythm, but it only
   matches that one tag. Surfaces that list something else — questions, bookings,
   contact rows — take the SAME rhythm from here instead of each page declaring
   its own separator: one definition, one look (US-7). */
.board-list > * + * {
	border-top: 2px dotted var(--c-board-sep);
}

/* ── The items ──────────────────────────────────────────────────────────── */
.board-group__items {
	display: flex;
	flex-direction: column;
}

park-rule {
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: start;
	gap: 0.95rem;
	padding: 1rem 0;
}
/* Dotted separators BETWEEN items only — never a trailing rule under the last
   one, and never above the first (spec edge case: no stray separator). */
park-rule + park-rule {
	border-top: 2px dotted var(--c-board-sep);
}

.board-rule__badge {
	display: grid;
	place-items: center;
	width: 44px;
	height: 44px;
	border-radius: var(--r-circle);
	background: var(--c-board-badge-bg);
	color: var(--c-board-badge-ink);
}
.board-rule__badge .board-icon {
	width: 24px;
	height: 24px;
}
.board-rule__text {
	min-width: 0;
}
.board-rule__title {
	margin: 0.35rem 0 0.3rem;
	color: var(--c-board-title);
	font-family: var(--ff-display);
	font-size: var(--fs-upper);
	font-weight: var(--fw-bold);
	line-height: 1.2;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	/* Long titles wrap inside the item rather than overflowing (spec edge case). */
	overflow-wrap: anywhere;
}
/* Title-only or body-only items must not leave dead space above the text. */
.board-rule__title:first-child {
	margin-top: 0.5rem;
}
.board-rule__body {
	margin: 0;
	color: var(--c-body);
	font-size: var(--fs-sm);
	line-height: var(--lh-body);
}
.board-rule__body:first-child {
	margin-top: 0.6rem;
}

@media (max-width: 480px) {
	/* -- bp-sm: keep the icon column, shrink the gutter rather than the copy. */
	park-rule {
		gap: 0.7rem;
	}
	.board-group,
	park-rule-group {
		padding: 1.35rem 1.05rem 1.15rem;
	}
	.board-group__header {
		padding-right: 1rem;
	}
}


/* ── park-section-card.css ── */
/* park-section-card (#142) — island/section card from the home grid. Reproduces
 * the live HomePage.astro `.sec-card` styles 1:1 (de-dup, not redesign). Colours
 * are --c-* tokens with NO literal fallback (anton/forrest#865). `.sec-grid` stays page layout, not lifted here.
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

.sec-card {
	border: 1px solid var(--c-border);
	border-radius: 10px;
	padding: 1.25rem;
	background: var(--c-surface);
}
.sec-photo {
	display: block;
	width: 100%;
	/* The frame is an ASPECT RATIO, never a fixed height (spec frontend FR-20,
	 * anton/forrest#1445). The park photos are 16:9; with `height: 180px` the
	 * visible crop was a function of the card's width — tolerable in a two-card
	 * grid (~430px wide), but once a single active island filled the row
	 * (~1090px) that same 180px left a ~6:1 strip and cut the hut out of its
	 * own photo. Matching the source ratio shows the whole frame at every
	 * width, and makes the crop independent of how many islands are active. */
	aspect-ratio: 16 / 9;
	height: auto;
	object-fit: cover;
	border-radius: 8px;
	margin: 0 0 0.9rem;
}
.sec-card h3 {
	margin: 0 0 0.4rem;
	color: var(--c-heading);
}
.sec-desc {
	color: var(--c-body);
	margin: 0 0 0.4rem;
}
.sec-cap {
	color: var(--c-muted);
	font-size: var(--fs-tiny);
	margin: 0 0 0.6rem;
}
.sec-price {
	margin: 0.15rem 0;
	font-size: var(--fs-md);
	color: var(--c-heading);
}
.sec-price--alt {
	font-size: var(--fs-xs);
}
.sec-muted {
	color: var(--c-muted);
	font-weight: var(--fw-medium);
}


/* ── park-stage.css ── */
/* <park-stage> — one stage of the staged booking flow (anton/forrest#1387,
 * booking-flow-ux FR-10/FR-10a/FR-10d).
 *
 * Three states, driven by `data-state` which the page script sets:
 *   open     — the stage the customer is filling in
 *   folded   — finished; header shows the chosen value and reopens on activate
 *   upcoming — not reached yet
 *
 * The three MUST be separable without colour (FR-10 inherits the flow's
 * no-colour-only rule): open is a raised surface with a filled step number,
 * folded is flat with a check and its value, upcoming is dimmed with a hollow
 * number. Weight, fill and content differ, not just hue.
 *
 * Board vernacular (anton/forrest#1409, spec `board-visual-language`
 * FR-16/FR-18): a stage IS a board card, and its header IS the board's dark
 * pill with a circular badge — the same signature `.board-group__header`
 * carries on the rules board (wc/css/park-rule-board.css). The step number
 * takes the badge's place, so the flow reads as the park's own board rather
 * than a generic wizard. The three states keep their non-colour separation and
 * gain one more: OPEN wears the filled pill, folded a flat pill with a check,
 * upcoming a dotted outline with a hollow badge. */

.stage {
	border: 1px solid var(--c-board-card-border);
	border-radius: 22px;
	background: var(--c-board-card);
	/* Room for the header pill, which rides up over the card's top edge. */
	margin: 1.4rem 0 0.75rem;
}
.stage[data-state="open"] {
	box-shadow: var(--shadow-sm);
}
.stage[data-state="upcoming"] {
	opacity: 0.55;
}

/* Wraps the heading and the back control so the latter can be positioned
   against the header without living INSIDE the `<h2>`, where it would become
   part of the heading's accessible name. Layout-neutral: no box of its own, so
   the toggle's negative margins still pull the pill over the card corner. */
.stage-headrow {
	position: relative;
}
.stage-head {
	margin: 0;
	font-size: var(--fs-xs);
}
/* The whole header is the control (FR-10a): a folded stage must be reopenable
 * by pointer and keyboard alike, so the summary IS the button. It overhangs the
 * card's top-left corner exactly as the board's group header does. */
.stage-toggle {
	width: calc(100% + 0.5rem);
	display: flex;
	align-items: center;
	gap: 0.85rem;
	padding: 0.4rem 1.4rem 0.4rem 0.5rem;
	margin: -1.4rem 0 0.5rem -0.5rem;
	border: 2px solid var(--c-board-header-bg);
	border-radius: var(--r-pill);
	background: var(--c-board-card);
	font: inherit;
	text-align: start;
	color: var(--c-board-title);
	cursor: pointer;
}
/* The stage being filled in wears the board's FILLED pill — the strongest
 * signal on the page, and what the badge's own contrast is tuned for. */
.stage[data-state="open"] .stage-toggle {
	background: var(--c-board-header-bg);
	color: var(--c-board-header-ink);
	box-shadow: var(--shadow-sm);
}
.stage[data-state="upcoming"] .stage-toggle {
	cursor: default;
	border-style: dotted;
	border-color: var(--c-board-sep);
}
.stage-toggle:focus-visible {
	outline: 3px solid var(--c-board-ink);
	outline-offset: 2px;
}

/* The board's circular badge, carrying the step number instead of a pictogram
 * — the definition `.board-group__badge` uses. */
.stage-num {
	flex: 0 0 auto;
	width: 2.4rem;
	height: 2.4rem;
	border-radius: var(--r-circle);
	border: 2px solid var(--c-board-header-ink);
	background: var(--c-board-badge-bg);
	color: var(--c-board-badge-ink);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-family: var(--ff-display);
	font-size: var(--fs-tiny);
	font-weight: var(--fw-bold);
}
/* Folded carries a check INSTEAD of the digit — a completed stage is
 * recognisable at a glance without reading, and without relying on colour. */
.stage[data-state="folded"] .stage-num::after {
	content: "\2713";
}
.stage[data-state="folded"] .stage-num {
	font-size: var(--fs-sm);
}
/* Upcoming: a HOLLOW badge — the digit in board ink on the card, no fill. The
 * shape differs from both other states before any colour is read. */
.stage[data-state="upcoming"] .stage-num {
	background: none;
	color: var(--c-board-title);
	border-color: var(--c-board-sep);
	border-style: dotted;
}

.stage-title {
	font-family: var(--ff-display);
	font-size: var(--fs-upper);
	font-weight: var(--fw-bold);
	letter-spacing: 0.02em;
	text-transform: uppercase;
	line-height: 1.15;
	flex: 0 0 auto;
}
/* The chosen value — the whole point of folding (FR-10a). Truncates rather
 * than wrapping so a long value can never push the edit affordance off-row. */
.stage-summary {
	flex: 1 1 auto;
	min-width: 0;
	color: inherit;
	font-weight: 500;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.stage-summary[data-empty="true"] {
	display: none;
}
/* Folded only — the same rule `.stage-edit` has always had, and what this
 * element is FOR ("the chosen value, shown once folded"). On the OPEN stage the
 * value is already stated in full by the stage's own body a few pixels below,
 * so a copy of it in the header is a second voice saying the same thing. */
.stage:not([data-state="folded"]) .stage-summary {
	display: none;
}
/* Inherits the pill's ink rather than the page's grey — grey on cream drops
 * below AA (NFR-1), and inside the filled pill it would vanish outright. */
.stage-edit {
	flex: 0 0 auto;
	font-size: var(--fs-tiny);
	color: inherit;
	text-decoration: underline;
}
.stage:not([data-state="folded"]) .stage-edit {
	display: none;
}

/* Back — return to the previous stage from the stage you are IN, so correcting
 * an earlier choice does not mean hunting for its folded header. Only on stages
 * that were given a `backLabel`, and only while that stage is open: on a folded
 * stage the header is already the way back, and `.stage-edit` says so. */
.stage-back {
	position: absolute;
	top: 50%;
	right: 0.9rem;
	transform: translateY(-50%);
	z-index: 1;
	padding: 0.15rem 0.6rem;
	border: 1px solid currentcolor;
	border-radius: var(--r-pill);
	background: transparent;
	font: inherit;
	font-size: var(--fs-tiny);
	color: var(--c-board-header-ink);
	cursor: pointer;
}
/* Inverts against the filled pill rather than washing it — a translucent white
   would be a colour literal outside the token cascade (spec FR-2). */
.stage-back:hover {
	background: var(--c-board-card);
	color: var(--c-board-header-bg);
}
.stage-back:focus-visible {
	outline: 3px solid var(--c-board-card);
	outline-offset: 2px;
}
.stage:not([data-state="open"]) .stage-back {
	display: none;
}
/* Reserve the room rather than letting a long title slide under the control.
   Keyed off the class the component stamps, so no `:has()` is needed. */
.stage.has-back[data-state="open"] .stage-toggle {
	padding-right: 6.5rem;
}

.stage-body {
	padding: 0.35rem 1.4rem 1.35rem;
}
.stage[data-state="folded"] .stage-body,
.stage[data-state="upcoming"] .stage-body {
	display: none;
}

/* FR-10d — the advance transition. Deliberately short, and it animates only
 * the ARRIVING stage: nothing here encodes information, so removing it (below)
 * costs nothing but the flourish. */
@media (prefers-reduced-motion: no-preference) {
	.stage[data-state="open"] .stage-body {
		animation: stage-slide-in 180ms ease-out;
	}
}
@keyframes stage-slide-in {
	from {
		opacity: 0;
		transform: translateY(-6px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* FR-10b — the warning shown before a reopen discards a later choice. */
.stage-invalidates {
	margin: 0.5rem 0 0;
	padding: 0.5rem 0.7rem;
	border-radius: 8px;
	background: var(--c-avail-partial-bg);
	color: var(--c-avail-partial-ink);
	font-size: var(--fs-tiny);
}


/* ── park-steps.css ── */
/* park-steps (#144) — the "Näin se toimii / How it works" numbered step grid.
 * Reproduces the live HomePage.astro .step-grid/.step-card/.step-num styles 1:1
 * (de-dup, not redesign; .step-num is a circle). Colours are --c-* tokens with
 * NO literal fallback (anton/forrest#865).
 * Auto-concatenated into public/_ds_components.css by scripts/build-ds-bundle.mjs. */

.step-grid {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 1rem;
	counter-reset: step;
}
.step-card {
	border: 1px solid var(--c-border);
	border-radius: 10px;
	padding: 1.4rem;
	background: var(--c-surface);
}
.step-num {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2rem;
	height: 2rem;
	border-radius: 50%;
	background: var(--c-secondary);
	color: var(--c-on-primary);
	font-weight: 700;
	margin-bottom: 0.75rem;
}
.step-card h3 {
	margin: 0 0 0.4rem;
	color: var(--c-heading);
	font-size: var(--fs-sm);
}
.step-card p {
	margin: 0;
	color: var(--c-body);
}

@media (max-width: 768px) {
	/* --bp-md (was 700px; collapsed per anton/forrest#367) */
	.step-grid {
		grid-template-columns: 1fr;
	}
}

