/* Cardbase's half of the stylesheet — the palettes, and everything that knows
   what a person, an occasion or a design looks like. The other half is
   js/shell/shell.css, which is app-agnostic and is going to a sibling repo;
   docs/architecture.md → "The split" is the reasoning for both.

   IT LOADS SECOND, AND THAT IS A CONTRACT. index.html links shell.css first, so
   at EQUAL specificity a rule here wins. A unit test pins the order.

   THE RESET AND THE BARE ELEMENT RULES ARE NOT HERE. `*`, `html, body`, the
   touch/selection behaviour and bare `label` / `input` / `textarea` live in the
   shell's reboot block, because they have to load before both halves to keep
   their specificity ties resolving — that block's header has the detail.

   THE HOVER GUARD APPLIES HERE TOO. Every `:hover` rule below carries
   `:where(:root[data-input="pointer"])`, and js/app/tests/unit/hover-guard.test.mjs
   fails if one opts out. `data-input` is a SHELL contract — written by
   js/shell/pointer-input.js, defaulted in index.html's markup — so this half
   depends on it exactly as it depends on the shell's `data-theme` blocks. The
   full argument, including why `:where()` is load-bearing rather than
   decoration, is stated once, in shell.css beside the guard's own components.

   THE PALETTE BELOW IS A CONTRACT THE SHELL READS. shell.css consumes ~22 of
   these custom properties and defines none of them, so a palette that drops one
   breaks the shell silently. js/app/tests/unit/css-tokens.test.mjs is what makes
   that loud. The panel metrics (--slot-*, --panel-gap*) run the other way: the
   shell publishes them and rules here read them. */

/* ---- Theme tokens -------------------------------------------------------
   Five palettes, named for the paper they're made of. Two PAIRS — Mulberry and
   Kraft, each in a light and a dark — plus Plum, the house palette, which is
   the app's original look and has no light half.

       Mulberry Light   cream ground, wine ink      (was `light`)
       Mulberry Dark    near-black ground, rose ink  (the default, so :root)
       Kraft Light      tan ground, umber ink
       Kraft Dark       dull brown ground, gold ink
       Plum             deep plum ground, gold ink  (was `dark`)

   A pair shares its INK, not its ground: Mulberry's two halves both run the
   wine hue (329° and 332°), Kraft's both run gold. That's what makes the light
   and dark halves read as the same theme rather than two themes that happen to
   sit next to each other. Plum breaks it — plum ground, gold ink — which is
   why it's unpaired rather than "Mulberry Bold".

   Mulberry Dark holds bare `:root`, so a first visit and any stored value we
   don't recognise land on it. That block and DEFAULT_THEME in js/app/config.js
   are ONE fact written twice and must agree: theme-bootstrap.js only sets
   data-theme when something is stored, so a first paint comes from bare `:root`
   and theme.js only normalises it once the modules have loaded. Point the two
   at different palettes and a first visit paints one and then repaints to the
   other. js/shell/theme.js says the same thing from the other side.

   Every block is also keyed to [data-theme-sample], so a swatch inside the
   theme picker can wear a palette the page isn't wearing (js/shell/theme.js).
   That's the whole trick behind the side-by-side samples: no duplicated hexes,
   no canvas, just the same tokens scoped to an element instead of the root.

   Cardbase palette: ink-and-paper with a dusty-rose accent — deliberately
   distinct from its sibling fiber-kit (which is green/mustard). */

/* Tell the UA which theme it's painting, so native scrollbars, form-control
   popups and inset shadows are drawn to match — not the light default that
   otherwise shows a pale scrollbar against the plum. Keyed on data-theme (set
   by theme.js + the pre-paint bootstrap), so it follows the in-app choice, not
   the OS. Declared apart from the token blocks below because those blocks also
   apply to sample swatches, and a nested color-scheme would repaint the UA
   chrome of whatever is drawn inside them. */
:root,
:root[data-theme="plum"],
:root[data-theme="mulberry-dark"],
:root[data-theme="kraft-dark"] { color-scheme: dark; }

:root[data-theme="mulberry-light"],
:root[data-theme="kraft-light"] { color-scheme: light; }

:root[data-theme="plum"],
[data-theme-sample="plum"] {
	--bg: #211829;            /* deep plum (icon ground) */
	--surface: #2c2137;
	--surface-2: #392b45;
	--surface-3: #493653;
	--border: #573d54;        /* wine-tinted divider */
	--text: #f2ece0;          /* cream (icon) */
	--text-muted: #b5a6b0;
	--text-faint: #7d7083;
	--accent: #dbb257;        /* gold (icon): headings / labels */
	--primary: #dbb257;       /* gold (icon): active / actions */
	--primary-hover: #e7c56d;
	--on-primary: #241a2e;
	--value: #7fc9a9;         /* mint (icon): result values / active nav */
	--connected: #7fc9a9;     /* mint (icon): cloud sync active */
	/* Burnt orange: destructive. --on-danger OPPOSES the swatch rather than being
	   white everywhere — on a bright orange, white is 3.12:1 and this plum-black
	   is 5.34:1. See docs/ui-conventions.md → Themes. */
	--danger: #e0743f;
	--on-danger: #241a2e;
	--input-bg: #1a1222;
	/* The brand mark's plinth. On a dark ground the mark needs none — its four
	   quads sit straight on the rail — so this is transparent in every dark
	   palette and the shadow goes with it (a shadow with no card behind it draws
	   a floating rectangle). The light palettes keep the plum card, where it's
	   load-bearing: the mark's cream quad is 1.15:1 against cream and would
	   vanish without it. */
	--mark-ground: transparent;
	--mark-shadow: transparent;
	--shadow: rgba(0,0,0,0.4);
	--style-fem: #db8fac;     /* rose: feminine card style (birthday icon tint) */
	--style-masc: #57b1a7;    /* teal: masculine card style */
	--style-neutral: #d9cfcc; /* subtle: anniversary + remembrance icons, softer than the name */
	--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%23b5a6b0' d='M6 8L0 0h12z'/></svg>");

	/* Circle-identity palette (docs/circle-identity.md): 11 names a user can pin to
	   a Circle for its marker/chip. Stored value is the NAME; --circle-<name> maps
	   it to a per-theme swatch, so the audit retunes here without touching data.
	   Chosen clear of the reserved meanings (rose=fem, teal=masc, gold=primary,
	   mint=value, orange=danger). */
	--circle-red: #d97a6c;
	--circle-copper: #c98f5f;
	--circle-olive: #a9b061;
	--circle-green: #7fb56b;
	--circle-blue: #7aa9d8;
	--circle-violet: #a78fd6;
	--circle-berry: #c77bc2;
	--circle-slate: #8fa0ad;
	--circle-cocoa: #a97e55;
	--circle-lemon: #ddd35c;
	--circle-tangerine: #f29c4f;
}

:root[data-theme="mulberry-light"],
[data-theme-sample="mulberry-light"] {
	--bg: #f8f1e4;
	--surface: #fffcf2;
	--surface-2: #f1e7d4;
	--surface-3: #e8dbc2;
	--border: #e0d2b8;
	--text: #322c3a;
	--text-muted: #6f6678;
	/* Darkened from #a49caf, hue and saturation held: the tertiary tier reads as
	   quiet on the darks and as washed-out lavender on cream. 2.35:1 → 3.59:1 on
	   --bg, joining the darks' 3.68–4.15 band. Themes → Contrast. */
	--text-faint: #857a94;
	--accent: #7c3f5f;        /* wine-plum (icon): headings / labels on cream */
	--primary: #7c3f5f;       /* warm wine-plum: active / actions */
	--primary-hover: #693451;
	--on-primary: #ffffff;
	--value: #3f8f6c;
	--connected: #3f8f6c;     /* deep mint: cloud sync active */
	/* Pulled two steps darker than the #cc5803 it started as: that value sat
	   mid-tone, where NEITHER ink passed (white 4.23, its own --text 3.19). Same
	   hue 25°, same saturation, three points of value — white now 5.05, and as
	   text on --bg it clears 4.5 for the first time. */
	--danger: #b84f02;
	--on-danger: #ffffff;
	--input-bg: #fffcf1;
	--mark-ground: #211829;   /* stays plum: the mark's cream quad needs a dark ground */
	--mark-shadow: rgba(60,40,60,0.14);
	--shadow: rgba(60,40,60,0.12);
	--style-fem: #b85c85;     /* deeper rose on cream: feminine card style */
	--style-masc: #2f8c82;    /* deeper teal: masculine card style */
	--style-neutral: #4d4557; /* subtle: anniversary + remembrance icons, softer than the name */
	--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%236f6678' d='M6 8L0 0h12z'/></svg>");

	/* Circle-identity palette — light-theme swatches (deeper, for cream grounds). */
	--circle-red: #b0453a;
	--circle-copper: #96612f;
	--circle-olive: #6f7530;
	--circle-green: #3f7d34;
	--circle-blue: #35679f;
	--circle-violet: #6a4aa8;
	--circle-berry: #94408f;
	--circle-slate: #5a6b78;
	--circle-cocoa: #6e4e2a;
	--circle-lemon: #9d940f;
	--circle-tangerine: #c06a15;
}

/* Mulberry Dark — the light half photographed as a negative. Its cream paper
   becomes the ink; its wine becomes the rose, lifted until it clears 6:1 on a
   near-black ground. One identity colour doing --accent and --primary at once,
   exactly as wine does in Mulberry Light: no gold anywhere in the interface
   (the brand mark keeps its own, but those four quads are fixed hexes).
   The ground is the plum's hue at 11% saturation against Plum's 26% — a trace
   of the brand in the neutral rather than a wash of it.
   It also holds bare `:root` — see the fallback note at the top of this file. */
:root,
:root[data-theme="mulberry-dark"],
[data-theme-sample="mulberry-dark"] {
	--bg: #181419;
	--surface: #221d24;
	--surface-2: #2d262f;
	--surface-3: #39303b;
	--border: #403542;
	--text: #f2ece0;          /* Mulberry Light's paper, now its ink */
	--text-muted: #b3a8b2;
	--text-faint: #7e747f;
	--accent: #d087a9;        /* the wine #7c3f5f, lifted to clear 6.7:1 */
	--primary: #d087a9;
	--primary-hover: #e09cba;
	--on-primary: #241a26;
	--value: #7fc9a9;
	--connected: #7fc9a9;
	--danger: #e0743f;
	--on-danger: #241a26;     /* opposes the swatch: 5.39:1, where white is 3.12 */
	--input-bg: #120f13;
	--mark-ground: transparent;
	--mark-shadow: transparent;
	--shadow: rgba(0,0,0,0.5);
	/* Lifted a full 17 lightness points off --accent, because the rose that
	   tints feminine birthday icons and the rose that means "interactive" would
	   otherwise sit 5° apart in the same row. That 17-point gap is not a new
	   invention: it's the exact separation Mulberry Light already runs between
	   its wine #7c3f5f (L37) and its own fem tint #b85c85 (L54). Same problem,
	   same solution, solved upward instead of downward. */
	--style-fem: #f0bcd0;
	--style-masc: #57b1a7;
	--style-neutral: #d9cfcc;
	--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%23b3a8b2' d='M6 8L0 0h12z'/></svg>");

	/* Circle-identity palette — the dark set, with berry pushed violet-ward so
	   a Circle chip can't be mistaken for the rose that means "interactive". */
	--circle-red: #d97a6c;
	--circle-copper: #c98f5f;
	--circle-olive: #a9b061;
	--circle-green: #7fb56b;
	--circle-blue: #8fb6e0;
	--circle-violet: #a78fd6;
	--circle-berry: #b98fd0;
	--circle-slate: #8fa0ad;
	--circle-cocoa: #a97e55;
	--circle-lemon: #ddd35c;
	--circle-tangerine: #f29c4f;
}

/* Kraft Dark — brown paper in shadow. The ground is hue 20 at 12% saturation,
   and both numbers are the point: an earlier pass at hue 25 / 25% sat sat too
   close to the gold in both hue and chroma, so the accent read as a lighter
   shade of the page rather than a colour on it. Draining the ground fixed it
   without touching a single accent, which is why this palette carries Plum's
   gold, mint and burnt orange completely unchanged. */
:root[data-theme="kraft-dark"],
[data-theme-sample="kraft-dark"] {
	--bg: #1d1917;
	--surface: #272220;
	--surface-2: #322d29;
	--surface-3: #3e3833;
	--border: #47403a;
	--text: #f2ece0;
	--text-muted: #b5a99e;
	--text-faint: #847a70;
	--accent: #dbb257;
	--primary: #dbb257;
	--primary-hover: #e7c56d;
	--on-primary: #1d1917;
	--value: #7fc9a9;
	--connected: #7fc9a9;
	/* Burnt orange shares this ground's hue and still reads as an alarm, because
	   it carries 72% saturation against the ground's 12%. Chroma separates them
	   where hue can't — which is the same trick the ground itself is built on. */
	--danger: #e0743f;
	--on-danger: #1d1917;     /* opposes the swatch: 5.60:1, where white is 3.12 */
	--input-bg: #161311;
	--mark-ground: transparent;
	--mark-shadow: transparent;
	--shadow: rgba(0,0,0,0.55);
	--style-fem: #db8fac;
	--style-masc: #57b1a7;
	--style-neutral: #e2d8cc;
	--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%23b5a99e' d='M6 8L0 0h12z'/></svg>");

	/* Circle-identity palette — the dark set. Only cocoa moves: it's the one
	   swatch in the same hue family AND the same low chroma as this ground, so
	   it dropped to 4.82:1 and read as page rather than marker. Copper and
	   tangerine share the hue but carry enough saturation to hold on their own. */
	--circle-red: #d97a6c;
	--circle-copper: #c98f5f;
	--circle-olive: #a9b061;
	--circle-green: #7fb56b;
	--circle-blue: #7aa9d8;
	--circle-violet: #a78fd6;
	--circle-berry: #c77bc2;
	--circle-slate: #8fa0ad;
	--circle-cocoa: #c09a72;
	--circle-lemon: #ddd35c;
	--circle-tangerine: #f29c4f;
}

/* Kraft Light — the same brown paper in daylight, with the gold inverted into
   a deep umber. A tan ground sits four lightness points below cream, and every
   accent pays for that, so this palette's tints are drawn deeper rather than
   borrowed from Mulberry Light — which is why each one actually beats its
   equivalent there (value 4.05 vs 3.49, danger 4.72 vs 4.50, fem 4.55 vs 3.81).
   The danger pair is the narrowest of the three: both lights were pulled deeper
   to clear 4.5 as text, so this one had to move too in order to stay ahead. */
:root[data-theme="kraft-light"],
[data-theme-sample="kraft-light"] {
	--bg: #f0e6d4;
	--surface: #fbf5e9;
	--surface-2: #e6dac4;
	--surface-3: #dbcdb3;
	--border: #d8c9ac;
	--text: #2b2620;
	--text-muted: #6b6152;
	/* Darkened from #9b9081 for the same reason as Mulberry Light's, and by the
	   same method — lightness only. 2.53:1 → 3.56:1 on --bg. Themes → Contrast. */
	--text-faint: #827767;
	--accent: #6b4a10;        /* Kraft Dark's gold, inverted into umber */
	--primary: #6b4a10;
	--primary-hover: #55390b;
	--on-primary: #fbf5e9;
	--value: #357c5d;
	--connected: #357c5d;
	--danger: #b13f18;        /* pulled red, clear of the ground's warmth */
	--on-danger: #fbf5e9;     /* opposes the swatch: cream, 5.38:1 */
	--input-bg: #fbf6ec;
	--mark-ground: #211829;
	--mark-shadow: rgba(70,55,35,0.16);
	--shadow: rgba(70,55,35,0.14);
	--style-fem: #a8456b;
	--style-masc: #2a7a70;
	--style-neutral: #4a4034;
	--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%236b6152' d='M6 8L0 0h12z'/></svg>");

	/* Circle-identity palette — the light set, which is already drawn for warm
	   grounds; only cocoa and copper are near this ground's hue, and both carry
	   enough depth against a L89 paper to stay legible. */
	--circle-red: #b0453a;
	--circle-copper: #96612f;
	--circle-olive: #6f7530;
	--circle-green: #3f7d34;
	--circle-blue: #35679f;
	--circle-violet: #6a4aa8;
	--circle-berry: #94408f;
	--circle-slate: #5a6b78;
	--circle-cocoa: #6e4e2a;
	--circle-lemon: #9d940f;
	--circle-tangerine: #c06a15;
}

/* Theme-independent tokens — the same in both palettes, so they sit outside the
   two blocks above rather than being duplicated into each.

   --icon-weight is the outline icon language's ONE line weight, in CSS px.

   It needs saying because SVG stroke-width is normally in viewBox units, so a
   flat stroke renders LIGHTER the smaller the icon is drawn. The set used to
   declare one stroke per family and vary only the size, which meant the same
   circle glyph rendered a 0.94px line in a 15px person-detail chip and a 1.38px
   line in a 22px title mark — one mark, 47% apart — while a 24px occasion icon
   in a detail header came out at 1.65px, 76% heavier than a circle chip near
   it. `vector-effect: non-scaling-stroke` takes the stroke out of user space
   and into screen space, so this single value IS the rendered weight at every
   size and a new size can't get it wrong — the same "correct by existing" move
   the drawer forms made when they stopped enumerating classes.

   1.375px is not a new choice: it's what the Upcoming row already rendered
   (20px at stroke 1.65) and what the v41 walkthrough approved, so the reference
   icon is unchanged and everything else moves to meet it.

   Deriving it arithmetically instead — `calc(33 / var(--icon-px))` — was built
   and rejected: Chromium resolves it, but GECKO REFUSES calc() with a var()
   divisor and silently falls back to stroke-width 1. Don't reintroduce it. */
:root {
	--icon-weight: 1.375;
}

/* Text for assistive technology only. Where a visual cue carries meaning that a
   screen reader cannot see — an unlogged past row says "nothing was recorded"
   by drawing NO control, which is silence to a screen reader — the sentence has
   to be said somewhere. Clipped rather than `display: none` or `visibility:
   hidden`, both of which remove it from the accessibility tree as well. */
.u-visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---- Import preview (js/app/import-ui.js, in the shell drawer) ---------- */
.import-panel { display: flex; flex-direction: column; gap: 0.7rem; }

.import-file { font-size: 0.82rem; color: var(--text-muted); word-break: break-all; }

.import-heading { font-size: 0.9rem; font-weight: 600; }

.import-problem { color: var(--danger); }

.import-note { font-size: 0.85rem; color: var(--text-muted); }

.import-detail summary { cursor: pointer; font-size: 0.92rem; padding: 0.15rem 0; }

.import-names { margin: 0.25rem 0 0.4rem; padding-left: 1.3rem; font-size: 0.85rem; color: var(--text-muted); }

.import-names li { padding: 0.08rem 0; }

/* Type-to-confirm, for the one irreversible thing the app can do to itself. The
   input inherits the global input[type="text"] rule, which is 1.1rem — it must
   stay ≥16px or mobile Safari zooms the page on focus (docs/ui-conventions.md
   → Mobile viewport). */
.import-confirm { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.4rem; }

.import-confirm label { font-size: 0.85rem; color: var(--text-muted); }

.import-issues { margin: 0.25rem 0 0; padding-left: 1.1rem; font-size: 0.85rem; color: var(--text-muted); }

.import-issues li { padding: 0.15rem 0; }

.person-list {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

/* The demo offer, in Upcoming's nothing-yet state and nowhere else — the only
   empty message in the app that carries a control. It sits inside .sh-empty, so
   the parent's text-align is what centres the box on the line; the inline-flex is
   for what is INSIDE the box, so the label sits in the middle of a target taller
   than itself rather than at the top of it.

   Its own class rather than .link-add, which is the app's other quiet text
   button: that one is form furniture (`align-self: flex-start`, left-aligned
   under a field), and reusing it here would centre it by accident of the parent
   while its declarations went on describing a layout it is not in. Primary
   colour, because unlike .sh-link-alt this is the offer rather than the
   afterthought — it is why the line beneath the message exists.

   44pt, stated as `min-height` for the reason a timeline row states it that way:
   it is the intent, not arithmetic that holds while the label is three words. It
   costs nothing to spend the height here — the screen it appears on is empty by
   definition — so it needs none of the negative-margin trick the pane head uses
   to buy the same target back out of padding, and it is not gated on
   `(pointer: fine)`: this is the first control a new phone owner reaches for. */
.occ-empty-demo {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: 2.75rem;
	margin-top: 0.35rem;
	background: none;
	border: none;
	color: var(--primary);
	font-size: 0.95rem;
	font-family: inherit;
	cursor: pointer;
	padding: 0 0.75rem;
}

:where(:root[data-input="pointer"]) .occ-empty-demo:hover { text-decoration: underline; }

.occ-list { display: flex; flex-direction: column; }

.occ-month {
	font-size: 0.72rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--text-muted);
	padding: 0.9rem 0.25rem 0.35rem;
}

.occ-month:first-child { padding-top: 0.1rem; }

/* The year on non-current-year month headers, quieter than the month itself. */
.occ-month-year { color: var(--text-faint); }

/* The present month is the "you are here" band: while every other header sits
   quiet in muted grey, the current one wears the accent over a soft accent wash —
   a lighter cue than boxing the whole month, and it pairs with the Today pill. */
.occ-month.is-current {
	color: var(--accent);
	background: color-mix(in srgb, var(--accent) 10%, transparent);
	border-radius: 0.5rem;
	padding-top: 0.4rem;
	padding-bottom: 0.4rem;
	margin: 0.35rem 0 0.15rem;
}

.occ-month.is-current .occ-month-year { color: color-mix(in srgb, var(--accent) 70%, var(--text-faint)); }

.occ-today-pill {
	position: absolute;
	bottom: 1rem;
	left: 50%;
	transform: translateX(-50%);
	z-index: 2;
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	padding: 0.4rem 0.9rem;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--surface-2);
	color: var(--text);
	font-size: 0.8rem;
	font-weight: 600;
	cursor: pointer;
	box-shadow: 0 4px 24px var(--shadow);
}

.occ-today-pill[hidden] { display: none; }

.occ-today-pill svg {
	width: 0.85rem;
	height: 0.85rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.occ-today-pill.is-down svg { transform: rotate(180deg); }

/* Row = a flex container of two sibling buttons: the leading status control and
   the selectable body (nesting buttons would be invalid HTML). */
.occ-row {
	display: flex;
	align-items: center;
	border-radius: 0.5rem;
	transition: background 0.15s;
}

:where(:root[data-input="pointer"]) .occ-row:hover { background: var(--surface-2); }

.occ-row:active { background: var(--surface-3); transition-duration: 0s; }

.occ-row.is-selected { background: color-mix(in srgb, var(--value) 12%, transparent); }

/* Leading status control — tap advances the card through the pipeline.
   2.75rem tall, matching .occ-select below, so the control fills the row's full
   height. At 2.2rem it left a ~4px dead strip top and bottom once the row grew:
   .occ-row itself carries no click handler (the two buttons are siblings, not a
   wrapper), so a tap landing there does nothing at all rather than falling
   through to the row. The WIDTH stays 2rem deliberately — taking it to a square
   44 would push the day column 12px right and re-lay-out the row horizontally,
   which is a different decision from row height. */
.occ-status {
	flex-shrink: 0;
	width: 2rem;
	height: 2.75rem;
	display: grid;
	place-items: center;
	background: transparent;
	border: none;
	padding: 0;
	cursor: pointer;
	color: var(--text-faint);
}

.occ-status svg { width: 1.2rem; height: 1.2rem; display: block; }

.occ-status.is-in-progress, .occ-status.is-made { color: var(--accent); }

.occ-status.is-sent { color: var(--value); }

:where(:root[data-input="pointer"]) .occ-status:hover { color: var(--text); }

:where(:root[data-input="pointer"]) .occ-status.is-sent:hover { color: var(--value); }

/* The shared read-only status token — the same mark as the button above, plus a
   label — rendered by statusToken() (js/app/cardStatus.js) everywhere card
   status is DISPLAYED: the Cards roster tile, the People card history, the
   Circles season rows. Those three each had their own treatment before (a
   filled pill, plus two copies of a plain-text one), and the pill had `made`
   mint and `sent` gold — the two states swapped relative to every other status
   display in the app, so one card read two different colours depending which
   view you found it in. That's TODO #13.
   The CONTAINER around the token still differs by context — a roster is a tile,
   a log is a dense table row (v42's roster-vs-log rule); only the token is
   shared. The status EDITOR is a separate thing and stays a segmented control. */
.status-token {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	gap: 0.3rem;
	font-size: 0.8rem;
	white-space: nowrap;
	color: var(--text-muted);
	/* The `sent` mark knocks its check out of a filled disc, so the check has to
	   be painted in whatever sits BEHIND it. Default to the page ground, which is
	   right for the dense log rows; the tile contexts override below. */
	--mark-knockout: var(--bg);
}

/* Tiles sit on --surface, so the knockout has to match the tile, not the page. */
.card-row .status-token,
.circle-member .status-token { --mark-knockout: var(--surface); }

.status-token svg { width: 0.875rem; height: 0.875rem; display: block; flex-shrink: 0; }

.status-token.is-in-progress, .status-token.is-made { color: var(--accent); }

.status-token.is-sent { color: var(--value); }

.status-token.is-skipped { color: var(--text-faint); }

/* Hover repaints the ground under the mark, so the knockout must follow it. */
:where(:root[data-input="pointer"]) .card-row:hover .status-token,
:where(:root[data-input="pointer"]) .hist-link:hover .status-token { --mark-knockout: var(--surface-2); }

/* Enclose-a-cheque in a read-only display: the same glyph and mint as the
   Upcoming row's coin, but WITHOUT the disc — a coin badge means "system nudge"
   in the marker grammar (docs/circle-identity.md), and a card history is
   reporting a fact, not nudging you. Retires the ASCII "✓$" #13 complained of. */
.status-enclose-mark { font-weight: 700; font-size: 0.78rem; color: var(--value); }

/* The rest of the row — opens the detail. */
/* 2.75rem is 44pt — Apple's minimum tap target, and the classic single-line table
   row. The row took its height from this padding alone and measured 36pt, which
   is short as a target and read visibly shorter than People's and Cards' tiles.
   min-height rather than more padding: it states 44pt as the intent instead of
   encoding it as arithmetic that only holds while the occasion icon is 20px.
   It is NOT gated on @media (pointer: fine) like .sh-glyph-btn is. That gate is
   for chrome, where 44pt is wasted space a mouse never needs; a row's height is
   content rhythm, and the complaint it answers — too dense beside the tile views
   — is just as true on the desktop. */
.occ-select {
	flex: 1;
	min-width: 0;
	min-height: 2.75rem;
	display: flex;
	align-items: center;
	gap: 0.55rem;
	text-align: left;
	background: transparent;
	border: none;
	padding: 0.5rem 0.4rem 0.5rem 0.1rem;
	cursor: pointer;
	color: inherit;
}

/* Terminal states read as crossed off: struck name, dimmed row. */
.occ-row.is-done .occ-select, .occ-row.is-skipped .occ-select { opacity: 0.55; }

.occ-row.is-done .occ-name, .occ-row.is-skipped .occ-name { text-decoration: line-through; }

/* The RECORD region. Above the current month the timeline stops being a working
   list and becomes a log, and every row in it steps down one colour — the same
   move, and the same token pair, the archived People section uses.
   Two things are deliberate here:
   - It targets .occ-name, not the .occ-names wrapper. .occ-name declares its own
     `color: var(--text)` below, and a direct declaration beats an inherited one,
     so a rule aimed at the wrapper silently dims only the day number.
   - It SUPPRESSES the terminal alpha above rather than sitting alongside it.
     "Crossed off" is a working-list idea; inside a log where everything already
     happened, dimming the completed rows says nothing and merely compounds —
     alpha multiplies the token, so a sent row went faint enough on the light
     palettes to read as broken, which is the fault the archived rows record.
     The strikethrough stays, so sent and skipped are still told apart. */
.occ-row.is-past .occ-name { color: var(--text-muted); }

.occ-row.is-past .occ-day { color: var(--text-faint); }

.occ-row.is-past.is-done .occ-select,
.occ-row.is-past.is-skipped .occ-select { opacity: 1; }

/* The coin leaves the accent family in here, and it is the ONLY marker that does.
   The region dims what describes this occurrence and leaves what describes the
   occasion, which is why the card-style candle, the circle glyph and the milestone
   ordinal are all still at full strength two lines above — those are facts about
   the occasion, true whichever year you are looking at.
   The coin's flag lives on the event too, so it is not the data's location that
   separates it: mint does not encode identity, it encodes ACTIONABILITY. It is the
   "nudge you can act on" class in the trailing-slot grammar, picked as mint
   precisely so an actionable coin would not be read as one of the identity marks
   beside it. A log has nothing to act on, so the mint is what is wrong here — but
   the marker stays, because a past row carrying the flag and marked sent is the
   only evidence that a cheque went in. Suppressing it outright (the stash spare's
   treatment) was built and rejected for exactly that reason.
   --text-faint, not --text-muted: muted is the ink the row's own NAME wears, so a
   coin wearing it comes out as loud as the thing it annotates. Faint is the tier
   the day number and the plain ages already sit in, which is where a marker
   belongs. A pass at muted shipped briefly and came back — see Themes → Contrast
   for why the reasoning behind it did not hold. */
.occ-row.is-past .occ-coin {
	background: color-mix(in srgb, var(--text-faint) 16%, transparent);
	color: var(--text-faint);
}

/* An unlogged past occurrence draws no leading control — the slot holds its
   width so the day column stays aligned, and nothing more. It is a span, not a
   disabled button: there is no action here to disable. */
.occ-status--empty { cursor: default; }

.occ-day {
	flex-shrink: 0;
	width: 1.6rem;
	text-align: right;
	font-size: 0.85rem;
	font-variant-numeric: tabular-nums;
	color: var(--text-muted);
}

.occ-body { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.55rem; }

.occ-names { min-width: 0; font-size: 0.98rem; }

/* Occasion icon carries the card-style colour (the paper list's ink colours):
   birthdays tint rose / teal; anniversaries and remembrances stay cream. */
.occ-glyph { flex-shrink: 0; display: flex; color: var(--style-neutral); }

.occ-glyph.occ-ic--fem { color: var(--style-fem); }

.occ-glyph.occ-ic--masc { color: var(--style-masc); }

/* Occasion icons (candle / rings / feather). Size is declared once as --icon-px
   and the stroke derived from it — see --icon-weight in the token block. */
.occ-ic {
	--icon-px: 20;
	width: calc(var(--icon-px) * 1px);
	height: calc(var(--icon-px) * 1px);
	display: block;
	fill: none;
	stroke: currentColor;
	stroke-width: var(--icon-weight);
	stroke-linecap: round;
	stroke-linejoin: round;
}

/* vector-effect isn't inherited, so it goes on the drawn shapes, not the <svg>. */
.occ-ic * { vector-effect: non-scaling-stroke; }

/* Filled counterpart to .circle-glyph-dot, and it exists for the same reason:
   a shape that is meant to read as a solid speck must scale PROPORTIONALLY,
   where a stroked one holds a constant screen weight and fattens as the mark
   shrinks. The firework is most of the argument — it is twenty specks and six
   strokes, and stroking the specks makes it a smudge at 20px. */
.occ-ic .occ-ic-dot { fill: currentColor; stroke: none; }

.occ-name { font-weight: 600; color: var(--text); }

.link-amp { color: var(--text-faint); font-weight: 400; }

/* Computed age; the milestone years get the emphasis. Plain ages sit in the
   tertiary tier with the day number beside them — see Themes → Contrast in
   docs/ui-conventions.md for what that tier is held to and why. */
.occ-age {
	flex-shrink: 0;
	font-size: 0.85rem;
	font-variant-numeric: tabular-nums;
	color: var(--text-faint);
}

.occ-age.is-significant, .is-significant { color: var(--accent); font-weight: 700; }

/* Coin badge — the "system nudge" marker class (docs/circle-identity.md marker
   grammar): a small mint disc holding a $ (enclose-check) or the stash mini-stack.
   Mint (--value), NOT gold — the old gold coin clashed with the accent-gold
   milestone ordinal sitting right beside it in the same trailing slot. */
.occ-coin {
	flex-shrink: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.3rem;
	height: 1.3rem;
	border-radius: 50%;
	background: color-mix(in srgb, var(--value) 22%, transparent);
	color: var(--value);
}

.occ-check { margin-left: auto; font-size: 0.8rem; font-weight: 700; }

/* THE PANEL IS NOT A ROW, and the auto margin above is a row rule. On a list row
   the coin belongs in the trailing slot, so it is pushed there. The detail head
   reuses the same class inside `.sh-detail-name`, which is a flex line as wide as
   the META LINE beneath it — so the coin was shoved two thirds of the way across
   the header and floated in dead space between the name and Edit, attached to
   neither. Anchored to the name, it reads as what it is: a mark on this occasion,
   the same way the cake glyph on its left is. */
.occ-detail .occ-check { margin-left: 0; }

/* When the age chip is also present it takes the right edge, so the coin sits just
   left of it rather than grabbing all the space. */
.occ-check + .occ-age, .occ-stash + .occ-age { margin-left: 0.5rem; }

/* The stash mini stack, drawn small inside the coin (a bare outline was confusable
   beside boxy circle glyphs — dice / square / camera). */
.occ-coin-ic {
	--icon-px: 12;
	width: calc(var(--icon-px) * 1px);
	height: calc(var(--icon-px) * 1px);
	display: block;
	fill: none;
	stroke: currentColor;
	stroke-width: var(--icon-weight);
	stroke-linecap: round;
	stroke-linejoin: round;
}

.occ-coin-ic * { vector-effect: non-scaling-stroke; }

/* Circle marker — the recipient's can't-share circle wearing its identity: the
   user's chosen glyph (or the generic three-dot trio) tinted the circle's colour
   (inline, from --circle-<name>) or faint when unstyled. Quiet, ambient context you
   can spot while scanning, before opening the occasion. */
.occ-circle { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--text-faint); }

/* The 47-glyph identity set, drawn at five sizes across the app (chip 15 → row
   and title mark 22). Only --icon-px varies per site; the stroke follows it. */
.circle-glyph {
	--icon-px: 18;
	width: calc(var(--icon-px) * 1px);
	height: calc(var(--icon-px) * 1px);
	display: block;
	fill: none;
	stroke: currentColor;
	stroke-width: var(--icon-weight);
	stroke-linecap: round;
	stroke-linejoin: round;
}

.circle-glyph * { vector-effect: non-scaling-stroke; }

.circle-glyph .circle-glyph-dot { fill: currentColor; stroke: none; }

.occ-detail { display: flex; flex-direction: column; }

.occ-detail .sh-detail-name { display: flex; align-items: center; gap: 0.5rem; }

.occ-detail .sh-detail-name .occ-ic { --icon-px: 24; }

.occ-detail .occ-detail-names { min-width: 0; }

/* Detail status picker: every state incl. Skip, for precise control. */
/* Status is ONE segmented control (iOS/macOS), not five loose buttons: a single
   rounded container, hairline dividers between segments, one filled selection. */
.status-picker {
	display: flex;
	border: 1px solid var(--border);
	border-radius: 0.55rem;
	background: var(--input-bg);
	overflow: hidden;
}

.status-opt {
	flex: 1 1 0;
	min-width: 0;
	padding: 0.4rem 0.3rem;
	border: none;
	border-left: 1px solid var(--border);
	border-radius: 0;
	background: transparent;
	color: var(--text-muted);
	font-size: 0.8rem;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	cursor: pointer;
	transition: background 0.15s, color 0.15s;
}

.status-opt:first-child { border-left: none; }

:where(:root[data-input="pointer"]) .status-opt:hover { color: var(--text); background: var(--surface-2); }

/* In a panel field the picker is a WRAPPING SET OF PILLS, not a joined track.
   The track cannot wrap: its options share one outer border with hairlines
   between them, so a second row comes out borderless and detached. Five states
   do not fit one line in a value column at phone width — which is what the
   truncation report was, an equal-column track with no room for the ellipsis to
   be doing anything useful. Natural widths, because equal columns stretch the
   one wrapped option across a whole row on its own. */
.sh-detail-field .status-picker {
	width: 100%;
	flex-wrap: wrap;
	gap: 0.35rem;
	border: none;
	border-radius: 0;
	overflow: visible;
	background: none;
}

.sh-detail-field .status-opt {
	flex: 0 1 auto;
	border: 1px solid var(--border);
	border-radius: 0.45rem;
	padding: 0.4rem 0.75rem;
	overflow: visible;
	text-overflow: clip;
}

.sh-detail-field .status-opt.is-selected { border-color: var(--value); }

.status-opt.is-selected {
	color: var(--text);
	font-weight: 600;
	background: color-mix(in srgb, var(--value) 14%, var(--surface));
}

/* Occasion detail's Design picker — a type-to-search combobox (input + datalist,
   outside a form), so it just uses the global text-input styling + a little gap. */

.person-row {
	width: 100%;
	text-align: left;
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 0.75rem;
	padding: 0.6rem 0.9rem;
	cursor: pointer;
	transition: background 0.2s, border-color 0.2s;
}

:where(:root[data-input="pointer"]) .person-row:hover { background: var(--surface-2); border-color: var(--primary); }

.person-row:active { background: var(--surface-3); transition-duration: 0s; }

.person-name { font-weight: 600; font-size: 0.98rem; color: var(--text); }

.person-sub { font-size: 0.8rem; color: var(--text-muted); }

/* The person detail's Occasions block (#40) — a read-only log of the dates you
   keep for someone and how each is marked, linking through to Upcoming where
   they are edited. A LOG, not a roster, so it takes the dense borderless table
   row of the card history rather than the tile treatment (the v42 rule in
   js/shell/resource-view.js) — these two blocks sit one above the other on the
   same panel, so a mismatch between them would be visible in one glance. */
.person-occ-row {
	width: 100%;
	display: grid;
	grid-template-columns: 4.6rem 1fr auto;
	align-items: baseline;
	gap: 0.5rem;
	padding: 0.4rem 0.15rem;
	background: none;
	border: none;
	border-bottom: 1px solid var(--border);
	text-align: left;
	cursor: pointer;
	font-size: 0.85rem;
	color: var(--text);
}

.person-occ-row:last-child { border-bottom: none; }

:where(:root[data-input="pointer"]) .person-occ-row:hover { background: var(--surface-2); }

.person-occ-when { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* The channel reads as quiet metadata, not as a status: it is a standing fact
   about the occasion, and the row's job here is the date. The Upcoming row is
   where the channel earns emphasis, in the leading mark. */
.person-occ-send { color: var(--text-faint); font-size: 0.78rem; }

.person-occ-send.is-text { color: var(--text-muted); }

/* Its own quiet empty line rather than borrowing the history block's: `hist-*` is
   historyList.js's to emit now, and a block used by whoever happens to want the
   declarations is the drift the registry exists to catch. */
.person-occ-empty { color: var(--text-muted); font-size: 0.9rem; }

/* Archived people — shown only when the Filters toggle asks for them, and then
   in their own section under a header (see personRows() for why a container +
   a label-colour step-down, and not a dim or a badge).

   Colour, never opacity: `--text-muted` / `--text-faint` are designed pairs that
   hold their contrast in BOTH themes, whereas alpha multiplies whatever the
   token already was — which is how round 1's 0.55 sample left the light theme's
   subtitle barely there. The name also drops from 600 to 500 so the step reads
   at a glance rather than only in comparison. */
.person-group {
	font-size: 0.72rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--text-muted);
	padding: 0.9rem 0.25rem 0.35rem;
}

.person-row.is-archived .person-name { color: var(--text-muted); font-weight: 500; }

.person-row.is-archived .person-sub { color: var(--text-faint); }

/* Which person the detail pane is showing. Only meaningful once the panes
   split — below that, picking someone hides the list anyway. */
.person-row.is-selected {
	border-color: var(--value);
	background: color-mix(in srgb, var(--value) 9%, var(--surface));
}

/* Circles browse rows — a leading identity mark (the chosen glyph tinted the
   chosen colour, or the generic faint three-dot when unset) beside the name +
   member count. Same card treatment as the People rows so the three browse
   lists read as one system. */
.circle-list { display: flex; flex-direction: column; gap: 0.5rem; }

.circle-row {
	width: 100%;
	text-align: left;
	display: flex;
	align-items: center;
	gap: 0.7rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 0.75rem;
	padding: 0.6rem 0.9rem;
	cursor: pointer;
	transition: background 0.2s, border-color 0.2s;
}

:where(:root[data-input="pointer"]) .circle-row:hover { background: var(--surface-2); border-color: var(--primary); }

.circle-row:active { background: var(--surface-3); transition-duration: 0s; }

.circle-row.is-selected {
	border-color: var(--value);
	background: color-mix(in srgb, var(--value) 9%, var(--surface));
}

.circle-row-mark { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--text-faint); }

.circle-row-mark .circle-glyph { --icon-px: 22; }

.circle-row-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }

.circle-row-name { font-weight: 600; font-size: 0.98rem; color: var(--text); }

.circle-row-sub { font-size: 0.8rem; color: var(--text-muted); }

.person-detail { display: flex; flex-direction: column; }

/* Cross-entity links in a detail pane. A recipient name stays the cream it is
   everywhere else (the card-style colour language), but a dotted underline marks
   it tappable — persistent, since there's no hover on touch. The button resets to
   inherit the surrounding name type so it reads as text, not a control. */
.link-name {
	font: inherit;
	color: inherit;
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	text-decoration: underline dotted;
	text-decoration-color: var(--text-faint);
	text-underline-offset: 3px;
}

:where(:root[data-input="pointer"]) .link-name:hover { text-decoration-color: var(--primary); }

/* The design row is the one field carrying TWO actions — go to that design, and
   change which design — so it can't be a single control like every other field
   (v42's one-control-per-field rule). It used to render two right-chevrons side
   by side, one small and one large, meaning different things, which read as a
   rendering bug rather than as two controls.
   Rather than invent a symbol for either, both actions now wear a treatment
   already on screen in this very panel: the design NAME is a .link-name — the
   dotted-underline cross-entity link the recipient names directly above it use
   — and "Change" is a quiet text button, the same shape as the header's Edit.
   No new vocabulary, and nothing that needs a legend. */
/* The assigned design is a SECTION, not a squeezed one-line field. It used to be
   a single row carrying two controls — go to the design, and change it — which
   is why it kept collecting indicators (at one point two identical chevrons side
   by side). Upcoming's detail was also the only panel not using the section
   header the other three use, so this fixes both: "Design" is now a heading like
   CARDS / ROSTER / RECIPIENTS, its Edit sits on the heading (the same "title …
   Edit" relationship the panel header already has), and the value below is a
   full-width row you can tap to open that design in Cards. */
/* NB .design-link is already taken (Cards' "Open in UpNote" anchor, gold) —
   hence design-value. The .design-* prefix is crowded in this file. */
/* The row is flex whether or not it carries a cover: holding only the name, a
   flex row and a block row render the same. The alternative — asking
   `:has(.occ-design-thumb)` — is the selector this file has already declined
   once (see .sh-filter-group--loose), and the emitter would otherwise have to
   write a modifier for a state no rule needs, so nothing is written at all. */
.occ-design-value {
	display: flex;
	align-items: center;
	gap: 0.55rem;
	padding: 0.45rem 0.5rem;
	margin: 0 -0.5rem;
	border-radius: 0.45rem;
	font-size: 0.95rem;
}

.occ-design-none { padding: 0.45rem 0; color: var(--text-faint); font-size: 0.9rem; }

/* The design's cover, inside that row and before its name — a field value, so
   2rem sits between the history log's 1.5rem and the browse row's 2.75rem tile.
   Nothing is drawn where a design has no picture: this is one row rather than a
   list, so there is no column to hold open. */
.occ-design-thumb {
	flex: 0 0 2rem;
	width: 2rem;
	height: 2rem;
	border-radius: 0.3rem;
	object-fit: cover;
	display: block;
}

.occ-design-thumb.is-broken { background: var(--surface-3); }

/* Spare-stash nudge under the Design picker: name the make-ahead cards that fit this
   occasion, each a tap-to-give chip. Quiet lead, value-tinted chips (good news). */
.spare-hint { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.35rem; }

.spare-lead { font-size: 0.78rem; color: var(--text-muted); }

.spare-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }

.spare-chip {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	padding: 0.25rem 0.6rem;
	font-family: inherit;
	font-size: 0.85rem;
	color: var(--value);
	background: color-mix(in srgb, var(--value) 12%, transparent);
	border: 1px solid color-mix(in srgb, var(--value) 35%, transparent);
	border-radius: 999px;
	cursor: pointer;
}

:where(:root[data-input="pointer"]) .spare-chip:hover { background: color-mix(in srgb, var(--value) 20%, transparent); }

.spare-chip .spare-n {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 1.1rem;
	height: 1.1rem;
	padding: 0 0.2rem;
	font-size: 0.72rem;
	font-weight: 700;
	color: var(--bg);
	background: var(--value);
	border-radius: 999px;
}

.spare-more { font-size: 0.78rem; color: var(--text-faint); }

/* Why the top spare is offered, when the reason is a colour — the one match the
   row cannot show for itself. Faint and italic: it is a footnote to the chips
   above it, not a second nudge. */
.spare-why { font-size: 0.76rem; color: var(--text-faint); font-style: italic; }

/* ---- Colour swatches (js/app/colorField.js) ------------------------------
   The cardstock vocabulary's surface, on the person, occasion and design forms
   and their detail panels.

   **These carry no --circle-<name> tokens and add nothing to the theme blocks.**
   A Circle colour is retuned per theme so a marker holds against its ground; a
   cardstock colour is a sample of real paper, so retuning it would make the app
   disagree with the sheet on the desk. The hex is fixed in js/app/colors.js and
   arrives here as --sw. Legibility comes from the ring below instead, which is
   the theme-aware part — and being one ring rather than 41 × 5 values is most of
   why this field cost no palette work at all. */

.swatch-grid { display: flex; flex-direction: column; gap: 0.4rem; }

.swatch-row { display: flex; align-items: center; gap: 0.6rem; }

/* Fixed width so the swatch columns line up down the ten rows — a ragged left
   edge makes the grid read as ten unrelated strips rather than one palette. */
.swatch-row-label {
	flex: 0 0 4.2rem;
	font-size: 0.7rem;
	color: var(--text-faint);
	text-align: right;
}

.swatch-row-btns { display: flex; flex-wrap: wrap; gap: 0.3rem; }

.swatch-btn {
	width: 1.5rem;
	height: 1.5rem;
	border-radius: 50%;
	background: var(--sw, transparent);
	border: 2px solid transparent;
	padding: 0;
	cursor: pointer;
	/* The contrast ring. Inset so it never grows the box, and mixed from the
	   theme's own ink so a pale swatch has an edge on cream and a dark one has an
	   edge on plum. This is the whole of what a per-theme value set would have
	   bought, at one declaration instead of 205. */
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent);
}

/* Selected is the primary ring OUTSIDE the swatch, so the colour itself is never
   overdrawn — a tick or an inset border would sit on top of the one thing the
   control exists to show. */
.swatch-btn.is-selected {
	border-color: var(--primary);
}

/* The read-only value: a swatch before each name, on the detail panels. */
.swatch-line { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }

.swatch-name { display: inline-flex; align-items: center; gap: 0.3rem; }

.swatch-dot {
	width: 0.85rem;
	height: 0.85rem;
	border-radius: 50%;
	background: var(--sw);
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent);
	flex: none;
}

/* The app's four controls opting into the value slot: cancel the box's own
   padding so the column keeps one left edge. --slot-pull is published by the
   shell (js/shell/shell.css → the `:root` above .sh-detail-value), and the shell
   carries the identical declaration for ITS three slot children. Two statements
   of the opt-in, one statement of the value — a shell rule cannot name .seg once
   the sheets are apart. */
.sh-detail-value > .seg,
.sh-detail-value > .status-picker,
.sh-detail-value > .make-value,
.sh-detail-value > .chip-list { margin-left: var(--slot-pull); margin-right: var(--slot-pull); }

/* A segmented control in a value slot is sized by its labels, not stretched to
   the row: `.seg-btn` is `flex: 1` with 0.7rem of padding for a drawer form,
   which in a panel field came out taller than everything beside it. Same
   treatment the status picker already gets one block down.

   It WRAPS, for the same reason the status picker does: card style is a set of
   three now, and three labels of real words do not fit a phone's value slot
   beside a 7.5rem label column. Sized-by-its-labels and never-shrinking is what
   makes the overflow silent — the row would push its own text out of the pane
   rather than get narrower. */
.sh-detail-field .seg { flex: 0 1 auto; flex-wrap: wrap; gap: 0.35rem; }

.sh-detail-field .seg-btn {
	flex: 0 1 auto;
	padding: var(--slot-pad-y) 0.7rem;
	font-size: var(--slot-font);
	line-height: 1.45;
	border-radius: var(--slot-radius);
}

/* The status picker's pills are the same control wearing a different block
   name, so they take the same metrics rather than their own near-miss. */
.sh-detail-field .status-opt {
	padding: var(--slot-pad-y) 0.7rem;
	font-size: var(--slot-font);
	line-height: 1.45;
	border-radius: var(--slot-radius);
}

/* The read-only card-history LOG: one compact row per card, year → occasion →
   design → status, echoing the List's status colours. Emitted only by
   js/app/historyList.js, which both the People detail (rows across a person's
   occasions) and the occasion detail (rows across the years) render through —
   the occasion panel drops the `-occ` column, since every row there is the same
   occasion. It was `person-card-*` while People was the only panel with one. */
/* No rule above it: the section heading already says a list starts here, and a
   hairline as well is the same statement twice. */
.hist {
	display: flex;
	flex-direction: column;
	gap: 0.1rem;
	padding: 0.1rem 0;
}

.hist .sh-detail-label { margin-bottom: 0.25rem; }

.hist-row {
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	font-size: 0.9rem;
	padding: 0.25rem 0.35rem;
	border-radius: 0.4rem;
}

.hist-year { flex: 0 0 2.6rem; color: var(--text-faint); font-variant-numeric: tabular-nums; }

.hist-occ { flex: 0 0 auto; color: var(--text); }

.hist-design { flex: 1; min-width: 0; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The design's cover, in the one column that changes from row to row. It is a
   CELL of its own rather than an image inside .hist-design, so the name keeps
   its own ellipsis and the pictures line up down the panel.

   1.5rem against the browse tile's 2.75rem: this is the dense-log flavour of a
   related list (docs/ui-conventions.md -> "Related lists come in two flavours"),
   where the row is 0.9rem text on 0.25rem of padding, and a tile at row-tile
   size would make the log a second browse list. `align-self: center` because
   the row aligns on the baseline, which an image has no useful one for.

   Whether these cells exist at all is decided per SECTION, in historyList.js:
   all of them once any design in the log has a cover, none otherwise. */
.hist-thumb {
	flex: 0 0 1.5rem;
	width: 1.5rem;
	height: 1.5rem;
	align-self: center;
	border-radius: 0.25rem;
	object-fit: cover;
	display: block;
}

/* Nothing is drawn in the cell until the bytes arrive, and nothing at all in a
   row whose design has no picture: at this size a grey square would be a
   placeholder promising a photograph rather than the loading state the browse
   tile's is. What holds the column open is the flex basis above, and the cell
   is only rendered once something in this log has a cover — so a photoless
   library renders exactly what it rendered before.

   A cover that fails to resolve keeps the square, because there IS a photo and
   a blank cell would say otherwise. */
.hist-thumb.is-broken { background: var(--surface-3); }

/* (The per-status colours that used to live here as .pcard-status moved into the
   shared .status-token, which the history rows now render.) */
.hist-empty { color: var(--text-muted); font-size: 0.9rem; }

/* Rows that link to a design folder — button reset + a hover/press affordance. */
/* A row that navigates: full width, tints on hover. Shared by the card history
   and Upcoming's design row so the two can't drift apart. */
.hist-link, .link-row { width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: none; cursor: pointer; }

:where(:root[data-input="pointer"]) .hist-link:hover,
:where(:root[data-input="pointer"]) .link-row:hover { background: var(--surface-2); }

:where(:root[data-input="pointer"]) .hist-link:hover .hist-design { color: var(--text); }

/* No transition-duration reset here, unlike the row tiles: these two never eased
   their background in the first place, so the press is already instant. */
.hist-link:active, .link-row:active { background: var(--surface-3); }

/* `.u-mt` is a one-consumer utility: the remembrance row's subject field, which
   needs a gap below the date line above it. It was scoped to `.person-form` and
   `.card-form` — the two drawer forms that then existed — and when the person's
   add form moved onto the panel, that selector matched nothing and the gap went
   with it. Scoped to the utility itself now, which is what a utility class is
   for; the form it happens to be inside is not part of the rule. */
input.u-mt { margin-top: 0.5rem; }

/* Segmented two-way toggle for card style — a labelled radiogroup that reads at
   a glance, since the whole list colour-codes by this choice. */
.seg { display: flex; gap: 0.5rem; }

.seg-btn {
	flex: 1;
	padding: 0.7rem;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--input-bg);
	color: var(--text-muted);
	font-size: 0.95rem;
	cursor: pointer;
	transition: border-color 0.2s, background 0.2s, color 0.2s;
}

:where(:root[data-input="pointer"]) .seg-btn:hover { border-color: var(--primary); }

.seg-btn.is-selected {
	border-color: var(--value);
	background: color-mix(in srgb, var(--value) 12%, var(--surface));
	color: var(--text);
	font-weight: 600;
}

.form-row { display: flex; gap: 0.5rem; }

.form-col { display: flex; flex-direction: column; flex: 1; }

.form-col.u-grow { flex: 2; }

/* Inline hint at the top of a form (e.g. "add people first"). */
.form-hint {
	font-size: 0.85rem;
	color: var(--text-muted);
	background: var(--surface-2);
	border-radius: 0.5rem;
	padding: 0.6rem 0.75rem;
	margin-bottom: 0.2rem;
}

/* `.form-opt` styled the "(optional)" suffix, and `.person-occ-inline` wrapped the
   Add-a-person form's birthday + remembrance rows. Both went with the add form's
   move onto the panel's field vocabulary: the first-run block is an ordinary
   detail group now, and a field that can be left blank says so with a placeholder
   rather than a word (docs/ui-conventions.md → "(optional)" is never written). */
.link-add {
	margin-top: 0.5rem;
	align-self: flex-start;
	background: none;
	border: none;
	color: var(--primary);
	font-size: 0.9rem;
	cursor: pointer;
	padding: 0.2rem 0;
}

:where(:root[data-input="pointer"]) .link-add:hover { text-decoration: underline; }

.remem-row { margin-top: 0.5rem; }

.remem-del {
	flex: 0 0 2.6rem;
	align-self: stretch;
	display: flex;
	align-items: center;
	justify-content: center;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--input-bg);
	color: var(--text-muted);
	font-size: 1.3rem;
	line-height: 1;
	cursor: pointer;
}

:where(:root[data-input="pointer"]) .remem-del:hover { border-color: var(--danger); color: var(--danger); }

/* Circle chips on the person form (many-to-many uniqueness groups); the combobox
   below them adds/creates a circle. */
.chip-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.15rem 0 0.5rem; }

.chip {
	display: inline-flex;
	align-items: center;
	gap: 0.25rem;
	padding: 0.3rem 0.35rem 0.3rem 0.7rem;
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: 1rem;
	font-size: 0.85rem;
	color: var(--text);
}

.chip-x {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.2rem;
	height: 1.2rem;
	border: none;
	border-radius: 50%;
	background: transparent;
	color: var(--text-muted);
	font-size: 1.05rem;
	line-height: 1;
	cursor: pointer;
}

:where(:root[data-input="pointer"]) .chip-x:hover { background: var(--surface-3); color: var(--text); }

.chip-list-empty { font-size: 0.85rem; color: var(--text-muted); }

/* A circle chip on the person *detail* — tappable, opening that circle's roster.
   No × to make room for, so unlike .chip its padding is even. */
/* In a detail row the chip list is the value, so it carries no margin of its
   own — the row already spaces itself. */
.sh-detail-field .chip-list { margin: 0; }

.chip-link {
	padding: 0.3rem 0.7rem;
	cursor: pointer;
	transition: border-color 0.15s, color 0.15s;
}

:where(:root[data-input="pointer"]) .chip-link:hover { border-color: var(--primary); color: var(--primary); }

/* ---- Circle detail: the group-centric roster -------------------------- */
/* The occasion's design picker, revealed under the Design row rather than in a
   sheet over it (js/app/resources/list.js → openDesignPicker). It is a pick, not
   a form: the combobox and nothing else, applying on change. `.occ-pick-panel`
   was the sheet's wrapper and went with the sheet. */
.occ-design-pick { margin-top: 0.4rem; }

.occ-design-pick[hidden] { display: none; }

/* A RELATED LIST inside a detail comes in exactly two flavours, and which one to
   use is decided by whether the rows do anything:
     · ROSTER (interactive — tap through, remove): a tile. Circles' members and
       Cards' recipients are the same kind of thing, so they share this container.
     · LOG (read-only history): a dense borderless table row — see .hist-row, which
       js/app/historyList.js renders on both the People and the occasion detail,
       where four short columns scan better than four tiles.
   Roster tile container, kept in step with .card-row. */
.circle-members-empty { color: var(--text-muted); font-size: 0.9rem; }

/* The Warn row's value is the STATE — On or Off — and the sentence after it is a
   descriptor, so it takes the `-sub` treatment every other quieter second line in
   the app uses (person-sub, design-sub, circle-row-sub). Read is then the same
   shape as edit, where the checkbox carries the state and the text beside it
   explains: value first, explanation quieter behind it. */
.circle-warn-sub { display: block; margin-top: 0.15rem; font-size: 0.8rem; color: var(--text-muted); line-height: 1.45; }

.circle-detail { display: flex; flex-direction: column; }

.circle-members { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }

.circle-member {
	display: flex;
	flex-direction: column;
	gap: 0.3rem;
	padding: 0.6rem 0.8rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 0.6rem;
}

.circle-member-top { display: flex; align-items: center; gap: 0.5rem; }

.circle-member-name {
	flex: 1 1 auto;
	text-align: left;
	border: none;
	background: transparent;
	color: var(--text);
	font-size: 0.92rem;
	font-weight: 600;
	cursor: pointer;
	padding: 0;
}

:where(:root[data-input="pointer"]) .circle-member-name:hover { color: var(--primary); }

/* The at-a-glance panel: each member's design(s) this season, under their name. */
.circle-season { display: flex; flex-direction: column; gap: 0.15rem; }

.circle-season-design {
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	width: 100%;
	text-align: left;
	font-family: inherit;
	font-size: 0.8rem;
	border: none;
	background: transparent;
	color: var(--text-muted);
	cursor: pointer;
	padding: 0.05rem 0;
}

.circle-season-design .circle-season-name {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

:where(:root[data-input="pointer"]) .circle-season-design:hover .circle-season-name { color: var(--text); }

.circle-season-none { font-size: 0.8rem; color: var(--text-faint); }

.circle-remove {
	flex-shrink: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.4rem;
	height: 1.4rem;
	border: none;
	border-radius: 50%;
	background: transparent;
	color: var(--text-muted);
	font-size: 1.1rem;
	line-height: 1;
	cursor: pointer;
}

:where(:root[data-input="pointer"]) .circle-remove:hover { background: var(--surface-3); color: var(--danger); }

#circle-add { margin-bottom: 0.5rem; }

/* The status line reserves a row inside forms (so text arriving doesn't shift
   the layout), but on the circle detail it's empty almost always — collapse it
   rather than leave a gap above the button. */
#circle-msg:empty { min-height: 0; margin-top: 0; }

/* ---- Circle identity editors (colour + glyph pickers) ----------------- */
/* The glyph mark beside the circle's title on its detail, tinted its colour. */
/* An inline-flex box baselines on its bottom edge, so without the nudge the
   glyph rides high above the title text — same fix .chip-mark uses, plus room
   to breathe before the name. */
.circle-title-mark {
	flex-shrink: 0;
	display: inline-flex;
	align-items: center;
	margin-right: 0.45rem;
	vertical-align: -0.18em;
	color: var(--text-faint);
}

.circle-title-mark .circle-glyph { --icon-px: 22; }

/* The identity mark on a circle chip (person detail), sized to the chip text. */
.chip-mark { display: inline-flex; align-items: center; margin-right: 0.35rem; vertical-align: -0.15em; color: var(--text-faint); }

.chip-mark .circle-glyph { --icon-px: 15; }

/* Colour swatches: a wrap of filled discs, the chosen one ringed. */
.circle-color-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.6rem; }

.circle-color-swatch {
	width: 1.6rem;
	height: 1.6rem;
	border-radius: 50%;
	border: 2px solid transparent;
	background: var(--sw, transparent);
	cursor: pointer;
	padding: 0;
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}

.circle-color-swatch.is-selected { border-color: var(--text); }

/* The "no colour" chip reads as an empty ring, not a filled swatch. */
.circle-color-swatch.circle-color-swatch--none { background: transparent; color: var(--text-faint); position: relative; }

.circle-color-swatch.circle-color-swatch--none::after {
	content: "";
	position: absolute;
	left: 50%; top: 50%;
	width: 1.4rem; height: 1px;
	background: currentColor;
	transform: translate(-50%, -50%) rotate(-45deg);
}

/* Glyph grid: fixed-size outline buttons, the chosen one boxed + tinted, in
   labelled sections (Default / Places & pastimes / Nature / Creatures / Shapes
   — the runs come from GLYPH_GROUPS). The label reuses the Filters
   sheet's small-caps group label, since it does the same job: naming a run of
   choices inside a form. */
.circle-glyph-group-label {
	display: block;
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--text-faint);
	/* One margin owns the whole rhythm: a big gap above (separating this section
	   from the previous grid) and a small one below, so the label binds to the
	   glyphs it names rather than floating between two sections. */
	margin: 1rem 0 0.4rem;
}

.circle-glyph-group:first-child .circle-glyph-group-label { margin-top: 0.7rem; }

.circle-glyph-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(2.4rem, 1fr));
	gap: 0.35rem;
	margin-top: 0.35rem;
}

.circle-glyph-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--surface-2);
	color: var(--text-muted);
	cursor: pointer;
	padding: 0;
}

.circle-glyph-btn .circle-glyph { --icon-px: 20; }

:where(:root[data-input="pointer"]) .circle-glyph-btn:hover { background: var(--surface-3); }

.circle-glyph-btn.is-selected { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 12%, transparent); }

/* Two-line row: what it is, then how to recognise it. Named to match the browse
   lists' name/sub pairing so the three surfaces read alike. */
.result-name { font-size: 0.95rem; font-weight: 600; }

.result-sub { font-size: 0.8rem; color: var(--text-muted); }

/* ---- Cards view: designs (folders) + their card roster ---------------- */
/* Folder list mirrors the People browse list. Every row is one design record —
   there is no synthetic row, so there is no variant to style. */
.design-list { display: flex; flex-direction: column; gap: 0.5rem; }

/* A design row is GRID rather than the column flex People uses, so the cover tile
   can span both text lines without wrapping the name and subtitle in a div. A row
   with no photo has an empty first track, which collapses to zero width — so the
   markup carries no placeholder and the layout needs no "has a photo" variant.
   Cards diverging from the People row here is the convention working, not being
   broken: "one standard means one character, not one geometry", and a design has a
   photo to show where a person does not. */
.design-row {
	width: 100%;
	text-align: left;
	display: grid;
	grid-template-columns: auto 1fr;
	grid-template-areas: "tile name" "tile sub";
	align-items: center;
	column-gap: 0.7rem;
	row-gap: 0.15rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 0.75rem;
	padding: 0.6rem 0.9rem;
	cursor: pointer;
	transition: background 0.2s, border-color 0.2s;
}

.design-name { grid-area: name; }

.design-sub { grid-area: sub; }

/* In a list that shows photos, EVERY row reserves the square and stands a full
   tile tall — including the designs that have no picture, whose slot is simply
   empty rather than a grey placeholder. Without this the two kinds of row differ
   in both height and text indent, and a half-photographed library reads as two
   lists interleaved. Nothing is drawn in the empty slot on purpose: a box where
   there is no photo promises one. */
.design-list.has-tiles .design-row {
	grid-template-columns: 2.75rem 1fr;
	min-height: 2.75rem;
}

/* 2.75rem is the app's 44pt row rhythm (docs/ui-conventions.md -> Rows and
   markers), so a photographed row is exactly one tap target tall and the list
   keeps its beat whether or not a given design has a picture. */
.design-tile {
	grid-area: tile;
	width: 2.75rem;
	height: 2.75rem;
	border-radius: 8px;
	object-fit: cover;
	display: block;
	background: var(--surface-3);
}

/* Until the observer fills the src in, and if the object has gone, the tile is
   the empty surface square rather than a broken-image glyph. */
.design-tile:not([src]), .design-tile.is-broken { color: transparent; }

:where(:root[data-input="pointer"]) .design-row:hover { background: var(--surface-2); border-color: var(--primary); }

.design-row:active { background: var(--surface-3); transition-duration: 0s; }

.design-name { font-weight: 600; font-size: 0.98rem; color: var(--text); }

.design-sub { font-size: 0.8rem; color: var(--text-muted); }

.design-sub .make-nudge { color: var(--accent); font-weight: 600; }

.design-row.is-selected {
	border-color: var(--value);
	background: color-mix(in srgb, var(--value) 9%, var(--surface));
}

/* ---- The two picture grids: covers, and the gallery -------------------
   Both are layouts of the SAME list pane — same mount, same filters, same query,
   same selection — so each sits inside .design-list-wrap exactly where
   .design-list does and inherits the scroller it is already in.

   THE TWO SHARE THEIR GEOMETRY AND DIFFER IN WHAT A CELL HOLDS. `auto-fill` with
   a minimum, not a fixed column count: the pane is 100vw on a phone and a third
   of a desktop window in the split, and a number chosen for either is wrong in
   the other.

   6.5rem is a THRESHOLD, not a tile width — `1fr` stretches the tiles to fill —
   which is why one value serves both grids. Measured across the three viewports
   this app meets: the phone's grid gets 358px, the 1440 split's 352px, and iPad
   portrait's only 245px (the list track is clamp(17rem, 40%, 24rem), so the
   desktop list pane is NARROWER than a phone). Anything from 5.25rem to 7.1rem
   lands three across on the first two and two on the third, identically. A
   second, smaller number for covers was drawn and deleted for exactly that
   reason: it implied a density difference that does not exist, and four across
   — which would need 5.2rem or under — is a contact sheet, not a lookup grid. */
:root {
	--tile-min: 6.5rem;
	--tile-gap: 3px;
	--tile-radius: 0.35rem;
}

/* ---- The gallery: the Cards list as pictures --------------------------

   The gap is 3px — tightened from the row list's 0.5rem, which was borrowed
   before there was anything to compare it against and read as loose beside iOS
   Photos. It is deliberately NOT a hairline: these are photographs of separate
   cards, and a grid closed to 2px reads as one image cut into squares. Three
   pixels plus each tile's own border is enough separation to keep them cards. */
.gallery-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr));
	gap: var(--tile-gap);
}

/* Square, because a card library is portrait and landscape both and a grid that
   honoured each picture's own ratio would be a masonry wall — which is a layout
   for things of different KINDS, where this is one kind shot twice. `cover` crops
   to the middle; the viewer one tap away is where the whole picture is.

   The corner was 0.75rem and is 0.35rem. At the old radius a 104px tile lost a
   visible bite of the photograph to each corner and the grid read as a row of
   app icons; iOS Photos is nearer square, and a picture wants to be a rectangle.
   It is not squared off outright because the tile has a border, and a hard
   corner on a 1px stroke is the one place this palette's low-contrast borders
   look like a rendering fault. */
.gallery-tile {
	position: relative;
	aspect-ratio: 1;
	padding: 0;
	border: 1px solid var(--border);
	border-radius: var(--tile-radius);
	overflow: hidden;
	background: var(--surface-3);
	cursor: pointer;
	transition: border-color 0.2s, box-shadow 0.2s;
}

.gallery-pic { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Same rule as the row tile: an unfilled or dead object is the empty surface
   square, never a broken-image glyph. */
.gallery-pic:not([src]), .gallery-pic.is-broken { color: transparent; }

:where(:root[data-input="pointer"]) .gallery-tile:hover { border-color: var(--primary); }

.gallery-tile:active { transition-duration: 0s; }

/* The selection reads as a RING rather than as the row's tinted ground, because
   the tile has no ground to tint — the picture covers it. Inset, so it draws on
   top of the photo instead of being cropped away by `overflow: hidden`. Both
   tiles of a two-picture design light: they are one design, not two. */
.gallery-tile.is-selected {
	border-color: var(--value);
	box-shadow: inset 0 0 0 2px var(--value);
}

/* ---- Covers: the Cards list as one picture per design -----------------
   The gallery's sibling and the LOOKUP half of the pair — one cell per design
   rather than one per picture, the name drawn, and a tap that selects. Its own
   block rather than a modifier on `gallery`, because a covers cell is a frame
   PLUS a caption where a gallery tile is the button itself: the two share the
   grid's geometry through the tokens above and share no box.

   The row gap is larger than the column gap. Equal gaps put a design's name the
   same distance from its own picture as from the picture of the design below,
   and the caption then reads as a heading for the wrong cell — the one fault a
   captioned grid has that an uncaptioned one cannot. */
.covers-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr));
	gap: 0.7rem var(--tile-gap);
}

/* The cell is a bare button: no border, no ground, no padding. The frame inside
   it carries every edge, so the caption sits on the pane rather than inside a
   card — which is what keeps a grid of cards from becoming a grid of boxes. */
.covers-cell {
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	text-align: left;
	cursor: pointer;
	min-width: 0;
}

/* Square and cropped, exactly as a gallery tile is, and for the same reason: a
   card library is portrait and landscape both, and a grid honouring each ratio
   is a masonry wall. A design with NO picture keeps this frame and draws nothing
   in it — see coversHtml() in js/app/resources/cards.js for why an empty square
   is right here and wrong on a row. */
.covers-frame {
	position: relative;
	aspect-ratio: 1;
	border: 1px solid var(--border);
	border-radius: var(--tile-radius);
	overflow: hidden;
	background: var(--surface-3);
	transition: border-color 0.2s, box-shadow 0.2s;
}

.covers-pic { width: 100%; height: 100%; object-fit: cover; display: block; }

.covers-pic:not([src]), .covers-pic.is-broken { color: transparent; }

/* Two lines, then clipped. A design's name is short by habit but nothing
   enforces it, and a single cell growing to four lines would step every cell in
   its row down with it — the grid's rows size to their tallest member. Clipping
   is safe here in a way it would not be on a row, because the name is a cue
   beside a picture you also recognise, and the panel one tap away has it whole. */
.covers-name {
	font-size: 0.76rem;
	line-height: 1.25;
	font-weight: 600;
	color: var(--text);
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
	padding: 0 0.1rem;
}

:where(:root[data-input="pointer"]) .covers-cell:hover .covers-frame { border-color: var(--primary); }

.covers-cell:active .covers-frame { transition-duration: 0s; }

/* The same inset ring the gallery uses, plus the name in the selection colour.
   The ring alone was tried and is not enough here: the caption puts a run of
   text between one frame and the next, so a single lit border has more to
   compete with than it does in the gallery's closed grid. */
.covers-cell.is-selected .covers-frame {
	border-color: var(--value);
	box-shadow: inset 0 0 0 2px var(--value);
}

.covers-cell.is-selected .covers-name { color: var(--value); }

.design-detail { display: flex; flex-direction: column; }

.design-blurb { font-size: 0.85rem; color: var(--text-muted); margin: 0.1rem 0 0.4rem; line-height: 1.4; }

.design-link { color: var(--accent); text-decoration: none; }

:where(:root[data-input="pointer"]) .design-link:hover { text-decoration: underline; }

/* ---- Pictures on a record (js/app/photoField.js) ------------------------
   One block for every record that has them — a design's card photos, a person's
   or an occasion's swatches — because the geometry is the same and only the
   record's meaning differs. It carried the `design-` prefix while cards.js was
   the only view with photos; the name is neutral now that three views share it,
   and the block registry names the two modules allowed to emit it.

   Panel: the strip, cover first. A design renders that cover large, because
   there the photo IS the record's subject; elsewhere every picture is a cue and
   the grid stays uniform. Sheet: owned thumbs (remove, plus the cover star where
   a cover means something) and — designs only — a paste-a-link importer with a
   candidate picker for multi-photo notes. */
.photo-pics { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.2rem 0 0.6rem; }

/* The read strip's items are buttons: tapping one views it. They carry no
   chrome of their own — the picture is the control. */
.photo-pic-btn { padding: 0; border: none; background: none; line-height: 0; cursor: pointer; border-radius: 8px; max-width: 100%; }

.photo-pic-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The large cover takes the whole row rather than being sized by its content —
   an auto-width flex item holding a width:100% image resolves to nothing. */
.photo-pic-btn.is-cover { flex: 0 0 100%; }

.photo-pic { border-radius: 8px; background: var(--surface-3); max-width: 100%; display: block; }

.photo-pic.is-cover { width: 100%; max-height: 60vh; object-fit: contain; }

.photo-pic:not(.is-cover) { width: 4.5rem; height: 4.5rem; object-fit: cover; }

.photo-pic.is-broken, .photo-thumb.is-broken { opacity: 0.4; }

.photo-locked { font-size: 0.85rem; color: var(--text-muted); }

/* Nothing filed yet: the empty state IS the way in, so it is a control rather
   than the quiet line an unfillable empty section would use. Without it the
   section would offer Edit for a thing that does not exist, and hide Add. */
.photo-empty-add {
	display: block; width: 100%; text-align: left;
	padding: 0.6rem 0; border: none; border-top: 1px solid var(--border);
	background: none; font: inherit; font-size: 0.95rem; color: var(--primary); cursor: pointer;
}

:where(:root[data-input="pointer"]) .photo-empty-add:hover { text-decoration: underline; }

.photo-editor { display: flex; flex-direction: column; gap: 0.5rem; }

.photo-strip { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.photo-thumb { position: relative; width: 4.5rem; height: 4.5rem; border-radius: 8px; overflow: hidden; background: var(--surface-3); }

.photo-thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }

.photo-thumb.is-cover { outline: 2px solid var(--accent); outline-offset: -2px; }

.photo-thumb-cover, .photo-thumb-remove {
	position: absolute; top: 0.2rem; border: none; cursor: pointer; padding: 0;
	width: 1.35rem; height: 1.35rem; border-radius: 50%; line-height: 1;
	display: flex; align-items: center; justify-content: center;
	background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 0.8rem;
}

.photo-thumb-cover { left: 0.2rem; }

.photo-thumb-remove { right: 0.2rem; }

.photo-thumb.is-cover .photo-thumb-cover { background: var(--accent); color: var(--surface); }

.photo-row { display: flex; gap: 0.4rem; }

.photo-row input { flex: 1; }

.photo-btn {
	border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
	border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; font: inherit; white-space: nowrap;
}

.photo-btn:disabled { opacity: 0.5; cursor: default; }

/* The add control: the LAST CELL of the grid, the same 4.5rem box as the thumbs
   it sits beside. At the end rather than in a row above, because a list's add
   control belongs at the end of the list it feeds — and because #102 puts drag
   handling on these thumbs, where a row above would sit against the drag target. */
.photo-add-tile {
	width: 4.5rem;
	height: 4.5rem;
	flex: 0 0 auto;
	border-radius: 8px;
	border: 1px dashed var(--border);
	background: none;
	color: var(--text-muted);
	font-size: 1.5rem;
	line-height: 1;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
}

:where(:root[data-input="pointer"]) .photo-add-tile:hover { border-color: var(--accent); color: var(--accent); }

.photo-add-tile:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The UpNote route, revealed under the section it adds to rather than opening a
   sheet over it (→ ui-conventions.md → A picker reveals…). Same shape as the
   occasion's Design picker: a hidden host the module fills. */
.photo-import { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.4rem; }

.photo-import[hidden] { display: none; }

/* THE PICKER INPUT IS PLACED, NOT HIDDEN. iOS anchors its Photo Library / Take
   Photo / Choose Files sheet to this element's own box, and `hidden` gives it
   none — `display: none` measures as zeros, so the sheet anchored to the top-left
   corner of the viewport and appeared adrift from the control that opened it.
   Invisible and untappable here instead, with a real 1px box that photoField
   moves under the pressed control just before it fires. A programmatic `click()`
   is unaffected by `pointer-events`. */
.photo-file {
	position: fixed;
	left: 0;
	top: 0;
	width: 1px;
	height: 1px;
	opacity: 0;
	padding: 0;
	border: 0;
	pointer-events: none;
	z-index: -1;
}

/* The panel's own status line, for a batch that lost something. Silent — and so
   taking no space — when there is nothing to report, because a photo arriving on
   the strip is its own message. */
.photo-field-msg:empty { display: none; }

.photo-picker { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: flex-start; }

.photo-cand { border: 2px solid transparent; border-radius: 8px; overflow: hidden; padding: 0; background: none; cursor: pointer; line-height: 0; }

.photo-cand img { width: 4.5rem; height: 4.5rem; object-fit: cover; display: block; }

.photo-cand.is-selected { border-color: var(--accent); }

.photo-msg { font-size: 0.8rem; color: var(--text-muted); min-height: 1rem; }

/* The viewfinder, which takes over the Photos sheet rather than opening one of
   its own (js/app/photoCapture.js says why). It is the second route to a photo
   and exists for the case the system picker is worst at: a card is a front and
   an inside, and iOS's Take Photo returns one image and closes.

   The stage is a fixed slice of the viewport rather than a flex remainder. The
   drawer body scrolls, so a `flex: 1` here would resolve against a height that
   depends on how many shots are in the tray — the viewfinder would shrink as you
   worked. A stated height keeps the frame you are aiming through the same size
   from the first shot to the fifth. */
/* FULL SCREEN, not a panel inside the sheet. A viewfinder boxed into a bottom
   sheet is the one shape a camera never has on this platform: it was measured on
   a phone at 55vh inside the drawer and read as pinned, with the shutter jammed
   against the bottom edge. Every camera the user already knows owns the screen,
   and aiming a card at a letterbox is worse than aiming it at a frame.

   `position: fixed` here is the thing index.html warns about — it resolves
   against a transformed ancestor, and `.sh-drawer` is transformed. It is safe in
   this one place because the drawer sets `transform: none` while open
   (`[data-drawer="open"]`), which creates no containing block, and the camera can
   only be opened from a sheet that is already open and settled. It is also torn
   down the instant `data-drawer` flips to closed (photoEditor watches that
   attribute), so it never survives into the closing transform. Move either half
   of that and this goes back to being a box inside the sheet. */
/* Belt to photoCapture's braces. `display: flex` below beats the UA's
   `[hidden] { display: none }` — author declarations win over UA ones whatever
   the specificity — so a host left carrying this class while hidden stays a
   full-screen black rectangle over the app, eating every tap. photoCapture takes
   the class off on close; this makes the failure unreachable even if it stops. */
.photo-cam[hidden] { display: none; }

.photo-cam {
	position: fixed; inset: 0; z-index: 40;
	display: flex; flex-direction: column;
	/* Black, not a surface token: this is the ground behind a camera frame and it
	   reads as one in every theme. The palette's surfaces would tint it. */
	background: #000;
}

.photo-cam-stage { position: relative; flex: 1; min-height: 0; overflow: hidden; }

.photo-cam-video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The tray of what this session has shot, newest last. It scrolls sideways and
   never wraps: a wrapping tray grows a second row mid-session and pushes the
   shutter off the bottom of the sheet, which is the one control that must not
   move while you are shooting. */
.photo-cam-tray {
	display: flex; gap: 0.4rem; overflow-x: auto; min-height: 3.25rem;
	scrollbar-width: none; padding: 0.5rem 0.75rem 0; flex: 0 0 auto;
}

.photo-cam-tray::-webkit-scrollbar { display: none; }

.photo-cam-shot {
	position: relative; flex: 0 0 auto; width: 3.25rem; height: 3.25rem;
	border-radius: 8px; overflow: hidden; background: var(--surface-3);
}

.photo-cam-shot-img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Still going to the bucket: dimmed, and carrying no remove — taking a shot back
   mid-upload would have to cancel a write in flight and then chase the bytes. */
.photo-cam-shot.is-uploading { opacity: 0.45; }

/* Never arrived. It stays in the tray wearing the failure rather than vanishing,
   because a photo that silently did not save is invisible against a tray nobody
   counted. */
.photo-cam-shot.is-failed { opacity: 0.45; outline: 2px solid var(--danger); outline-offset: -2px; }

.photo-cam-shot-drop {
	position: absolute; top: 0.15rem; right: 0.15rem; border: none; cursor: pointer; padding: 0;
	width: 1.2rem; height: 1.2rem; border-radius: 50%; line-height: 1;
	display: flex; align-items: center; justify-content: center;
	background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 0.75rem;
}

/* The controls sit on the black, so they are painted against it rather than in
   palette colours — this strip is not a surface of the app, it is the bottom of a
   camera. The generous bottom padding is what kept the shutter off the screen
   edge: the home indicator lives there, and no safe-area inset can be read here
   because index.html deliberately omits viewport-fit=cover. */
.photo-cam-bar {
	display: flex; align-items: center; gap: 0.5rem; flex: 0 0 auto;
	padding: 0.75rem 1rem 2rem;
}

/* Count on the left, Done on the right, shutter dead centre — the two flanks take
   equal space so the shutter stays put as the count grows from nothing to
   "12 photos". */
.photo-cam-count { flex: 1; font-size: 0.8rem; color: rgba(255, 255, 255, 0.75); }

/* THE FLANK TAKES THE SPACE; THE BUTTON TAKES ITS TEXT. Done was itself the
   `flex: 1` element, so while its label sat at the right edge its tap target was
   the entire right third of the bar — reaching the shutter, which is the control
   your thumb is aiming at. Reported from a phone as the shutter sometimes
   registering as Done. The flank keeps the shutter centred as the count grows;
   the button is now the size of the word. */
.photo-cam-flank { flex: 1; display: flex; justify-content: flex-end; }

.photo-cam-done {
	flex: 0 0 auto; border: none; background: none; font: inherit;
	color: #fff; cursor: pointer; padding: 0.5rem 0.3rem; font-weight: 600;
}

:where(:root[data-input="pointer"]) .photo-cam-done:hover { text-decoration: underline; }

/* The ring-and-disc every phone camera uses. Drawn in white on the black rather
   than in theme tokens, for the same reason the bar is. */
.photo-cam-shutter {
	flex: 0 0 auto; width: 3.9rem; height: 3.9rem; border-radius: 50%; cursor: pointer;
	background: #fff; border: 3px solid #000; box-shadow: 0 0 0 3px #fff;
}

.photo-cam-shutter:disabled { opacity: 0.35; cursor: default; }

:where(:root[data-input="pointer"]) .photo-cam-shutter:not(:disabled):hover { background: var(--accent); box-shadow: 0 0 0 3px var(--accent); }

.photo-cam-msg {
	font-size: 0.8rem; color: rgba(255, 255, 255, 0.75); min-height: 1rem;
	padding: 0 1rem 0.75rem; text-align: center;
}

/* Roster: the cards filed under a folder. */.roster { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }

.person-list-empty, .roster-empty { margin-top: 0.6rem; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }

/* Design-first "add a recipient": the way into the Possible recipients sheet,
   which builds the design's roster from the un-carded occasions it fits. The
   control sits at the end of the roster it feeds (docs/ui-conventions.md → "a
   list's add control belongs at the end of the list it feeds"). */
.add-recip { display: flex; flex-direction: column; margin-top: var(--panel-gap); }

.add-recip .sh-detail-label { margin-bottom: 0.3rem; }

.add-recip-open { width: 100%; }

/* The sheet itself. The rows are an interactive roster, so they take the tile
   treatment the design's own roster and the Circles members share, rather than
   the dense log treatment (docs/ui-conventions.md → "Related lists come in two
   flavours"). */
.add-recip-fit {
	font-size: 0.85rem;
	color: var(--text-muted);
	line-height: 1.5;
	margin-bottom: 0.7rem;
}

.add-recip-fit b { color: var(--text); font-weight: 600; }

/* The filter takes the PANEL's control metrics, not the drawer's. It used to
   inherit `input[type="search"]`'s 1.1rem / 0.75rem, which is the drawer's taller
   control and read a full 2px larger than the rows underneath it once the picker
   moved onto the panel. `--slot-font` is 1rem, which is also WebKit's floor: below
   16px, focusing a text control on iOS zooms the page, and the shell is
   scroll-locked so there is nothing to settle it back. Do not shrink this to match
   the 0.92rem rows — the rows are not focusable and this is. Element-qualified
   for the same reason the browse field is: unqualified, every line below loses
   back to `input[type="search"]` and the field silently keeps the drawer's
   metrics it is written to escape. */
input.add-recip-filter {
	padding: 0.4rem 0.6rem;
	font-size: var(--slot-font);
	/* Inside .add-recip-scope, which draws the frame for both zones. */
	border: none;
	border-radius: 0;
	background: none;
	min-width: 0;
	flex: 1;
}

input.add-recip-filter:focus { border-color: transparent; }

/* Name and year in ONE field, split by a hairline — the year narrows the same
   list the name does, which is the test the field's clear × sets: position and
   referent, not whether the value is a word. */
.add-recip-scope {
	display: flex;
	align-items: stretch;
	margin-bottom: 0.6rem;
	border: 1px solid var(--border);
	border-radius: var(--slot-radius);
	background: var(--input-bg);
	overflow: hidden;
	transition: border-color 0.2s;
}

.add-recip-scope:focus-within { border-color: var(--primary); }

.add-recip-year {
	display: flex;
	align-items: center;
	flex: none;
	padding: 0 0.15rem;
	border-left: 1px solid var(--border);
}

/* A stepper, not a segmented track: years grow with the data, which is what
   ruled segmented out for single-select (docs/ui-conventions.md → the Filters
   sheet). */
.add-recip-year-step {
	border: none;
	background: none;
	color: var(--text-faint);
	font-size: var(--slot-font);
	line-height: 1;
	padding: 0.35rem 0.4rem;
	border-radius: var(--slot-radius);
	cursor: pointer;
	transition: color 0.15s, background 0.15s;
}

:where(:root[data-input="pointer"]) .add-recip-year-step:hover {
	color: var(--primary);
	background: var(--surface-2);
}

.add-recip-year-val {
	min-width: 2.6rem;
	text-align: center;
	font-size: 0.88rem;
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	color: var(--text);
}

/* The picker, revealed under the roster it feeds rather than in a drawer over it
   (js/app/resources/cards.js → openRecipientPicker says why, and what was
   compared against). The dashed frame is load-bearing rather than decorative:
   this block BATCHES behind Assign while the roster rows above it apply on tap,
   and two commit models on one panel need the boundary drawn rather than
   inferred. Tinted with --accent at low alpha so it reads as "this appeared"
   without competing with the mint of a ticked row. */
.add-recip-picker {
	margin-top: 0.7rem;
	padding: 0.8rem 0.75rem 0.85rem;
	border: 1px dashed color-mix(in srgb, var(--accent) 45%, transparent);
	border-radius: 0.6rem;
	background: color-mix(in srgb, var(--accent) 5%, transparent);
}

.add-recip-picker[hidden] { display: none; }

/* The block's own foot. `.sh-btn` already carries a small top margin; the message
   line sits between the rows and the buttons exactly as a form's does. */
.add-recip-picker .sh-form-msg { margin-top: 0.6rem; }

.add-recip-rows { display: flex; flex-direction: column; gap: 0.4rem; }

.add-recip-row {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	width: 100%;
	padding: 0.5rem 0.7rem;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--surface-2);
	color: var(--text);
	font: inherit;
	text-align: left;
	cursor: pointer;
	transition: background 0.15s, border-color 0.15s;
}

:where(:root[data-input="pointer"]) .add-recip-row:not(.is-locked):hover { background: var(--surface-3); }

.add-recip-row:active { transition-duration: 0s; }

.add-recip-row.is-selected {
	background: color-mix(in srgb, var(--value) 14%, transparent);
	border-color: color-mix(in srgb, var(--value) 55%, transparent);
}

/* Locked: a can't-share circle-mate already holds this design. Dimmed with alpha
   rather than a token step-down, because unlike an archived row this is a
   DISABLED CONTROL — the browser's own affordance for "there is an action here
   and you cannot take it" — and the reason is spelled out in words on the row. */
.add-recip-row.is-locked { opacity: 0.45; cursor: not-allowed; }

/* The tick. Its check is painted only when selected, so the empty box holds the
   same space and the column never reflows as rows are chosen. */
.add-recip-tick {
	flex-shrink: 0;
	width: 1.3rem;
	height: 1.3rem;
	border: 1px solid var(--text-faint);
	border-radius: 0.3rem;
	display: grid;
	place-items: center;
	color: transparent;
}

.add-recip-row.is-selected .add-recip-tick {
	background: var(--value);
	border-color: var(--value);
	color: var(--on-primary);
}

.add-recip-tick svg {
	width: 0.85rem;
	height: 0.85rem;
	display: block;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.4;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.add-recip-lines { display: flex; flex-direction: column; min-width: 0; flex: 1; }

.add-recip-who { font-weight: 600; }

.add-recip-when { font-size: 0.8rem; color: var(--text-faint); }

.add-recip-lock { color: var(--danger); }

/* Circle identity, at the chip size the person detail's circle chips use. Both
   can't-share and grouping-only circles draw one here — see the comment on
   circleMarks() in cards.js for why this sheet differs from an Upcoming row. */
.add-recip-circle { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--text-faint); }

.add-recip-circle .circle-glyph { --icon-px: 15; }

.add-recip-age {
	flex-shrink: 0;
	font-size: 0.85rem;
	font-variant-numeric: tabular-nums;
	color: var(--text-faint);
}

/* Making: the batch — how many of this design you're making, a count + stepper.
   Spare (making − spoken-for) is derived and shown in the summary; a shortfall
   (more spoken for than made) turns the summary into an accent-coloured nudge.
   The summary rides BESIDE the stepper, both inside the one value slot, so the
   row is `detailControl('Making', …)` like every other field. Under the stepper
   was built first and looked bottom-heavy — the field ran to two lines where
   every neighbour is one, and the caption sat further from the number than the
   label it belongs to. Beside it, "Making · [− 3 +] · 3 spoken for" reads as one
   sentence. It wraps rather than truncating on a narrow phone, because a
   shortfall ("2 more to make") is a nudge and must not be the half that is cut.
   `.make-row` used to be a hand-rolled `justify-content: space-between` line with
   its own `.sh-detail-label` — the one place in js/app that wrote a shell class
   by hand, and the only value in the app that was not on the label column. It
   put the stepper at x=308 in read and x=128 in edit: the single field that
   MOVED when the panel flipped mode, which is the one thing editing-in-place
   exists to prevent. */
.make-value { display: flex; align-items: center; flex-wrap: wrap; gap: 0.15rem 0.7rem; }

.make-summary { margin: 0; }

.make-summary.is-short { color: var(--accent); font-weight: 600; }

.stash-stepper { display: flex; align-items: center; gap: 0.3rem; }

/* Sized from the slot block like every other panel control, because the ONLY
   place the stepper appears is a detail panel. Scoping it to `.sh-detail-field`
   was tried and left it 34px against 29px for everything beside it — back when
   the read stepper sat outside any field, in a hand-rolled `.make-row`. It is a
   real `detailControl` row now, but the sizing stays unscoped on purpose: what
   went wrong was a selector that described where the markup happened to hang,
   and a panel control has to be the right size wherever it is dropped. */
.stash-step-btn {
	width: 1.8rem;
	height: 1.8rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: var(--slot-font);
	line-height: 1;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--input-bg);
	color: var(--text);
	cursor: pointer;
	transition: border-color 0.15s, background 0.15s, color 0.15s;
}

:where(:root[data-input="pointer"]) .stash-step-btn:hover:not(:disabled) { border-color: var(--primary); }

.stash-step-btn:disabled { opacity: 0.4; cursor: default; }

.stash-count { min-width: 1.4rem; text-align: center; font-weight: 600; font-size: var(--slot-font); color: var(--text); }

/* In a panel field it sits beside a slimmed segmented control, where the
   form-sized button reads heavy. */

.card-row {
	width: 100%;
	text-align: left;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.6rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 0.6rem;
	padding: 0.6rem 0.8rem;
	cursor: pointer;
	transition: background 0.2s, border-color 0.2s;
}

:where(:root[data-input="pointer"]) .card-row:hover { background: var(--surface-2); border-color: var(--primary); }

.card-row:active { background: var(--surface-3); transition-duration: 0s; }

.card-lines { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.card-recip { font-weight: 600; font-size: 0.92rem; color: var(--text); }

.card-occ { font-size: 0.78rem; color: var(--text-muted); }

/* The occasion link is a button in the column-flex summary — keep it content-width
   and left-aligned rather than stretching across the row. */
.card-summary button.card-occ { align-self: flex-start; text-align: left; }

.card-row.is-done { opacity: 0.55; }

/* The Cards roster's status used to be a filled pill here, with its own colour
   scheme — one that had `made` mint and `sent` gold, i.e. the two states
   swapped relative to every other status display in the app. It now renders the
   shared .status-token like the other two surfaces do (see the token rules). */

/* The read-only who/occasion header at the top of the card editor. */
.card-summary {
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
	padding: 0.6rem 0.75rem;
	background: var(--surface-2);
	border-radius: 0.5rem;
	margin-bottom: 0.3rem;
}

/* ---- The milestone-years panel (js/app/milestoneEditor.js) --------------
   Which ages get marked, for one occasion type. Opened from a Settings row into
   the shared drawer, the way Appearance opens the theme picker.

   MINT, NOT ROSE, on everything that means "marked" — the tile and the lit grid
   cell both take --value. That is the same reading --value carries on a lit
   filter chip and on the stash coin: a thing that is switched on, as against
   --primary, which is a thing you can press. A grid of a hundred cells in the
   interactive colour would be a wall of buttons claiming to be actions.

   The bulk buttons deliberately do NOT reuse .sh-filter-chip, though they would
   have looked right. A chip in this app is a thing you are LOOKING FOR
   (docs/ui-conventions.md → The pane head), and these do something and go back
   to rest. Borrowing the class would have put a fourth meaning on it, in the one
   place a reader has no list in front of them to disambiguate against. */
.milestone-panel { display: flex; flex-direction: column; }

.milestone-label {
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--text-faint);
	margin: 0.9rem 0 0.5rem;
}

.milestone-panel > .milestone-label:first-child { margin-top: 0.2rem; }

.milestone-bulks { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.milestone-bulk {
	padding: 0.45rem 0.8rem;
	border: 1px solid var(--border);
	border-radius: 0.5rem;
	background: var(--input-bg);
	color: var(--text-muted);
	font-size: 0.9rem;
	cursor: pointer;
}

:where(:root[data-input="pointer"]) .milestone-bulk:hover { border-color: var(--primary); color: var(--text); }

.milestone-bulk:active { filter: brightness(0.92); }

.milestone-tiles { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.milestone-tile {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	padding: 0.3rem 0.4rem 0.3rem 0.6rem;
	border: 1px solid var(--value);
	border-radius: 0.45rem;
	background: color-mix(in srgb, var(--value) 14%, var(--surface));
	color: var(--text);
	font-size: 0.9rem;
	font-weight: 600;
	font-variant-numeric: tabular-nums;
}

.milestone-x {
	display: flex;
	padding: 0;
	border: none;
	background: none;
	color: var(--text-muted);
	line-height: 1;
	cursor: pointer;
}

.milestone-x svg {
	width: 0.85rem;
	height: 0.85rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.2;
	stroke-linecap: round;
}

:where(:root[data-input="pointer"]) .milestone-x:hover { color: var(--text); }

.milestone-empty { font-size: 0.85rem; color: var(--text-faint); }

/* Ten across, so a decade is a column and the shape of what you picked is
   readable at a glance — the one thing a list of tiles cannot show you. */
.milestone-grid {
	display: grid;
	grid-template-columns: repeat(10, 1fr);
	gap: 0.2rem;
}

.milestone-cell {
	aspect-ratio: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	border: 1px solid transparent;
	border-radius: 0.35rem;
	background: var(--input-bg);
	color: var(--text-faint);
	font-size: 0.72rem;
	font-variant-numeric: tabular-nums;
	cursor: pointer;
}

.milestone-cell.is-selected {
	background: color-mix(in srgb, var(--value) 18%, var(--surface));
	border-color: var(--value);
	color: var(--text);
	font-weight: 600;
}

:where(:root[data-input="pointer"]) .milestone-cell:hover { border-color: var(--primary); }

.milestone-cell:active { filter: brightness(0.92); }

/* The rule row — a switch for the one kind of milestone that is not an age.
   It is deliberately NOT .sh-settings-row: that shape belongs to the Settings
   list, where a row carries a leading mark column and main.js forwards a
   whole-row tap to the first button in it. Neither exists inside a drawer panel,
   so borrowing the class would import an anatomy with nothing behind it. What it
   does borrow is .sh-switch, which is the shell's control and is the same
   control here as there — one bit, three channels, no hue carrying the state
   alone.
   The note sits UNDER the label rather than in a card footer, which is where the
   Settings switch puts its explanation: a footer describes the section it hangs
   off and a panel has no sections to hang one off. It is carrying the whole
   meaning of the switch — "golden birthday" is not a phrase the reader is assumed
   to arrive knowing — so it has to be adjacent and it has to be seen. */
.milestone-rule {
	display: flex;
	align-items: flex-start;
	gap: 0.75rem;
	padding-bottom: 0.15rem;
}

.milestone-rule-text { min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }

.milestone-rule-name { font-size: 0.95rem; color: var(--text); }

.milestone-rule-note { font-size: 0.8rem; line-height: 1.45; color: var(--text-faint); }

/* Aligned to the label's cap-height, not to the block: the switch answers to the
   name, and centring it against a two-line row floats it beside the note. */
.milestone-rule .sh-switch { flex-shrink: 0; margin-top: 0.15rem; }

.milestone-ways { display: flex; flex-direction: column; align-items: center; }


