/* Components: the app's styles, ported from the review sheet's app CSS
   (appCss in design/flow/build.js) without its review scaffolding; the header
   and footer are in header.css, which every page loads. The rules
   behind each piece are in docs/design.md. Every page loads it
   (views/layouts/base.njk); it replaced style.css one page at a time, finished
   September 29. Colors, sizes and timing come from tokens.css. */

/* ---------- Base ---------- */

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--surface); color: var(--ink); font: 300 var(--text-reading)/1.4 var(--font);
  /* A gray page's color; a list's comes from data-list, below. */
  --list-fill: var(--gray-fill);
}
/* A link's underline is 1 pixel, and where a pointer can hover it thickens to
   3 (Ty, October 15, 437: "i like the hover state. make that standard for all
   text links."; the hover is below, with the others). */
/* Bold is SemiBold, the face's heaviest (docs/design.md, Type): `strong`
   alone is a step above its parent, which over Light text is only Regular. */
strong, b { font-weight: 600; }
a { color: var(--blue-ink); text-underline-offset: 2px; text-decoration-thickness: 1px; transition: text-decoration-thickness var(--dur-press) var(--ease-standard); }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The page's content takes the keyboard only when a script hands it over (a
   banner closed with the keyboard in it), and shows no ring for it. */
main:focus { outline: 0; }
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 100; padding: var(--space-2xs) var(--space-xs); background: var(--ink); color: var(--surface); text-decoration: none; }
.skip-link:focus { left: var(--space-2xs); top: var(--space-2xs); }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Tiles: the inlined sprite, list marks and avatars. */
.tiles-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
svg.mark, svg.av { display: block; flex: none; }
svg.mark { --tile-ground: var(--surface); }

/* A list's color (docs/design.md, Module and color). Set on the page for a
   list page, on the row for a list in a list of lists. */
[data-list="blue"] { --list-fill: var(--blue-fill); }
[data-list="red-violet"] { --list-fill: var(--red-violet-fill); }
[data-list="green"] { --list-fill: var(--green-fill); }
[data-list="cossack-green"] { --list-fill: var(--cossack-green-fill); }
[data-list="orange"] { --list-fill: var(--orange-fill); }
[data-list="yellow"] { --list-fill: var(--yellow-fill); }
[data-list="yellow-orange"] { --list-fill: var(--yellow-orange-fill); }

/* ---------- Page ---------- */

.page { display: grid; gap: var(--space-l); padding: var(--space-m) var(--space-s) var(--space-l); }
.page > * { margin: 0; }
.page p, .page h2 { margin: 0; }
section { display: grid; gap: var(--space-xs); }
/* Things that belong together inside the page's 44-pixel rhythm, at a section's spacing: a warning and the date it depends on. */
.group { display: grid; gap: var(--space-xs); }
/* What's new, a day at a time (Ty, October 16, 446): the day once, with a hairline over it, and its entries under it. */
.news-day { gap: var(--space-m); border-top: 1px solid var(--rule); padding-top: var(--space-s); }
/* A day's date reads as a year does in a list's row of years (.year-line): the page's reading text, in ink (Ty, October 22, 520 a). */
.news-date { margin: 0; font: inherit; }
.news-entry { display: grid; gap: var(--space-xs); }
/* A News post's story (views/post.njk): paragraphs 17 apart, its list's items 10. */
.post { display: grid; gap: var(--space-s); }
.post > * { margin: 0; }
.post ul { display: grid; gap: var(--space-xs); padding-left: var(--space-s); }
.headline { margin: 0; font: 600 1.75rem/1.2 var(--font); text-wrap: balance; }
/* A News post's picture (views/post.njk), as wide as the story. */
.post-picture { display: block; width: 100%; height: auto; }

/* Headline's smallest size (docs/design.md, Type): a News post's title on News. */
.headline-xsmall { font: 600 1.25rem/1.2 var(--font); text-wrap: balance; }
.title { margin: 0; font: 600 1rem/1.2 var(--font); letter-spacing: 0.05em; text-transform: uppercase; }
.muted { color: var(--ink-2); }
.small { font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; }
.label { font: 400 var(--text-secondary)/1.25 var(--font); letter-spacing: 0.05em; }
/* The list's head (658 a; Ty, October 25): its icon over its name and
   description, centered, at three tiles, so each of its cells is a tile of
   the band above it. */
.list-title { display: flex; flex-direction: column; align-items: center; gap: var(--space-s); text-align: center; }
.list-title svg.mark { width: calc(3 * var(--tile)); height: calc(3 * var(--tile)); }
.list-title p { margin-top: var(--space-3xs); color: var(--ink-2); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; }
/* A list's parts under its name (Ty, October 8, 257; October 9, 261: "L2 wins."):
   links to the list's pages as tiles in a row, in Title's type, the one you're on
   in ink. Each takes an equal share, two or three (views/partials/list-head.njk),
   and neighbors share their line. A list's name has more air than a page's
   own top (Ty, October 9, 279: "it needs the additional spacing"): 44 above
   it on every screen, the page's 27 and 17 of its own, and 27 to the parts. */
.list-head { display: grid; gap: var(--space-m); padding-top: var(--space-s); }
.list-parts { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; margin: 0; padding: 0; list-style: none; }
.list-parts a { display: flex; align-items: center; justify-content: center; min-height: var(--tile); color: var(--ink); font: 600 1rem/1.2 var(--font); letter-spacing: 0.05em; text-transform: uppercase; text-decoration: none; box-shadow: inset 0 0 0 1px var(--border); transition: background-color var(--dur-press) var(--ease-standard); }
.list-parts li + li a { margin-left: -1px; }
.list-parts a[aria-current] { background: var(--ink); color: var(--surface); box-shadow: none; }
.list-parts a:focus-visible { position: relative; }
.empty { display: grid; gap: var(--space-s); justify-items: start; }
/* The Account page's avatar: two tiles, 44 pixels a cell. */
.account-avatar svg.av { width: calc(2 * var(--tile)); height: calc(2 * var(--tile)); }

/* Back link: the ◀ tile and the place it goes back to. */
.back a { display: inline-flex; align-items: center; gap: var(--space-2xs); min-height: var(--tile); font: 400 var(--text-reading)/1.25 var(--font); letter-spacing: 0.05em; color: var(--ink); text-decoration: none; }
.back a:hover span { text-decoration: underline; text-underline-offset: var(--space-3xs); }
.back .caret { flex: none; width: var(--space-s); height: var(--space-s); color: var(--ink); transition: transform var(--dur-move) var(--ease-standard); }
.page > .back:first-child { margin-block: calc(-1 * var(--space-xs)); }

/* ---------- Rows ---------- */

.rows { list-style: none; margin: 0; padding: 0; }
.rows > li { min-height: var(--tile); }
.rows > li + li { border-top: 1px solid var(--rule); }
/* Every row starts where the pairing's text does: a 4-pixel edge, then 10. */
.member { display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-2xs) 0 var(--space-2xs) var(--space-xs); border-left: var(--edge) solid transparent; }
.member svg.av { width: 28px; height: 28px; }
.member-text { display: grid; line-height: 1.25; min-width: 0; }
/* A row's second line, what it says about the first, is tertiary (Ty,
   October 21: "anything that's a second row and is descriptive of has
   supplementary info, should be tertiary text."). Light, as tertiary text
   is, set here since a switch's row is 400 (.switch-row). */
.member-meta { font-size: var(--text-tertiary); font-weight: 300; color: var(--ink-2); overflow-wrap: anywhere; }
/* A fact that keeps to one line and is cut short where the row runs out (Ty,
   October 9): an email, a partner. A note in a sentence still wraps. */
.member-meta.one-line { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; overflow-wrap: normal; }
/* A long page's parts, linked at its top (the privacy page; Ty, October 27, 689 d). */
.contents ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2xs); font-size: var(--text-secondary); }
/* A list's name where it heads that list's part of a page, as on Account's Email from your lists: its own words and its color's edge, so it reads as the list and not as a second heading (Ty, October 27, 689 a). */
.list-name { margin: 0; font: 600 var(--text-reading)/1.2 var(--font); border-left: var(--edge) solid var(--list-fill); padding-left: var(--space-xs); }
/* "Owner" or "Organizer" after the name (docs/design.md, Glyphs and controls):
   the meta line's type, so nothing new. A visually hidden comma and a real
   space come first, so a screen reader says "Ty Hatch, Owner" (Ty, October 2);
   the margin makes up the rest of 7. */
.tag { margin-left: var(--space-3xs); font-size: var(--text-secondary); font-weight: 400; color: var(--ink-2); }
/* The actions at a row's end are text buttons (below): Edit, Restore,
   Remove, Switch. They drop below the name when a phone runs out of room. A
   suggestion's Add and Dismiss are always below it, lined up with the name
   (design/flow/, screen 8c). */
/* When the buttons drop below the name they stay at the right, 3 under its line (Ty, October 3, 102a). */
.member-wrap { flex-wrap: wrap; row-gap: var(--space-3xs); }
/* The room a name is held before a row's buttons drop under it. A member's row has one button since October 9. */
.member-wrap .member-text { flex: 1 1 7em; }
.member-buttons { display: flex; margin-left: auto; }
.member-buttons form { display: flex; margin: 0; }
/* A row whose words run long puts its button under them on a phone, lined up with them, where beside them it squeezed them to four lines (Ty, October 27, 689 c). */
.member-under .member-buttons { flex-basis: 100%; margin-left: calc(-1 * var(--space-xs)); }
@media (min-width: 704px) { .member-under .member-buttons { flex-basis: auto; margin-left: auto; } }
.member-stack { display: grid; justify-items: start; gap: var(--space-xs); padding-block: var(--space-xs); }
.member-stack form { margin: 0; }
.member-stack .actions { align-items: center; }
.list-row { display: flex; align-items: center; gap: var(--space-s); padding: var(--space-2xs) 0 var(--space-2xs) var(--space-s); text-decoration: none; color: var(--ink); border-left: var(--edge) solid var(--list-fill); transition: background-color var(--dur-press) var(--ease-standard); }
/* 87, not two tiles: 29 pixels a cell, so the mark's cells meet on whole
   pixels (Ty, October 25: two tiles, set at 87 for that). */
.list-row svg.mark { width: 87px; height: 87px; }
.list-row strong { display: block; font: 600 1.25rem/1.2 var(--font); }
.list-row span span { display: block; font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; color: var(--ink-2); }
/* A row in the admin section whose one control is Open opens as a whole: the
   text button's tap area reaches the row's edges, and where a pointer can
   hover, the row takes admin mode's blue at 12 percent, as a list's row takes
   its color (Ty, October 26; docs/design.md, Glyphs and controls). A row, never
   the disclosures that share the name (details.opens): on the one-page site
   the blue lay under white words (the design pass, October 27). */
li.opens { position: relative; transition: background-color var(--dur-press) var(--ease-standard); }
li.opens .btn-text { position: static; }
/* Over the row's 4-pixel edge too, which tints with it (.member). */
li.opens .btn-text::after { inset: 0 0 0 calc(-1 * var(--edge)); }

/* ---------- Banners ---------- */

/* A 4-pixel edge and a 12 percent tint of the same color, by meaning, never
   the list's: green for a status (success), blue for information, yellow
   orange for a warning, red for an error (Ty, October 2). */
.banner { --banner: var(--green-fill); padding: var(--space-xs); background: color-mix(in srgb, var(--banner) 12%, var(--surface)); border-left: var(--edge) solid var(--banner); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; }
.banner.info { --banner: var(--blue-fill); }
/* Admin mode's strip, at the top of every admin page while it's on
   (views/partials/admin-strip.njk; design/flow/admin.html, C2): the info
   banner's ground, a 4-pixel blue rule across its top, its words tertiary as
   a banner's are (Ty, October 21), and Turn off at its end, centered in the
   strip: its form is a flex box, as a row's is, so no line of the strip's
   type sits under the button. */
.admin-strip { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); min-height: var(--tile); padding: 0 var(--space-s); background: color-mix(in srgb, var(--blue-fill) 12%, var(--surface)); border-top: 4px solid var(--blue-fill); color: var(--ink); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; }
.admin-strip form { display: flex; margin: 0; }

/* The admin section's numbers (views/admin-numbers.njk; 584 a, 604 a): a
   count at a row's end, bars, and a week's line, drawn by the server. This
   year's is the admin's blue; last year's, and what hasn't happened, the
   pattern gray. The page is wider than the reading column at a desktop, and
   its measures sit two to a row where two fit. */
.stat { font: 200 1.75rem/1.2 var(--font); font-variant-numeric: tabular-nums; white-space: nowrap; }
.stat small { font-size: var(--text-tertiary); font-weight: 300; color: var(--ink-2); }
.bars { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-xs); }
.bars li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3xs) var(--space-xs); }
.bar-count { font-variant-numeric: tabular-nums; }
.bar-track { grid-column: 1 / -1; height: var(--space-xs); background: color-mix(in srgb, var(--pattern-gray) 16%, var(--surface)); }
.bar-track > span { display: block; height: 100%; background: var(--blue-fill); }
.bars .gone .bar-track > span { background: var(--pattern-gray); }
.stack-bar { display: flex; height: var(--space-s); background: color-mix(in srgb, var(--pattern-gray) 16%, var(--surface)); }
.stack-bar > span { display: block; height: 100%; background: var(--key); }
.legend { display: flex; flex-wrap: wrap; gap: 0 var(--space-s); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; color: var(--ink-2); }
.key::before { content: ""; display: inline-block; width: var(--space-xs); height: var(--space-xs); margin-right: var(--space-2xs); background: var(--key); }
.key.now, .s-now { --key: var(--blue-fill); }
.key.prior, .key.open, .s-open { --key: var(--pattern-gray); }
.key.never, .s-never { --key: color-mix(in srgb, var(--pattern-gray) 40%, var(--surface)); }
.week-chart { margin: 0; display: grid; gap: var(--space-2xs); }
.chart { position: relative; padding-bottom: var(--space-m); }
.chart svg { display: block; width: 100%; height: calc(2 * var(--tile)); overflow: visible; }
.chart .base { stroke: var(--rule); stroke-width: 1; }
.chart polyline { fill: none; stroke-linejoin: round; stroke-linecap: round; }
.chart .now { stroke: var(--blue-fill); stroke-width: 3; }
.chart .prior { stroke: var(--pattern-gray); stroke-width: 2; }
.chart-months { position: absolute; inset: auto 0 0; height: var(--space-m); overflow: hidden; font-size: var(--text-tertiary); font-weight: 300; color: var(--ink-2); }
.chart-months span { position: absolute; top: var(--space-2xs); }
.dash { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(9 * var(--tile))), 1fr)); gap: var(--space-l) var(--space-xl); align-items: start; }
/* At a desktop the dashboard takes the window's width, as many measures to a
   row as fit (604, read again: "fill the space"). */
@media (min-width: 704px) {
  .page.page-wide { grid-template-columns: minmax(0, 1fr); }
}
/* View as (routes/view-as.js): the person's page sits in one disabled
   fieldset that draws nothing of its own, and what can't be pressed there,
   a control or a link that goes nowhere, says so to the pointer. */
.look { min-width: 0; margin: 0; padding: 0; border: 0; }
.look :disabled, .look a:not([href]) { cursor: not-allowed; }
.banner.warning { --banner: var(--yellow-orange-fill); }
.banner.error { --banner: var(--red-fill); }
/* A banner's action sits under its text, its label in line with it. */
.banner .btn-text { margin-top: var(--space-2xs); margin-left: calc(-1 * var(--space-xs)); }
.banner form { margin: 0; }
/* The ask to record a visit, and the status after it (services/recording.js;
   design/flow/analytics.html, A1, A4 and A5): above the page's own content,
   in its column, the gap under it a page's own, its two buttons under its
   words, the same width. Not a .page, which public/js/sheet.js swaps after
   a save. */
.page-ask { display: grid; padding: var(--space-m) var(--space-s) 0; }
.page-ask > *, .page-ask p { margin: 0; }
.page-ask + .page { padding-top: var(--space-l); }
.ask .actions { margin-top: var(--space-xs); }
.ask .btn { min-width: calc(2 * var(--tile)); }
/* A banner a redirect brought can be closed, and a plain status closes itself
   after six seconds (Ty, October 6; public/js/banner.js, which shows the
   button). The × is the sheet's, a plus on its corner that turns when the
   pointer is on it, drawn smaller here with a whole tile to press. Closing is
   a fade over --dur-move. A notice a page always carries has no data-banner
   and does neither. The button's display would undo its hidden attribute, so
   the rule after it says hidden again: without script there's no button. */
.banner[data-banner] { display: flex; align-items: flex-start; gap: var(--space-xs); transition: opacity var(--dur-move) var(--ease-standard); }
.banner[data-banner].is-closing { opacity: 0; }
.banner-body { flex: 1; min-width: 0; }
.banner-close { font: inherit; position: relative; flex: none; display: grid; place-items: center; width: var(--space-m); height: var(--space-m); margin: calc((1.4em - var(--space-m)) / 2) 0; padding: 0; border: 0; background: none; color: var(--ink); cursor: pointer; }
.banner-close[hidden] { display: none; }
.banner-close::after { content: ""; position: absolute; inset: calc((var(--space-m) - var(--tile)) / 2); }
/* The × rests at 70 percent and comes to full under the pointer (Ty, October 7).
   Under the keyboard and under a press too, since a phone has no pointer to hover with. */
.banner-close svg { width: var(--space-xs); height: var(--space-xs); transform: rotate(45deg); opacity: 0.7; transition: transform var(--dur-turn) var(--ease-turn), opacity var(--dur-press) var(--ease-standard); }
@media (hover: hover) { .banner-close:hover svg { transform: rotate(135deg); opacity: 1; } }
.banner-close:focus-visible svg, .banner-close:active svg { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .banner[data-banner] { transition-duration: var(--dur-press); } }

/* ---------- The drop area ---------- */
/* The import's (Ty, October 25, 637 b, 647 a, 649 a; docs/design.md, The drop
   area; views/partials/drop.njk): where its page says what it's doing, in the
   banner colors by meaning, and takes the file. The banner's tint inside an
   edge all round, dashed 10 on and 7 off at rest and solid while a file is
   held over it, centered at every width. A browser's own dashed edge can't be
   given a length, so the edge is four bands of the message's color, which
   follow its state and dark mode as an edge would. Four tiles tall at least,
   six from 704 pixels, where there's a mouse to drag with. */
.drop { --banner: var(--blue-fill); --dash: var(--space-xs); --gap: var(--space-2xs); --tint: 12%; display: grid; align-content: center; justify-items: center; gap: var(--space-s); min-height: calc(4 * var(--tile)); padding: var(--space-m) var(--space-s); text-align: center; background:
  repeating-linear-gradient(90deg, var(--banner) 0 var(--dash), transparent 0 calc(var(--dash) + var(--gap))) top / 100% 2px no-repeat,
  repeating-linear-gradient(90deg, var(--banner) 0 var(--dash), transparent 0 calc(var(--dash) + var(--gap))) bottom / 100% 2px no-repeat,
  repeating-linear-gradient(180deg, var(--banner) 0 var(--dash), transparent 0 calc(var(--dash) + var(--gap))) left / 2px 100% no-repeat,
  repeating-linear-gradient(180deg, var(--banner) 0 var(--dash), transparent 0 calc(var(--dash) + var(--gap))) right / 2px 100% no-repeat,
  color-mix(in srgb, var(--banner) var(--tint), var(--surface)); }
.drop.good { --banner: var(--green-fill); }
.drop.check { --banner: var(--yellow-orange-fill); }
.drop.fix { --banner: var(--red-fill); }
.drop.is-over { --gap: 0px; --tint: 24%; }
.drop-form { margin: 0; }
.drop-says { display: grid; justify-items: center; gap: var(--space-2xs); }
.drop-says p { margin: 0; }
.drop [hidden] { display: none; }
[data-drop-stale][hidden] { display: none; }
.trap { display: none; }
.drop-head { font: 400 var(--text-reading)/1.3 var(--font); overflow-wrap: anywhere; }
.drop .actions { justify-content: center; }
.drop .loader { width: var(--space-m); height: var(--space-m); margin-top: var(--space-xs); }
/* The field is under its button with the script; the button shows where it's focused. */
.drop:has(input[type="file"].visually-hidden:focus-visible) [data-drop-choose] { outline: 2px solid var(--ink); outline-offset: var(--space-3xs); }
/* With a mouse it says Drop first; on a phone, where nothing is dragged, Choose. */
.drop-coarse { display: none; }
@media (pointer: coarse) {
  .drop-fine { display: none; }
  .drop-coarse { display: inline; }
}
@media (min-width: 704px) { .drop { min-height: calc(6 * var(--tile)); } }

/* ---------- Forms ---------- */

/* No browser styling. */
form.stack { display: grid; gap: var(--space-m); }
.field { display: grid; gap: var(--space-2xs); }
label, legend { font: 400 var(--text-secondary)/1.25 var(--font); letter-spacing: 0.05em; color: var(--ink-2); }
input[type="text"], input[type="email"], input[type="password"], input[type="date"], select {
  -webkit-appearance: none; appearance: none; display: block; width: 100%; min-height: var(--tile); margin: 0;
  padding: 0 var(--space-xs); border: 1px solid var(--border); border-radius: 0;
  background: var(--surface); color: var(--ink); font: 400 var(--text-reading)/1.25 var(--font);
}
/* A visually hidden field (the gate's username, for password managers) keeps
   the hidden size: the rule above is more specific than .visually-hidden. */
input.visually-hidden { width: 1px; height: 1px; padding: 0; border: 0; }
/* What's typed is Regular (400) in ink, and a placeholder lighter, Light (300)
   (Ty, October 21: "placeholder text should be lighter weight"). */
input::placeholder { font-weight: 300; }
input::-webkit-date-and-time-value { text-align: left; }
.select, .date { position: relative; display: block; }
.select select, .date input { padding-right: var(--tile); }
.select .caret, .date .caret {
  position: absolute; top: calc(50% - var(--space-s) / 2); right: calc((var(--tile) - var(--space-s)) / 2);
  width: var(--space-s); height: var(--space-s); color: var(--ink); pointer-events: none;
  transition: transform var(--dur-turn) var(--ease-turn);
}
/* Active: the ▼ turns to ▲, two quarter turns. */
.select:focus-within .caret, .date:focus-within .caret { transform: rotate(180deg); }
.date input::-webkit-calendar-picker-indicator { position: absolute; top: 0; right: 0; width: var(--tile); height: 100%; margin: 0; padding: 0; opacity: 0; cursor: pointer; }
/* What a list's gift exchange is for (views/partials/gift-day-fields.njk): the
   date and the name under the picker are Another day's, so they show only
   while it's picked. A browser without :has() drops this rule and shows them
   always; they're read only for Another day either way. A date left half
   typed stays in view under a holiday too: a browser won't send a form with
   a box it can't read, and has to have somewhere to point. */
form.stack:has(select[name="holiday"] option:checked:not([value="another"])) .another-day:not(:has(:invalid)) { display: none; }
.hint { font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; color: var(--ink-2); }
/* A counter is a warning whenever it shows, from 10 left and past the limit
   (public/js/counter.js). */
.hint[data-count] { color: var(--red-ink); }
/* A field's check (public/js/check.js; Ty, October 13, 342 and 343, and
   after seeing it: "all red. Not just the input border."): wrong, its border,
   its focus ring and its line in red; an address shaped like one, the same in
   green. */
.field[data-check="wrong"] input, .field[data-check="wrong"] select, fieldset.choice[data-check="wrong"] .choice-row input { border-color: var(--red-ink); outline-color: var(--red-ink); }
.field[data-check="right"] input { border-color: var(--green-ink); outline-color: var(--green-ink); }
[data-line][data-says="wrong"] { color: var(--red-ink); }
[data-line][data-says="right"] { color: var(--green-ink); }
/* Find's typeahead in the admin section (public/js/find.js;
   views/partials/admin-matches.njk): what matches hangs under the field the
   way the account menu's panel hangs from its tile, on the raised ground with
   the 3-pixel ink edge and the shadow, over what's below it. A row to each
   match, its name at the menu's weight and its second line a row's; the one
   the arrow keys are on takes the hover's tint and the focus ring, drawn
   inside it, since the list scrolls past eight rows. */
.find-box { position: relative; }
.find-panel { position: absolute; left: 0; right: 0; top: 100%; z-index: 3; max-height: calc(8 * var(--tile)); overflow-y: auto; background: var(--surface-3); border: var(--space-3xs) solid var(--ink); box-shadow: var(--shadow-down); }
.find-group, .find-note { margin: 0; padding: var(--space-xs) var(--space-s) var(--space-3xs); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; color: var(--ink-2); }
.find-note { padding-bottom: var(--space-xs); }
.find-option { display: grid; align-content: center; min-height: var(--tile); padding: var(--space-2xs) var(--space-s); line-height: 1.25; color: var(--ink); text-decoration: none; border-top: 1px solid var(--rule); }
.find-group + .find-option { border-top: 0; }
.find-option .member-name { font-weight: 400; overflow-wrap: anywhere; }
[role="group"] + [role="group"], .find-list + .find-note { border-top: 1px solid var(--rule); }
.find-option[aria-selected="true"] { background: color-mix(in srgb, var(--pattern-gray) 12%, var(--surface-3)); outline: 2px solid var(--ink); outline-offset: -2px; }
fieldset.choice { margin: 0; padding: 0; border: 0; display: grid; }
fieldset.choice legend { padding: 0; margin-bottom: var(--space-2xs); }
.choice-row { display: flex; align-items: flex-start; gap: var(--space-xs); min-height: var(--tile); padding-block: var(--space-xs); font: 400 var(--text-reading)/1.25 var(--font); letter-spacing: 0; color: var(--ink); cursor: pointer; }
.choice-row input { font: inherit; -webkit-appearance: none; appearance: none; position: relative; flex: none; width: var(--space-s); height: var(--space-s); margin: calc((1.25em - var(--space-s)) / 2) 0 0; border: 1px solid var(--border); border-radius: 50%; background: var(--surface); }
/* On and off the way the switch below goes (Ty, October 6): empty when off;
   on, the positive color fills the middle and tints the ring round it, and
   both come and go over --dur-move. Opacity and transform only. */
.choice-row input::before, .choice-row input::after { content: ""; position: absolute; border-radius: inherit; opacity: 0; transition: opacity var(--dur-move) var(--ease-standard), transform var(--dur-move) var(--ease-standard); }
.choice-row input::before { inset: 0; background: color-mix(in srgb, var(--green-fill) 25%, var(--surface)); }
.choice-row input::after { inset: var(--space-3xs); background: var(--green-ink); transform: scale(0.4); }
.choice-row input:checked::before, .choice-row input:checked::after { opacity: 1; transform: none; }
.choice-row input[type="checkbox"] { border-radius: 0; }
/* An admin's recovery codes, shown once (R1): numbered, a little apart, the
   letters spaced so each reads on its own. */
.code-list { margin: 0; padding-left: var(--space-l); display: grid; gap: var(--space-2xs); letter-spacing: 0.08em; }
/* A switch (docs/design.md, Glyphs and controls; Ty, October 6, 220 and 224):
   on or off for one row, at the row's far end. A tile wide, square, with a
   small tile inside that slides. Off, the small tile is an empty square, the
   tick box's look; on, it's filled with the positive color and the track
   takes that color's tint. The checkbox itself lies over the whole switch,
   unseen, so it's what's pressed, tabbed to and read out; the track and tile
   after it are drawn from its state. Only position and opacity move.
   Disabled: grayed, the tile's edge too, and nothing to press. */
.switch { position: relative; flex: none; display: block; width: var(--tile); height: calc(var(--space-s) + 2 * var(--space-3xs) + 2px); }
.switch input { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.switch-track { position: absolute; inset: 0; border: 1px solid var(--border); background: var(--surface); }
.switch-track::before { content: ""; position: absolute; inset: 0; background: color-mix(in srgb, var(--green-fill) 25%, var(--surface)); opacity: 0; transition: opacity var(--dur-move) var(--ease-standard); }
.switch-tile { position: absolute; top: var(--space-3xs); left: var(--space-3xs); width: var(--space-s); height: var(--space-s); box-shadow: inset 0 0 0 1px var(--border); transition: transform var(--dur-move) var(--ease-standard); }
.switch-tile::before { content: ""; position: absolute; inset: 0; background: var(--green-ink); opacity: 0; transition: opacity var(--dur-move) var(--ease-standard); }
.switch input:checked + .switch-track::before, .switch input:checked + .switch-track .switch-tile::before { opacity: 1; }
.switch input:checked + .switch-track .switch-tile { transform: translateX(calc(var(--tile) - var(--space-s) - 2 * var(--space-3xs) - 2px)); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--ink); outline-offset: 2px; }
.switch input:disabled { cursor: not-allowed; }
.switch input:disabled + .switch-track { border-color: var(--ink-2); background: var(--surface-2); }
.switch input:disabled + .switch-track .switch-tile { box-shadow: inset 0 0 0 1px var(--ink-2); }
/* A person's row with a switch at its end (views/member_apart.njk): the whole
   row is the switch's label. One who can't be picked reads quieter. */
.switch-row { display: flex; align-items: center; gap: var(--space-xs); flex: 1; min-width: 0; font: 400 var(--text-reading)/1.25 var(--font); letter-spacing: 0; color: var(--ink); cursor: pointer; }
.switch-row .switch { margin-left: auto; }
.switch-row.is-full { cursor: default; }
.switch-row.is-full .member-name { color: var(--ink-2); }
/* A switch's row in a list of rows presses from the row's top to its bottom, not only as tall as its words (681). */
.member > .switch-row { align-self: stretch; margin-block: calc(-1 * var(--space-2xs)); padding-block: var(--space-2xs); }
/* Reduced motion (docs/design.md, Motion): nothing slides or grows, and what
   fades does it in a tenth of a second. */
@media (prefers-reduced-motion: reduce) {
  .switch-tile, .choice-row input::after { transition-property: opacity; transition-duration: var(--dur-press); }
  .choice-row input::after { transform: none; }
  .choice-row input::before, .switch-track::before, .switch-tile::before { transition-duration: var(--dur-press); }
}
.choice-row small { display: block; margin-top: var(--space-3xs); font-size: var(--text-tertiary); font-weight: 300; line-height: 1.4; color: var(--ink-2); }
/* Choosing a list's mark (views/create.njk; "list icon" on the page): six marks
   at 87, a row of lists' size, 29 a cell so cells meet on whole pixels, as many
   to a row as fit (three on a phone, six in the reading column; Ty, September
   29). The chosen one takes a 3-pixel ink edge, like the avatar menu's; focus
   adds the usual outline outside it. */
fieldset.marks { gap: var(--space-s); }
.mark-choices { display: grid; grid-template-columns: repeat(auto-fill, 87px); gap: var(--space-s); transition: opacity var(--dur-press) var(--ease-standard); }
.mark-choices.is-fading { opacity: 0; }
.mark-choice { display: block; cursor: pointer; }
.mark-choice svg.mark { width: 87px; height: 87px; }
.mark-choice input:checked + svg.mark { box-shadow: 0 0 0 var(--space-3xs) var(--ink); }
.mark-choice input:focus-visible + svg.mark { outline: 2px solid var(--ink); outline-offset: var(--space-2xs); }
/* Each field in one has its label for a screen reader alone, its section's
   heading naming it (686 a), so the button lines up with the field's top and
   stays there when a hint or a check's line runs under the field. */
.row-form { display: flex; gap: var(--space-xs); align-items: start; flex-wrap: wrap; }
/* The field takes what its button leaves, and the button drops under it only
   where that's less than 6em, 120 pixels at the reading size (Ty, October 9, 268). */
.row-form .field { flex: 1 1 6em; }

/* ---------- Buttons ---------- */

/* Primary ink with page-colored text (dark gray and white, flipped in dark
   mode) on every page; secondary outlined; destructive outlined in red,
   quieter than a primary. Hover: the primary goes to secondary gray, the
   others take a tint. */
.btn {
  -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2xs);
  min-height: var(--tile); margin: 0; padding: 0 var(--space-s); border: 0; border-radius: 0;
  background: var(--ink); color: var(--surface); font: 600 1rem/1.25 var(--font); letter-spacing: 0.05em;
  text-decoration: none; cursor: pointer;
  transition: transform var(--dur-press) var(--ease-standard), background-color var(--dur-press) var(--ease-standard);
}
.btn:active { transform: scale(0.97); }
.btn-secondary { background: transparent; color: var(--ink); box-shadow: inset 0 0 0 1px var(--border); }
.btn-danger { background: transparent; color: var(--red-ink); box-shadow: inset 0 0 0 1px var(--red-ink); }
/* A button is as wide as its words, never stretched to its form, in a form's
   grid too (Ty, October 16: "button should fit the text, not be stretched."). */
.btn { justify-self: start; }
.btn[aria-disabled="true"], .btn-text[aria-disabled="true"] { cursor: progress; }
/* A button, or a row, drawn hidden until a script shows it (a passkey's,
   public/js/passkey.js) stays hidden: its own display would show it. */
.btn[hidden], .btn-text[hidden], .member[hidden] { display: none; }
/* The sign-in page's Google button goes while the shared computer box is
   ticked (Ty, October 20, 487: Google is the one way in that leaves the
   browser signed in to something else), and comes back when it's unticked,
   with no script. A browser without :has() keeps it, and the server refuses
   its press with the box ticked (routes/auth.js). */
form.stack:has(input[name="shared"]:checked) [data-google] { display: none; }
/* And "Create a passkey" (496 a): one isn't made on a shared computer. */
form.stack:has(input[name="shared"]:checked) [data-passkey-create] { display: none; }
/* "Create a passkey" and "Use a passkey" are text buttons under the buttons,
   side by side: their words line up with the page's, as a banner's text
   button does, by the first one's own padding. */
.passkey-ways { display: flex; flex-wrap: wrap; gap: 0 var(--space-xs); margin-left: calc(-1 * var(--space-xs)); }
/* Without script both are hidden, and the row takes no room in the form. */
.passkey-ways:not(:has(> :not([hidden]))) { display: none; }
/* The loader (docs/design.md, The icon): a press that draws, redraws or
   changes a year mails people and can take a few seconds, so its button's
   words give way to the small icon's chase, centered, in the button's text color. The words
   stay, unseen, so the button keeps its size, and still read, so it keeps its name
   (visibility would take the name from a screen reader). public/js/loader.js marks the
   press; without it nothing changes and the browser's own loading shows. Each
   cell turns in and, a moment later, out again, a beat after the one before it
   along the stroke, so the G is never whole (l3, 378). It's status, so it keeps
   moving with reduced motion: fixed times, not the tokens that go to 0. */
.btn .loader { display: none; }
.btn[data-busy] { position: relative; }
.btn[data-busy] .btn-words { opacity: 0; }
.btn[data-busy] .loader { display: block; position: absolute; inset: 0; margin: auto; width: var(--space-m); height: var(--space-m); }
.loader-cell { transform-box: view-box; transform-origin: 50% 50%; animation: loader-chase 1680ms linear calc(var(--beat) * 240ms - 1680ms) infinite; }
@keyframes loader-chase {
  0% { transform: rotate(-45deg) scale(0); opacity: 0; animation-timing-function: var(--ease-turn); }
  23.81% { transform: none; opacity: 1; animation-timing-function: linear; }
  41.667% { transform: none; opacity: 1; animation-timing-function: var(--ease-turn); }
  65.476%, 100% { transform: rotate(90deg) scale(0); opacity: 0; }
}
/* A text button (docs/design.md, Glyphs and controls; Ty, October 2, 52): the
   actions inside rows and banners, links and buttons alike. Twelve pixels, Button's
   smallest size (eleven until October 8). Drawn 27 tall and tapped 44: the hit area
   reaches past the drawing, invisibly. */
.btn-text {
  -webkit-appearance: none; appearance: none; position: relative; display: inline-flex; align-items: center;
  min-height: var(--space-m); margin: 0; padding: 0 var(--space-xs); border: 0; border-radius: 0;
  background: transparent; color: var(--ink); font: 600 0.75rem/1 var(--font); letter-spacing: 0.1em;
  text-transform: uppercase; text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background-color var(--dur-press) var(--ease-standard);
}
.btn-text::after { content: ""; position: absolute; inset: calc((var(--space-m) - var(--tile)) / 2) 0; }
/* The tint's color says what kind of action it is (Ty, October 3, 105): blue for most, green for one that adds or
   brings back, red for one that takes away. The label says it first; the color only agrees. */
.btn-text { --tint: var(--blue-fill); }
.btn-text-add { --tint: var(--green-fill); }
.btn-text-remove { --tint: var(--red-fill); }
/* A tap gets the hover's tint while it's pressed, since touch has no hover. */
.btn-text:active { background: color-mix(in srgb, var(--tint) 24%, transparent); }
/* Side by side 10 apart; stacked on a phone, 27, as sign-in's are (Ty, October 27, 689 b). */
.actions { display: flex; flex-wrap: wrap; gap: var(--space-m) var(--space-xs); }
/* A line of text buttons under text: pulled back by a button's padding, so the first label lines up with the text above it.
   Side by side they touch (Ty, October 3, 102): their own padding is the space between labels. A wrapped line sits 44 below, so tap areas meet and don't overlap. */
.actions-text { margin-left: calc(-1 * var(--space-xs)); gap: var(--space-s) 0; }

/* Hover only where there is hover: on touch screens it would stick after a tap. */
@media (hover: hover) {
  a:hover, .motion-button:hover { text-decoration-thickness: var(--space-3xs); }
  .btn:hover { background: var(--ink-2); }
  .btn-secondary:hover, .list-parts a:hover { background: var(--surface-2); }
  .list-parts a[aria-current]:hover { background: var(--ink-2); }
  .btn-danger:hover { background: color-mix(in srgb, var(--red-ink) 12%, var(--surface)); }
  /* Its kind's color at 24 percent, over whatever it sits on: a row or a banner's tint. */
  .btn-text:hover { background: color-mix(in srgb, var(--tint) 24%, transparent); }
  .list-row:hover { background: color-mix(in srgb, var(--list-fill) 12%, var(--surface)); }
  li.opens:hover { background: color-mix(in srgb, var(--blue-fill) 12%, var(--surface)); }
  .find-option:hover { background: color-mix(in srgb, var(--pattern-gray) 12%, var(--surface-3)); }
  .back a:hover .caret { transform: translateX(calc(-1 * var(--space-3xs))); }
}
@media (prefers-reduced-motion: reduce) {
  .back a:hover .caret { transform: none; }
}

/* ---------- The draw ---------- */

.year { display: grid; gap: var(--space-xs); }
/* Between two years, 27: the section's 10 and this 17. Inside a year, 10. */
.year + .year { margin-top: var(--space-s); }
.year-head a { font: 200 2.5rem/1.1 var(--font); font-variant-numeric: tabular-nums; color: var(--ink); text-decoration: none; }
/* A year that opens (Ty, October 9, 265, 266): the browser's own disclosure, with
   its marker taken off. The current year's summary is its number at the
   year's size with a caret, ▶ turned down once it's open; any other year's is
   its whole line, a row like a member's with Details at its end, which reads
   Close while it's open. The lines under the current year's number are in the
   page twice, inside the disclosure and after it (views/list.njk says why),
   and the second is hidden while the first is open. */
summary.year-head, .year-line > summary { list-style: none; cursor: pointer; }
/* A row's 44 is the line that opens it, all of it. */
.year-line > summary { min-height: var(--tile); }
summary.year-head::-webkit-details-marker, .year-line > summary::-webkit-details-marker { display: none; }
summary.year-head { display: inline-flex; align-items: center; gap: var(--space-2xs); font: 200 2.5rem/1.1 var(--font); font-variant-numeric: tabular-nums; color: var(--ink); }
summary.year-head .caret { display: block; width: var(--space-s); height: var(--space-s); }
details:not([open]) > summary .when-open, details[open] > summary .when-closed { display: none; }
.year-opens[open] > summary { margin-bottom: var(--space-xs); }
.year-body, .year-closed, .year-details { display: grid; gap: var(--space-xs); }
.year-opens[open] + .year-closed { display: none; }
.year-body > .year-details, .year-line > .year-details { padding-top: var(--space-xs); }
.year-line > .year-details { padding-bottom: var(--space-s); }
/* A section that's shut until someone opens it (Settings' Transfer the list; Ty,
   October 9): the same disclosure, its summary the section's Title heading
   with the caret after it, a tile tall so it's a fair thing to press. */
.opens > summary { display: inline-flex; align-items: center; gap: var(--space-2xs); min-height: var(--tile); list-style: none; cursor: pointer; }
.opens > summary::-webkit-details-marker { display: none; }
.opens > summary .caret { display: block; width: var(--space-s); height: var(--space-s); }
.opens-body { display: grid; gap: var(--space-xs); }
/* A switch that's a button (a year's Secret; Ty, October 9, 273): pressing it
   sends its form, which goes to the page that asks first. Drawn as the
   checkbox's is, from aria-checked. One that can't be pressed says why in its
   own second line; it's marked aria-disabled and not disabled, so the keyboard
   still reaches it and a screen reader reads the reason. */
.switch-form { display: flex; flex: 1; min-width: 0; }
button.switch-row { -webkit-appearance: none; appearance: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; text-align: left; }
button.switch-row[aria-checked="true"] .switch-track::before, button.switch-row[aria-checked="true"] .switch-tile::before { opacity: 1; }
button.switch-row[aria-checked="true"] .switch-tile { transform: translateX(calc(var(--tile) - var(--space-s) - 2 * var(--space-3xs) - 2px)); }
button.switch-row:focus-visible { outline: none; }
button.switch-row:focus-visible .switch-track { outline: 2px solid var(--ink); outline-offset: 2px; }
button.switch-row[aria-disabled="true"] { cursor: not-allowed; }
button.switch-row[aria-disabled="true"] .switch-track { border-color: var(--ink-2); background: var(--surface-2); }
button.switch-row[aria-disabled="true"] .switch-tile { box-shadow: inset 0 0 0 1px var(--ink-2); }
.year-tiles { display: grid; grid-template-columns: repeat(9, 1fr); }
.year-tiles svg { display: block; width: 100%; height: auto; aspect-ratio: 1; --tile-ground: var(--surface); }
.pairs { list-style: none; margin: 0; padding: 0; }
.pairs li + li { border-top: 1px solid var(--rule); }
.pairs li { min-height: var(--tile); display: flex; align-items: center; padding: var(--space-2xs) 0 var(--space-2xs) var(--space-xs); border-left: var(--edge) solid transparent; }
/* A pairing (views/partials/pair.njk) is on one line when it fits; when it
   doesn't, the receiver drops whole with its arrow, 3 around the arrow either
   way (Ty, October 3, 107). A name too long for a line of its own still
   wraps, inside a word if it has nowhere else to break, and the arrow stays
   on the name's first line. */
.pair { display: flex; flex-wrap: wrap; align-items: flex-start; column-gap: var(--space-3xs); min-width: 0; overflow-wrap: anywhere; }
.pair .to { display: flex; align-items: flex-start; gap: var(--space-3xs); min-width: 0; }
/* With a note under it (the owner's page), a pairing keeps the body's line
   height, so a dropped receiver sits as far under its giver as in any row. */
.member-text .pair { line-height: 1.4; }
.pair .arrow { flex: none; width: calc(2 * var(--space-xs)); height: var(--space-xs); margin-top: calc((1lh - var(--space-xs)) / 2); color: var(--ink-2); }
/* The dot's tile draws a square of ground behind it, white unless set; the
   arrow sits on whatever is behind it, so its ground is clear (Ty, October 2). */
.arrow { --tile-ground: transparent; }
.pairs li.mine { border-left-color: var(--list-fill); font-weight: 600; }
.assignment { display: grid; gap: var(--space-3xs); padding: var(--space-3xs) 0 var(--space-3xs) var(--space-xs); border-left: var(--edge) solid var(--list-fill); }
/* A partner's pairing under your own: its label, then the pairing on the same line (681). */
.partner-pair { display: flex; flex-wrap: wrap; align-items: flex-start; column-gap: var(--space-2xs); }
.assignment .who { font: 600 1.75rem/1.2 var(--font); }
/* The current year's line (451; services/year-line.js; drawn on the header
   sheet from the one-page site's reminders): a rule in the list's color down
   the left, a dot for each step, filled once it's behind, and a square for
   the gift day. The dot is 17 and the rule 3, set 7 in, so it runs through
   the dots' middles, each in line with its step's first line (--mid, half
   the line's 1.4). */
.steps { --mid: 0.7em; display: grid; gap: var(--space-s); margin: 0; padding: 0; list-style: none; font-size: var(--text-secondary); line-height: 1.4; }
.steps li { position: relative; display: grid; padding-left: var(--space-m); }
/* The rule, from each mark's middle to the next one's, so it ends at the gift day. */
.steps li:not(:last-child)::before { content: ""; position: absolute; left: var(--space-2xs); top: var(--mid); bottom: calc(-1 * var(--space-s) - var(--mid)); width: var(--space-3xs); background: var(--ink); }
.steps li::after { content: ""; position: absolute; left: 0; top: calc(var(--mid) - var(--space-s) / 2); width: var(--space-s); height: var(--space-s); box-sizing: border-box; border-radius: 50%; border: var(--space-3xs) solid var(--ink); background: var(--surface); }
.steps li.done::after { background: var(--list-fill); }
.steps li.day::after { border-radius: 0; background: var(--list-fill); }
.step-what { font-weight: 600; }
.step-when { color: var(--ink-2); font-size: var(--text-tertiary); font-weight: 300; }

/* The reveal: the year's tiles turn in, then the pairings rise, one after
   another. Reduced motion keeps only a short fade. */
[data-reveal] .year-tiles svg { transform-box: fill-box; transform-origin: center; animation: tile-turn var(--dur-turn) var(--ease-turn) both; animation-delay: calc(var(--i) * var(--stagger)); }
/* The viewer's own line rises after the tiles. Only the copy that shows while
   the year is shut, or a member's, which has one: the copy inside a year's
   details would rise again when the year is opened. The pairings are in the
   details now, so they have no part in it. */
[data-reveal] > .assignment, [data-reveal] .year-closed .assignment { animation: row-rise var(--dur-move) var(--ease-standard) both; animation-delay: calc(400ms + var(--i) * var(--stagger) * 4); }
@keyframes tile-turn { from { transform: rotate(-90deg); opacity: 0; } }
@keyframes row-rise { from { transform: translateY(var(--space-2xs)); opacity: 0; } }
@keyframes row-fade { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  [data-reveal] > .assignment, [data-reveal] .year-closed .assignment { animation: row-fade var(--dur-press) linear both; }
}

/* ---------- Wide screens ---------- */

/* From 704 (16 tiles): pages keep 44 at the edges, the header's name inset
   (header.css), and a reading column of 15 tiles in the middle of the page
   (Ty, October 9: "when larger breakpoints, center the content."). The column
   is the page's one grid column, so what's in it lines up at its left edge as
   before, and so does what a sheet's body holds. */
@media (min-width: 704px) {
  .page { grid-template-columns: minmax(0, calc(15 * var(--tile))); justify-content: center; padding: var(--space-m) var(--space-l) var(--space-l); }
  .page-ask { grid-template-columns: minmax(0, calc(15 * var(--tile))); justify-content: center; padding: var(--space-m) var(--space-l) 0; }
}

/* ---------- Sheets ---------- */

/* A short task over the page it belongs to (docs/design.md, Sheets), always
   over a real page that works without script (public/js/sheet.js fills it
   from that page). One <dialog>, three forms: a bottom sheet on phones; from
   704 a side sheet for editing, 8 tiles wide from the right, and a dialog on
   the reading column for the big confirmations. Square, on the raised
   surface, a hairline where it meets the page. From 1452 a side sheet sits
   beside the column without blocking it; everywhere else it dims and blocks
   the page behind. */
dialog.sheet {
  margin: 0; padding: 0 var(--space-s) var(--space-l); border: 0; max-width: none; max-height: none;
  overflow: auto; overscroll-behavior: contain; background: var(--surface-3); color: var(--ink);
  transition: transform var(--dur-move) var(--ease-standard), opacity var(--dur-move) var(--ease-standard);
}
dialog.sheet::backdrop { background: var(--backdrop); transition: opacity var(--dur-move) var(--ease-standard); }
dialog.sheet.is-closing::backdrop { opacity: 0; }
dialog.sheet.is-closing { pointer-events: none; }
dialog.sheet.is-dragging { transition: none; }
dialog.sheet { scroll-padding-top: var(--tile); }
.sheet-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-xs); min-height: var(--tile); margin-right: calc(-1 * var(--space-s)); background: var(--surface-3); }
.sheet-head h2 { margin: 0; padding-top: var(--space-s); font: 600 1.25rem/1.2 var(--font); }
.sheet-body { display: grid; gap: var(--space-m); }
.sheet-body > *, .sheet-body p, .sheet-body h2 { margin: 0; }

/* Close: the thin plus turned 45 degrees, an ×, which takes a quarter turn on hover. */
.sheet-close { flex: none; display: grid; place-items: center; width: var(--tile); height: var(--tile); padding: 0; border: 0; background: none; color: var(--ink); cursor: pointer; }
.sheet-close svg { width: var(--space-s); height: var(--space-s); transform: rotate(45deg); transition: transform var(--dur-turn) var(--ease-turn); }
@media (hover: hover) { .sheet-close:hover svg { transform: rotate(135deg); } }

/* Phones: from the bottom, as tall as its content, lifted over the keyboard. */
.sheet-bottom { position: fixed; inset: auto 0 var(--kb, 0px) 0; width: 100%; max-height: calc(100dvh - var(--tile)); border-top: 1px solid var(--rule); box-shadow: var(--shadow-up); touch-action: pan-y; }
.sheet-bottom.is-closing { transform: translateY(100%); }
@starting-style { .sheet-bottom[open] { transform: translateY(100%); } }

/* Editing from 704: from the right, below the header while it's in view. */
.sheet-side { position: fixed; inset: var(--sheet-top, 0px) 0 0 auto; z-index: 5; width: calc(8 * var(--tile)); height: auto; padding: 0 var(--space-m) var(--space-l); border-left: 1px solid var(--rule); box-shadow: var(--shadow-left); }
.sheet-side .sheet-head { margin-right: calc(-1 * var(--space-m)); }
.sheet-side.is-closing { transform: translateX(100%); }
@starting-style { .sheet-side[open] { transform: translateX(100%); } }

/* The big confirmations from 704: on the reading column, faded in. */
.sheet-dialog { position: fixed; top: calc(3 * var(--tile)); left: calc(50% - 4 * var(--tile)); width: calc(8 * var(--tile)); padding: 0 var(--space-m) var(--space-m); border: 1px solid var(--rule); box-shadow: var(--shadow-down); }
.sheet-dialog .sheet-head { margin-right: calc(-1 * var(--space-m)); }
.sheet-dialog.is-closing { opacity: 0; }
@starting-style { .sheet-dialog[open] { opacity: 0; } }

/* A row a sheet just saved, marked for a moment. */
.member.is-saved { border-left-color: var(--green-fill); transition: border-color var(--dur-move) var(--ease-standard); }
/* The part of a page a sheet shows; on the page itself it's not a box of its own. */
.page > [data-sheet-body] { display: contents; }
/* The head is the handle a phone's sheet drags by. */
.sheet-bottom .sheet-head { touch-action: none; cursor: grab; }
/* Reduced motion: no slide, a short fade (docs/design.md, Motion). */
@media (prefers-reduced-motion: reduce) {
  dialog.sheet { transition-duration: var(--dur-press); }
  .sheet-bottom.is-closing, .sheet-side.is-closing { transform: none; opacity: 0; }
  @starting-style { .sheet-bottom[open], .sheet-side[open] { transform: none; opacity: 0; } }
}

/* ---------- Between pages ---------- */

/* Page continuity (docs/design.md, Motion, Page continuity; Ty, October 17,
   466 to 469): the browser's own view transitions between two pages, with no
   script. The page crossfades over --dur-move. The band is dealt again where
   it changed: services/band.js names each of the page's own tiles for its
   place, shape and color, so a tile the next page has too holds still, and
   says in its classes how it turns. A list's mark carries across from its
   row on Your lists to the top of its page (views/dashboard.njk,
   views/partials/list-head.njk). Nothing on a page can be pressed while one
   plays, so each is short. A browser without them changes pages as before. */
@view-transition { navigation: auto; }
::view-transition-group(*) { animation-duration: var(--dur-move); animation-timing-function: var(--ease-standard); }
::view-transition-old(*), ::view-transition-new(*) { animation-duration: var(--dur-move); animation-timing-function: var(--ease-standard); }
.mark-vt { display: block; flex: none; }

/* The header's parts and the band's tiles stay where the next page has them,
   so a page scrolled down doesn't bring them in from above. The bar is named
   on every page, so it's taken before what's in it and stays beneath it:
   named on one page only, it would be laid over the last page's tiles. A
   named element stacks on its own, always, so the account menu's panel
   would sit under what the page positions (a banner's ×): the header is
   lifted above the page, below a side sheet (5) and the skip link. */
.rhdr { view-transition-name: hdr-bar; view-transition-class: hdr; z-index: 4; }
.hdr-icon { view-transition-name: hdr-icon; view-transition-class: hdr; }
.hdr-name { view-transition-name: hdr-name; view-transition-class: hdr; }
.hdr-acct { view-transition-name: hdr-avatar; view-transition-class: hdr; }
/* Signed out, the tile holds Sign in across two tiles (header.css), so it has
   a name of its own: under the avatar's, the browser drew the last page's
   avatar and open menu at the new tile's width for the length of the change,
   twice their size on the way out, and Sign in squeezed into one tile on the
   way in. Apart, each goes or comes at its own size, and with no class it
   comes in as the page does, after sign-out's breath too (below). */
.hdr-acct.hdr-out { view-transition-name: hdr-sign-in; view-transition-class: none; }
::view-transition-group(*.hdr), ::view-transition-group(*.band) { animation: none; }
/* The band's turns stay in the header (Ty, October 25). The browser draws a
   page change in a layer of its own over the page, where the band's
   overflow: hidden doesn't reach, so a tile turning about a corner swung
   past the header's bottom edge into the page. Every tile rests on that edge
   (header.css), so each is clipped at its own foot, and left free above and
   to the sides, which are the header's or off the screen. */
::view-transition-group(*.band) { clip-path: inset(-100vh -100vw 0 -100vw); }

/* A tile only one of the two pages has turns: out about its point, its own
   way, quickening, and the next page's in from a quarter back, a beat later,
   at its step (at-0 to at-8, ten milliseconds apart). The turns are the
   breath's own (header.css, band-arrive), read through --dir. On the page a
   sign-in opens, the arrival brings the band in, so nothing turns it twice. */
@keyframes page-turn-out { to { transform: rotate(calc(var(--dir) * 90deg)) scale(0); opacity: 0; } }
@keyframes page-fade-out { to { opacity: 0; } }
::view-transition-old(*.band):only-child { animation: page-turn-out var(--dur-exhale) var(--ease-exhale) both; }
::view-transition-new(*.band):only-child { animation: band-arrive var(--dur-turn) var(--ease-inhale) both; }
html:has(.rhdr.arriving)::view-transition-new(*.band):only-child { animation: none; }
::view-transition-old(*.cw), ::view-transition-new(*.cw) { --dir: 1; }
::view-transition-old(*.ccw), ::view-transition-new(*.ccw) { --dir: -1; }
::view-transition-old(*.pv-0-0), ::view-transition-new(*.pv-0-0) { transform-origin: 0% 0%; }
::view-transition-old(*.pv-50-0), ::view-transition-new(*.pv-50-0) { transform-origin: 50% 0%; }
::view-transition-old(*.pv-100-0), ::view-transition-new(*.pv-100-0) { transform-origin: 100% 0%; }
::view-transition-old(*.pv-0-50), ::view-transition-new(*.pv-0-50) { transform-origin: 0% 50%; }
::view-transition-old(*.pv-50-50), ::view-transition-new(*.pv-50-50) { transform-origin: 50% 50%; }
::view-transition-old(*.pv-100-50), ::view-transition-new(*.pv-100-50) { transform-origin: 100% 50%; }
::view-transition-old(*.pv-0-100), ::view-transition-new(*.pv-0-100) { transform-origin: 0% 100%; }
::view-transition-old(*.pv-50-100), ::view-transition-new(*.pv-50-100) { transform-origin: 50% 100%; }
::view-transition-old(*.pv-100-100), ::view-transition-new(*.pv-100-100) { transform-origin: 100% 100%; }
::view-transition-new(*.at-0):only-child { animation-delay: var(--dur-breath-beat); }
::view-transition-new(*.at-1):only-child { animation-delay: calc(var(--dur-breath-beat) + 10ms); }
::view-transition-new(*.at-2):only-child { animation-delay: calc(var(--dur-breath-beat) + 20ms); }
::view-transition-new(*.at-3):only-child { animation-delay: calc(var(--dur-breath-beat) + 30ms); }
::view-transition-new(*.at-4):only-child { animation-delay: calc(var(--dur-breath-beat) + 40ms); }
::view-transition-new(*.at-5):only-child { animation-delay: calc(var(--dur-breath-beat) + 50ms); }
::view-transition-new(*.at-6):only-child { animation-delay: calc(var(--dur-breath-beat) + 60ms); }
::view-transition-new(*.at-7):only-child { animation-delay: calc(var(--dur-breath-beat) + 70ms); }
::view-transition-new(*.at-8):only-child { animation-delay: calc(var(--dur-breath-beat) + 80ms); }

/* Sign-out's last breath (398 b): on the page signing out opens
   (?signed_out=1, routes/index.js), the last page's icon gathers and goes
   out in one spiral about its middle, and its tiles gather and turn out at
   their own moments, a fifth of a breath's spread apart (mo-0 to mo-9), deep
   or shallow, with the breath's own keyframes (services/band.js, b-out); the
   name and the avatar go with them. Then a beat, and the page and its
   arrival come in after them (--arrive-after, which the arrival's delays
   add). It waits only in a browser that has view-transition-class, which is
   one that plays transitions between pages, so in one that doesn't (Firefox)
   the arrival doesn't wait for nothing. The header's bar stays. */
@supports (view-transition-class: none) {
  .after-out { --arrive-after: calc(var(--dur-breath-spread) / 5 + var(--dur-gather-deep) + var(--dur-exhale) + var(--dur-breath-beat)); }
}
.after-out::view-transition-new(root), .after-out::view-transition-new(hdr-sign-in) { animation-delay: var(--arrive-after); }
.after-out::view-transition-new(*.hdr), .after-out::view-transition-new(*.band) { animation: none; }
.after-out::view-transition-old(hdr-icon) { --dir: 1; animation: b-out calc(var(--dur-gather) + var(--dur-exhale)) linear both; }
.after-out::view-transition-old(hdr-name), .after-out::view-transition-old(hdr-avatar) { animation: page-fade-out var(--dur-exhale) var(--ease-exhale) var(--dur-gather) both; }
.after-out::view-transition-old(*.band):only-child { animation: b-out calc(var(--dur-gather) + var(--dur-exhale)) linear both; }
.after-out::view-transition-old(*.band.deep):only-child { animation-name: b-out-deep; animation-duration: calc(var(--dur-gather-deep) + var(--dur-exhale)); }
.after-out::view-transition-old(*.mo-1):only-child { animation-delay: 60ms; }
.after-out::view-transition-old(*.mo-2):only-child { animation-delay: 120ms; }
.after-out::view-transition-old(*.mo-3):only-child { animation-delay: 180ms; }
.after-out::view-transition-old(*.mo-4):only-child { animation-delay: 240ms; }
.after-out::view-transition-old(*.mo-5):only-child { animation-delay: 300ms; }
.after-out::view-transition-old(*.mo-6):only-child { animation-delay: 360ms; }
.after-out::view-transition-old(*.mo-7):only-child { animation-delay: 420ms; }
.after-out::view-transition-old(*.mo-8):only-child { animation-delay: 480ms; }
.after-out::view-transition-old(*.mo-9):only-child { animation-delay: 540ms; }

/* Less motion: a 100-millisecond fade, and nothing moves (docs/design.md, Motion values). */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) { animation: none !important; }
  ::view-transition-old(*) { animation: page-fade-out var(--dur-press) linear both !important; }
  ::view-transition-new(*) { animation: page-fade-out var(--dur-press) linear reverse both !important; }
}

/* Windows' high contrast (forced colors; the design pass, 682, Ty, October 27).
   The system's few colors replace these, and backgrounds and shadows are
   dropped, so whatever they draw is drawn again here: an edge for every
   button, the current part and the chosen icon in the system's Highlight, a
   switch's and a choice's on in Highlight, and the current year's line in
   the text's color. Outlines, not borders, so nothing moves; a focused
   control keeps its own ring. A row's clear edge stays clear, so the
   reader's own pairing is the one marked; a banner is a box, heavier for a
   warning or an error; the main button is heavier than the rest and a
   destructive one dashed; the drop area keeps its dashes (the read of
   October 27's pictures). */
@media (forced-colors: active) {
  .member, .pairs li { border-left-color: Canvas; }
  .pairs li.mine { border-left-color: Highlight; }
  .banner { outline: 1px solid CanvasText; outline-offset: -1px; }
  .banner.warning, .banner.error { outline-width: var(--space-3xs); outline-offset: calc(-1 * var(--space-3xs)); }
  a.btn { color: ButtonText; }
  .btn:not(:focus-visible) { outline: 1px solid ButtonText; outline-offset: -1px; }
  .btn:not(.btn-secondary):not(.btn-danger):not(:focus-visible) { outline-width: var(--space-3xs); outline-offset: calc(-1 * var(--space-3xs)); }
  .btn-danger:not(:focus-visible) { outline-style: dashed; }
  .drop { outline: 2px dashed CanvasText; outline-offset: -2px; }
  .drop.is-over { outline-style: solid; }
  .list-parts a[aria-current]:not(:focus-visible) { outline: var(--space-3xs) solid Highlight; outline-offset: calc(-1 * var(--space-3xs)); }
  .mark-choice input:checked:not(:focus-visible) + svg.mark { outline: var(--space-3xs) solid Highlight; outline-offset: 0; }
  .switch-tile { outline: 1px solid CanvasText; outline-offset: -1px; }
  .switch input:checked + .switch-track, button.switch-row[aria-checked="true"] .switch-track { border-color: Highlight; }
  .switch input:checked + .switch-track .switch-tile::before, button.switch-row[aria-checked="true"] .switch-tile::before { forced-color-adjust: none; background: Highlight; }
  .switch input:disabled + .switch-track, button.switch-row[aria-disabled="true"] .switch-track { border-color: GrayText; }
  .choice-row input:checked { border-color: Highlight; }
  .choice-row input:checked::after { forced-color-adjust: none; background: Highlight; }
  .steps li:not(:last-child)::before, .steps li.done::after, .steps li.day::after { forced-color-adjust: none; background: CanvasText; }
}
