/*
 * Lease Watch stylesheet: the V2 look (docs/plans/2026-10-15-001-feat-lease-watch-ui-v2-plan.md): a serif
 * for headings and large numbers, Atkinson Hyperlegible Next for text, navy, white cards on a soft ground
 * and seven tinted tones. Hand-written and self-hosted. The CSP allows only same-origin styles, fonts and
 * images (and, for the icons below, data: images), so no inline style="" attributes, no CDN and no CSS
 * framework.
 *
 * Sections: 1 tokens, 2 fonts, 3 base, 4 layout, 5 header and nav, 6 banners, 7 cards,
 * 8 controls (8b form system: field widths, rows, file picker, help and error text), 9 tables, 10 status chips and alerts (10b busy forms), 11 small components (11b date tile, page head,
 * empty state; 11c the shared components: group header, fact rows, next action, banner), 12a welcome, the runway and the list (on Leases), 9b restacking rows, 12b lease page, 12 review and conflict pages (the queue strip), 12c alert page, email draft and contacts, 12d sign-in pages, 12e technical details (and how a document was read), 12f opportunity page, 12g Today, 12h Add lease documents, 12i filing card, 12j rent schedule, 12k deciding an assignment, 12l Records and settings and search, 13 notice timeline, 13b first run and import (checklist, import steps and tables, mailbox status), 14 motion, 15 forced colours, 16 print.
 *
 * Written mobile-first: rules are the narrow layout, media and container queries add the wide one.
 * Status never rests on colour alone: every chip and alert has an icon and a word (icons are
 * decorative; the word carries the meaning). The chip is the one status component: the old badges,
 * whose dotted, dashed and double borders were a third cue, became chips in the design elevation's
 * U27. Its brief (docs/design/direction.md, "Tones and chips") dropped that cue, since the icon and
 * the word already carry every status: a chip's border is a clear 1px line on every tone, kept so it
 * turns visible in forced colours.
 */

/* ------------------------------------------------------------------ 1. tokens */

:root {
    color-scheme: light;

    /* Colours. Between the palette markers the lines are generated from docs/design/palette.toml by
     * `python -m designtools palette --only lines` (each hex with its OKLCH source): change the spec and
     * regenerate, never these lines by hand (tests/test_design_palette.py holds them to the spec).
     * - Surfaces and ink: a soft ground, white cards, navy-black text. --rule-strong is a firmer hairline
     *   for table heads, tile edges and rails: decorative, never the only cue.
     * - Navy: the primary button, the brand and the selected tab.
     * - The seven tones (plan "Tones and icons"). Each is a text colour and its own tinted background, and
     *   every use pairs it with an icon and a word: urgent (Needs action now, At risk, Overdue), action
     *   (Decisions, Action needed, On hold), send (Emails to send: a teal of its own since look
     *   checkpoint 1, so it never reads as one block with Decisions under it), evidence (Evidence
     *   review, Checking, Reading), missing (Missing information, Missing dates), ok (Check-in, On track,
     *   Renewed) and neutral (Watchlist, Snoozed, Reminders off, Not tracked).
     * - The scrim (--scrim) is the wash words over the banner picture sit on (a gradient of it laid over
     *   the picture), and the page still reads on the wash alone when the picture does not load.
     * - --highlight is the quote highlighter, --signal the development banner's signal yellow. */
    /* palette:start */
    --paper: #f4f6f9; /* oklch(0.9725 0.0045 258.32) */
    --sheet: #ffffff; /* oklch(1.0000 0.0000 0.00) */
    --ink: #172238; /* oklch(0.2535 0.0447 263.32) */
    --ink-2: #3a4659; /* oklch(0.3915 0.0360 259.49) */
    --muted: #5a6577; /* oklch(0.5041 0.0318 260.25) */
    --rule: #e1e5eb; /* oklch(0.9206 0.0092 258.34) */
    --rule-strong: #c4cbd6; /* oklch(0.8400 0.0171 259.42) */
    --control: #7b8597; /* oklch(0.6147 0.0297 262.27) */
    --link: #1d3a5f; /* oklch(0.3457 0.0740 255.41) */
    --navy: #1d3a5f; /* oklch(0.3457 0.0740 255.41) */
    --navy-strong: #142a47; /* oklch(0.2832 0.0606 256.01) */
    --navy-tint: #e8eef6; /* oklch(0.9469 0.0126 255.51) */
    --urgent: #a3271c; /* oklch(0.4725 0.1621 29.55) */
    --urgent-bg: #fdecea; /* oklch(0.9560 0.0187 25.60) */
    --action: #8a4800; /* oklch(0.4736 0.1140 57.46) */
    --action-bg: #fdf0dc; /* oklch(0.9601 0.0299 78.79) */
    --send: #0a6670; /* oklch(0.4674 0.0776 206.94) */
    --send-bg: #dcf2f3; /* oklch(0.9453 0.0232 200.08) */
    --evidence: #1f4f8c; /* oklch(0.4285 0.1136 255.97) */
    --evidence-bg: #e7effa; /* oklch(0.9493 0.0171 256.28) */
    --missing: #5b3a98; /* oklch(0.4375 0.1472 295.91) */
    --missing-bg: #f0ebfa; /* oklch(0.9483 0.0207 301.14) */
    --ok: #1c6946; /* oklch(0.4649 0.0936 159.19) */
    --ok-bg: #e3f3ea; /* oklch(0.9498 0.0207 161.47) */
    --neutral: #4a5466; /* oklch(0.4442 0.0323 262.16) */
    --neutral-bg: #edf0f4; /* oklch(0.9540 0.0063 255.48) */
    --scrim: #eaf0f6; /* oklch(0.9523 0.0103 247.93) */
    --highlight: #ffe58a; /* oklch(0.9240 0.1147 93.95) */
    --signal: #f2c300; /* oklch(0.8346 0.1706 91.13) */
    /* palette:end */
    --scrim-ink: var(--ink);

    /* type: the serif for headings, large numbers and lead lines; the text face for everything else,
     * small labels and buttons included */
    --font-serif: "Lease Watch Serif", "Lease Watch Serif Fallback", Georgia, "Times New Roman", serif;
    --font-text: "Atkinson Hyperlegible Next", "Atkinson Fallback", "Segoe UI", system-ui, sans-serif;
    --font-display: var(--font-serif);
    --fs-meta: 0.8125rem;
    --fs-small: 0.875rem;
    --fs-compact: 0.9375rem; /* 15px: tables, form labels, the quote's page words */
    --fs-base: 1rem;
    --fs-reading: 1.0625rem; /* 17px: a value or a letter read at length, a row's client */
    --fs-lead: 1.125rem;
    --fs-h4: 1rem;
    --fs-h3: 1.125rem;
    --fs-h2: 1.25rem;
    --fs-h1: clamp(1.75rem, 1.45rem + 1.2vw, 2.25rem);

    /* space */
    --s-1: 0.25rem;
    --s-2: 0.5rem;
    --s-3: 0.75rem;
    --s-4: 1rem;
    --s-6: 1.5rem;
    --s-8: 2rem;
    --s-12: 3rem;
    --s-18: 4.5rem;

    /* The gap between a page's sections: cards, the sheets drawn like them and a head block above the
     * first. It is always more than a card's padding, or the cards read as one stack: 1.25rem over a
     * phone card's --s-4, and --s-6 over its 1.25rem from 720px (the query after this block). */
    --section-gap: 1.25rem;

    /* Today's row grid: the gap after the date tile or the group's icon, and the inset from the sheet's
     * edge (both widen from 720px, where the tile gets a gutter of its own, --gutter; on a phone the
     * tile sits beside the name, U24). */
    --gutter-gap: var(--s-3);
    --inset: var(--s-3);
    --card-pad: var(--s-4); /* a card's padding, 1.25rem from 720px */

    /* shape: flat sheets (a hairline, no resting shadow); only what opens over the page lifts */
    --radius: 6px;
    --radius-sheet: 8px;
    --shadow: none;
    --lift: 0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent),
        0 6px 16px color-mix(in srgb, var(--ink) 8%, transparent);
    --target: 2.75rem;
    --page-max: 85rem;
    /* the measure: lead lines, the paragraph explaining a page and the single-purpose pages keep to
     * it, about 75 characters a line inside a card's padding; the page itself stays --page-max */
    --measure: 44rem;

    /* motion (section 14): a press takes the short step, a status line arriving, a fold opening or the
     * page popover opening the long one, both on one curve that slows into place and never overshoots.
     * Nothing moves for more than 300ms but the busy spinner (section 10b). */
    --dur-1: 80ms;
    --dur-2: 180ms;
    --ease-out: cubic-bezier(0.25, 1, 0.5, 1);

    /* field widths by what is typed (section 8b) */
    --field-xs: 7rem;
    --field-sm: 11rem;
    --field-md: 20rem;
    --field-lg: 28rem;

    /* icons: glyphs used as CSS masks (see the chip, icon and alert rules), on one grid: a 24-unit box,
     * a 2.4 stroke (a tenth of the box) with round caps and joins, and every outer edge inside the 2 to 22
     * live area (a ring is r 8.8); only the diamond, the triangle and the eye reach past it, as pointed
     * shapes must to look the same size. A dot is a filled disc of r 1.4, and the neutral dot and the paw
     * are the only filled glyphs. In the 1.25em box an icon takes beside words the stroke comes to 0.12em,
     * the text face's semibold stem: the weight of the labels icons sit beside (chips, buttons, group
     * heads). */
    --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.8'/%3E%3Cpath d='M8 12.4l2.7 2.7 5.3-5.5'/%3E%3C/svg%3E");
    --icon-eye: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.9 12C5 7.9 8.2 5.7 12 5.7s7 2.2 9.1 6.3C19 16.1 15.8 18.3 12 18.3S5 16.1 2.9 12z'/%3E%3Ccircle cx='12' cy='12' r='2.9'/%3E%3C/svg%3E");
    --icon-decision: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.8l9.2 9.2-9.2 9.2L2.8 12z'/%3E%3Cpath d='M12 7.6V12'/%3E%3Ccircle cx='12' cy='15.8' r='1.4' fill='black' stroke='none'/%3E%3C/svg%3E");
    --icon-cross: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.8'/%3E%3Cpath d='M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6'/%3E%3C/svg%3E");
    --icon-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.8'/%3E%3Cpath d='M12 7.4V12l3.1 2.1'/%3E%3C/svg%3E");
    --icon-dot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3C/svg%3E");
    --icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.8'/%3E%3Cpath d='M12 11.1v5.2'/%3E%3Ccircle cx='12' cy='7.9' r='1.4' fill='black' stroke='none'/%3E%3C/svg%3E");
    --icon-warning: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.9l9.1 15.8H2.9z'/%3E%3Cpath d='M12 9.5v3.7'/%3E%3Ccircle cx='12' cy='16.5' r='1.4' fill='black' stroke='none'/%3E%3C/svg%3E");
    --icon-pause: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.8 5.2v13.6M15.2 5.2v13.6'/%3E%3C/svg%3E");
    /* Snoozed and Reminders off, each with a glyph of its own instead of sharing the clock and the pause
     * (UI research gaps plan U4, Matt 2026-10-26), drawn by hand on this grid: two Zs, a large one low
     * and a small one high; and a bell with its slash, the bell's lines cut 3.4 units either side of the
     * slash so the two never close up at the 2.4 stroke. */
    --icon-zzz: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.6 11.2h8l-8 9.2h8M14 3.6h6.4l-6.4 7.2h6.4'/%3E%3C/svg%3E");
    --icon-bell-off: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17.6 12.8V11a5.6 5.6 0 0 0-7.16-5.38M6.4 12v4.8M4.4 16.8H12M10.2 18.8a1.8 1.8 0 0 0 3.6 0M3.6 3.6l16.8 16.8'/%3E%3C/svg%3E");
    --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.6 9.8l4.4 4.4 4.4-4.4'/%3E%3C/svg%3E");
    /* a chevron up over a chevron down: a Leases heading that sorts, at rest */
    --icon-sort: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 9.2l4-4 4 4M8 14.8l4 4 4-4'/%3E%3C/svg%3E");
    --icon-mail: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.2' y='5.4' width='17.6' height='13.2' rx='2.2'/%3E%3Cpath d='M4.6 7.8l7.4 5.6 7.4-5.6'/%3E%3C/svg%3E");
    --icon-question: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.8'/%3E%3Cpath d='M9.4 9.4a2.7 2.7 0 0 1 5.2.9c0 1.8-2.6 2.1-2.6 3.6'/%3E%3Ccircle cx='12' cy='17.1' r='1.4' fill='black' stroke='none'/%3E%3C/svg%3E");
    --icon-flag: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.8 20.6V3.8'/%3E%3Cpath d='M5.8 4.4H18l-2.8 4.3 2.8 4.3H5.8'/%3E%3C/svg%3E");
    --icon-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.8' y='5.2' width='16.4' height='15.4' rx='2.2'/%3E%3Cpath d='M3.8 10.2h16.4M8.4 3.2v4M15.6 3.2v4'/%3E%3C/svg%3E");
    --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10.6' cy='10.6' r='6.4'/%3E%3Cpath d='M15.3 15.3l5.1 5.1'/%3E%3C/svg%3E");
    --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5.2v13.6M5.2 12h13.6'/%3E%3C/svg%3E");
    /* practice types: a tooth, a cross and a paw, always beside the type's name */
    --icon-dental: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.6 3.7c-2.4 0-3.9 1.8-3.9 4.3 0 2.3 1 3.8 1.6 5.9.6 2.4.9 6.4 2.6 6.4 1.6 0 1.6-4.4 4.1-4.4s2.5 4.4 4.1 4.4c1.7 0 2-4 2.6-6.4.6-2.1 1.6-3.6 1.6-5.9 0-2.5-1.5-4.3-3.9-4.3-1.9 0-2.7 1.2-4.4 1.2S9.5 3.7 7.6 3.7z'/%3E%3C/svg%3E");
    --icon-medical: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.3 3.9h5.4v5.4h5.4v5.4h-5.4v5.4H9.3v-5.4H3.9V9.3h5.4z'/%3E%3C/svg%3E");
    --icon-veterinary: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cellipse cx='5.6' cy='10.4' rx='2' ry='2.6'/%3E%3Cellipse cx='9.4' cy='6' rx='2' ry='2.6'/%3E%3Cellipse cx='14.6' cy='6' rx='2' ry='2.6'/%3E%3Cellipse cx='18.4' cy='10.4' rx='2' ry='2.6'/%3E%3Cpath d='M12 11.6c-3 0-6.2 4.6-6.2 6.8 0 1.7 1.5 2.2 3 2.2 1.3 0 2-.6 3.2-.6s1.9.6 3.2.6c1.5 0 3-.5 3-2.2 0-2.2-3.2-6.8-6.2-6.8z'/%3E%3C/svg%3E");
}

/* From 720px a card's padding grows to 1.25rem, so the gap between sections grows past it. */
@media (min-width: 720px) {
    :root {
        --card-pad: 1.25rem;
        --section-gap: var(--s-6);
        --gutter: 3.25rem; /* Today's date tile column, drawn from here */
        --gutter-gap: var(--s-4);
        --inset: var(--s-4);
    }
}

/* ------------------------------------------------------------------ 2. fonts */

/* Source Serif 4 (Adobe, SIL OFL 1.1), Latin subset with both its axes (weight and optical size). The
 * font names "Source" as a Reserved Font Name, so the subset, a Modified Version under the licence,
 * carries its own family name; README "Self-hosted Assets" has the provenance. */
@font-face {
    font-family: "Lease Watch Serif";
    src: url("/static/fonts/lease-watch-serif-latin-opsz-wght-v4.004.woff2") format("woff2");
    font-weight: 200 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Next";
    src: url("/static/fonts/atkinson-hyperlegible-next-latin-wght-v5.3.0.woff2") format("woff2");
    font-weight: 200 800;
    font-style: normal;
    font-display: swap;
}

/* Local stand-ins scaled to the real faces' measured metrics (the overrides are the real ascent and
 * descent divided by size-adjust), so text does not jump when the fonts arrive: Georgia for the serif,
 * Arial for the text face. */
@font-face {
    font-family: "Lease Watch Serif Fallback";
    src: local("Georgia");
    size-adjust: 99.7%;
    ascent-override: 103.9%;
    descent-override: 33.6%;
    line-gap-override: 0%;
}

@font-face {
    font-family: "Atkinson Fallback";
    src: local("Arial");
    size-adjust: 100%;
    ascent-override: 98.4%;
    descent-override: 31.6%;
    line-gap-override: 0%;
}

/* ------------------------------------------------------------------ 3. base */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    font-size: 100%;
    scroll-padding-top: var(--s-4);
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    margin: 0;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font-text);
    font-size: var(--fs-base);
    line-height: 1.5;
    overflow-wrap: break-word;
}

/* Headings: the serif at its optical size for each step (the face's opsz axis; auto is the default,
 * kept here so nothing turns it off), and a heading that wraps never leaves one word on its last line.
 * "pretty", not "balance": balancing evens every line and can split a heading's date ("30 Sep / 2027"),
 * where pretty only moves a word or two down. */
h1,
h2,
h3,
h4 {
    font-family: var(--font-serif);
    font-weight: 600;
    font-optical-sizing: auto;
    line-height: 1.2;
    letter-spacing: -0.01em;
    margin: 0 0 var(--s-3);
    color: var(--ink);
    text-wrap: pretty;
}

h1 {
    font-size: var(--fs-h1);
    line-height: 1.15;
}

h2 {
    font-size: var(--fs-h2);
}

h3 {
    font-size: var(--fs-h3);
}

/* The smallest heading (Nick's rows on the admin's Today, a person on the filing card): a step under
 * h3 and a step over the text, with a tighter margin, so it never falls back to the browser's bold and
 * its wide default margins. */
h4 {
    font-size: var(--fs-h4);
    line-height: 1.25;
    margin-bottom: var(--s-2);
}

p {
    margin: 0 0 var(--s-3);
}

a {
    color: var(--link);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
    text-decoration-color: color-mix(in srgb, currentcolor 40%, transparent);
}

/* A link a keyboard reaches takes the hover's full colour too, beside the focus ring. :where() keeps
 * the rule as light as one class, so a component that sets its own colour (a button's words on navy,
 * a quiet link) keeps it by order and sets its own hover and focus. */
:where(a):is(:hover, :focus-visible) {
    color: var(--navy-strong);
    text-decoration-thickness: 2px;
    text-decoration-color: currentcolor;
}

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

::selection {
    background: var(--highlight);
    color: var(--ink);
}

img {
    max-width: 100%;
}

hr {
    border: 0;
    border-top: 1px solid var(--rule);
    margin: var(--s-6) 0;
}

dl,
ul,
ol {
    margin-top: 0;
}

small,
.small {
    font-size: var(--fs-small);
}

.muted {
    color: var(--muted);
}

/* Words that move to the next line whole rather than break inside: a recipient's address (the recipient
 * filter), the sign-in help's "(an authenticator app)", and a date with its short label ("notice deadline
 * Fri 30 Oct 2026": the words macro, UI review polish U5). An inline block, not white-space: nowrap, so
 * an address longer than a whole line on a phone (a ledger's value column, finishing pass U12) still
 * wraps inside its column instead of running out of it: anywhere, since an address has no space to
 * wrap at, and only when it can't fit whole. */
.keep-whole {
    display: inline-block;
    overflow-wrap: anywhere;
}

/* A link's underline doesn't reach into an inline block, so a date kept whole inside a link would
 * lose it: draw the link's own decoration on the span (UI review polish, code review). */
a .keep-whole {
    text-decoration: inherit;
}

/* Words for screen readers only: clipped, not display: none, so they stay in the accessibility tree
 * (the header's search label and its icon buttons' names). */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

time,
.data-table,
.facts dd {
    font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ 4. layout */

/* One width for every page: no page picks a wider canvas any more, so moving between pages never
 * shifts the column. The page-wide body class some templates still set changes nothing. */
.container {
    width: 100%;
    max-width: var(--page-max);
    margin: 0 auto;
    padding: 0 var(--s-4);
}

.site-main {
    flex: 1;
    padding-top: var(--s-6);
    padding-bottom: 2.5rem;
}

@media (min-width: 720px) {
    .container {
        padding: 0 var(--s-6);
    }

    .site-main {
        padding-top: var(--s-8);
        padding-bottom: var(--s-12);
    }
}

/* A single-purpose page (Account, an alert, an outcome, Records, search, Enter by hand, a client's
 * contacts, a landlord contact) sets the page-measure body class: its head and cards keep to the measure, on the
 * column's own left edge, so a few facts or a short form never sit in a card twice their width. The
 * column itself keeps the one width above. */
.page-measure .site-main > * {
    max-width: var(--measure);
}

.narrow {
    max-width: 32rem;
    margin-left: auto;
    margin-right: auto;
}

/* ------------------------------------------------------------------ 5. header and nav */

/* The header (UI V2 plan U17): the brand, two tabs, the search, Nick's "Add a lease" and the account
 * menu. Nothing here is fixed or sticky, and there is no bottom tab bar. It recedes (design elevation
 * U28): a small serif wordmark, tabs at a medium weight with only the current one in ink, a white search
 * box whose border is all that marks it, words for "Add a lease" and a quiet account button, so each
 * page's own head is the first thing read.
 * - Phone: the brand, the search icon and the account menu on the first row; the tabs and "Add a
 *   lease" on a row of their own under it. The row break is the row's ::before, a flex item as wide
 *   as the row and no height.
 * - From 720px the search form takes the icon's place on the first row.
 * - From 960px everything is one row: brand, tabs, search, "Add a lease", account menu.
 * The order in the markup is the wide order, which is also the keyboard's. */
.site-header {
    background: var(--sheet);
    border-bottom: 1px solid var(--rule);
}

.header-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: var(--s-2);
}

.header-row::before {
    content: "";
    order: 4;
    flex: 0 0 100%;
    height: 0;
}

.brand {
    order: 1;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    padding: var(--s-2) 0;
    color: inherit;
    line-height: 1.15;
    text-decoration: none;
}

.brand-name {
    font-family: var(--font-serif);
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--navy);
}

.brand-sub {
    color: var(--muted);
    font-size: var(--fs-meta);
}

/* The tabs sit at the header's bottom edge, so the current tab's rule meets the header's own. */
.site-nav {
    order: 5;
    align-self: stretch;
    display: flex;
    min-width: 0;
}

.site-nav a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--target);
    padding: 0 var(--s-3);
    border-bottom: 2px solid transparent;
    color: var(--ink-2);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}

.site-nav a:first-child {
    padding-left: 0;
}

.site-nav a:hover {
    color: var(--ink);
    border-bottom-color: var(--rule);
}

.site-nav a[aria-current="page"] {
    color: var(--ink);
    font-weight: 600;
    border-bottom-color: var(--navy);
}

/* The search: an icon link to /search on a phone, the form itself from 720px. */
.site-search {
    display: none;
}

.site-search input::placeholder {
    color: var(--muted);
}

.search-link {
    order: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: var(--target);
    height: var(--target);
    border-radius: var(--radius);
    color: var(--ink-2);
}

.search-link:hover,
.search-link:focus-visible {
    background: var(--paper);
    color: var(--navy);
}

.search-link .icon {
    width: 1.25rem;
    height: 1.25rem;
}

/* "Add a lease" (Nick only): a secondary button, white with a border, at the end of the tabs' row on a
 * phone. A way in on every page shouldn't outrank each page's own main action (layout pass U5), so it
 * shares .btn-secondary's colours in the controls (8), after .btn. Under 24em the words "a lease" are
 * clipped but still read, so the button shows "Add" with its plus and keeps its name; the label is one
 * span, so the space before "a lease" survives. */
.add-lease {
    order: 6;
    flex: none;
    gap: var(--s-1);
    margin: var(--s-1) 0 var(--s-1) auto;
    padding: 0 var(--s-3);
}

.add-lease-more {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The account menu: a details element, so it opens and closes without a script (the button again,
 * or Enter on it). The open menu hangs under its button at the header's right edge, over the page. */
.account {
    order: 3;
    position: relative;
    flex: none;
}

.account > summary {
    flex-direction: row-reverse;
    padding: 0 var(--s-2);
    border-radius: var(--radius);
    color: var(--ink-2);
    font-weight: 500;
    text-decoration: none;
}

.account > summary:hover,
.account[open] > summary {
    background: var(--paper);
    color: var(--ink);
}

/* A menu's chevron points down while it is closed and up while it is open. */
.account > summary::before {
    transform: none;
}

.account[open] > summary::before {
    transform: rotate(180deg);
}

.account-name {
    max-width: 7rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.account-panel {
    position: absolute;
    top: calc(100% + var(--s-1));
    right: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    min-width: 14rem;
    padding: var(--s-2) 0;
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    background: var(--sheet);
    box-shadow: var(--lift);
}

.account-panel .inline-form {
    display: block;
    margin-top: var(--s-1);
    padding-top: var(--s-1);
    border-top: 1px solid var(--rule);
}

.account-panel a,
.account-panel .link-button {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: var(--target);
    padding: 0 var(--s-4);
    color: var(--ink);
    text-align: left;
    text-decoration: none;
}

.account-panel a:hover,
.account-panel .link-button:hover {
    background: var(--paper);
    color: var(--navy-strong);
    text-decoration: underline;
}

@media (min-width: 24em) {
    .add-lease-more {
        position: static;
        width: auto;
        height: auto;
        margin: 0;
        overflow: visible;
        clip-path: none;
    }
}

@media (min-width: 720px) {
    .header-row {
        column-gap: var(--s-4);
    }

    .search-link {
        display: none;
    }

    .site-search {
        order: 2;
        position: relative;
        display: flex;
        flex: 1 1 18rem;
        max-width: 22rem;
        margin-left: auto;
    }

    .site-search input {
        flex: 1 1 auto;
        min-width: 0;
        padding-right: var(--target);
        background: var(--sheet);
    }

    .site-search-go {
        position: absolute;
        top: 0;
        right: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: var(--target);
        height: 100%;
        padding: 0;
        border: 0;
        border-radius: var(--radius);
        background: none;
        color: var(--muted);
        cursor: pointer;
    }

    .site-search-go:hover {
        color: var(--navy);
    }

    .brand {
        flex: 0 1 auto;
    }
}

@media (min-width: 960px) {
    .header-row {
        flex-wrap: nowrap;
    }

    .header-row::before {
        display: none;
    }

    /* On one row the search box gives way first: at 960px a brand that could shrink broke "Lease
     * Watch" over two lines (UI V2 plan U15). */
    .brand {
        flex: none;
    }

    .site-nav {
        order: 2;
        margin-left: var(--s-4);
    }

    .site-search {
        order: 3;
    }

    .add-lease {
        order: 4;
        margin: 0;
    }

    .account {
        order: 5;
    }
}

/* Keyboard users jump past the header with this; it stays off-screen until it has focus. */
.skip-link {
    position: absolute;
    left: var(--s-4);
    top: -6rem;
    z-index: 10;
    padding: var(--s-2) var(--s-4);
    border-radius: var(--radius);
    background: var(--navy);
    color: #ffffff;
}

.skip-link:focus {
    top: var(--s-2);
}

.site-main:focus {
    outline: none;
}

.site-footer {
    border-top: 1px solid var(--rule);
    color: var(--muted);
    font-size: var(--fs-meta);
}

/* The brand line is the header's on screen (its second line), so the footer leaves it out there and
 * gives it on paper only (design elevation U19: said once). */
.footer-brand {
    display: none;
}

.footer-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--s-2);
    padding-top: var(--s-4);
    padding-bottom: var(--s-4);
}

.footer-row p {
    margin: 0;
}

/* ------------------------------------------------------------------ 6. banners */

/* A banner is a full-width strip under the header that says which app this is: a development build
 * (the signal yellow) or production with its alerts paused (solid ink). Its words start where the page's
 * column does, on the brand's edge, so it reads as part of the page, not a notice laid over it; the
 * padding is the container's gutter plus whatever the page's maximum width leaves at the sides. */
.env-banner {
    padding: var(--s-2) calc(max(0px, (100% - var(--page-max)) / 2) + var(--s-4));
    font-size: var(--fs-small);
    font-weight: 600;
    line-height: 1.5;
}

/* Development (design elevation U28): Matt keeps it on screen in the demo, so it is built to be seen:
 * the signal field, one band of hazard stripes along its foot as the edge of the strip, and a DEV tag
 * in ink. Never a look the paused banner can take. */
.env-banner:not(.paused-banner) {
    --stripes: repeating-linear-gradient(135deg, var(--ink) 0 0.375rem, var(--signal) 0.375rem 0.75rem);
    background:
        var(--stripes) bottom / 100% 0.25rem no-repeat,
        var(--signal);
    color: var(--ink);
    padding-bottom: calc(var(--s-2) + 0.25rem);
}

.env-banner:not(.paused-banner)::before {
    content: "DEV";
    display: inline-block;
    margin-right: var(--s-3);
    padding: 0 0.4375rem;
    border-radius: var(--radius);
    background: var(--ink);
    color: var(--signal);
    font-weight: 800;
    letter-spacing: 0.06em;
}

.env-banner a {
    color: var(--ink);
    text-decoration-thickness: 2px;
}

/* Production, alerts paused after a restore: solid ink, a pause icon, never the dev look. The icon sits
 * in the 1.25em box every icon beside words takes, its middle on the words' capitals. */
.paused-banner {
    background: var(--ink);
    color: var(--paper);
}

.paused-banner::before {
    content: "";
    display: inline-block;
    width: 1.25em;
    height: 1.25em;
    margin-right: var(--s-2);
    vertical-align: -0.29em;
    background: currentcolor;
    -webkit-mask: var(--icon-pause) center / contain no-repeat;
    mask: var(--icon-pause) center / contain no-repeat;
}

@media (min-width: 720px) {
    .env-banner {
        padding-inline: calc(max(0px, (100% - var(--page-max)) / 2) + var(--s-6));
    }
}

/* ------------------------------------------------------------------ 7. cards */

/* A page's sections are white cards on the soft ground, the section gap apart (always more than their
 * padding); the narrow cards (sign-in, two-factor, recovery codes, an error) are the same card, in the
 * middle of the page. One treatment for every sheet, the cards and the sheets drawn like them (Today's
 * groups, the runway, the fact list and its pane, the review strip, the composer): white, a hairline,
 * the 8px radius, no shadow. */
.card {
    margin: 0 0 var(--section-gap);
    padding: var(--card-pad);
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: none;
}

.card > :last-child {
    margin-bottom: 0;
}

/* Sections of one sheet (design elevation U24): a page whose cards follow one another (search,
 * Records, Add lease documents, an alert, an outcome, deciding an assignment) wraps them in .one-sheet,
 * so they read as one sheet with a hairline between sections, as Today's groups and the lease abstract
 * do, not a card each. Each card keeps its whole border and overlaps the one before by its hairline,
 * so a card print leaves out (a form's) never takes the next card's top edge with it. */
.one-sheet {
    margin: 0 0 var(--section-gap);
}

.one-sheet > .card {
    margin: 0;
    border-radius: 0;
}

.one-sheet > .card + .card {
    margin-top: -1px;
}

.one-sheet > .card:first-child {
    border-top-left-radius: var(--radius-sheet);
    border-top-right-radius: var(--radius-sheet);
}

.one-sheet > .card:last-child {
    border-bottom-left-radius: var(--radius-sheet);
    border-bottom-right-radius: var(--radius-sheet);
}

.card h2:first-child {
    margin-top: 0;
}

.card h2:first-child,
.card > h2 {
    margin-bottom: 0.75rem;
}

/* .card's margin shorthand comes after .narrow's auto sides, so the narrow card centres itself again
 * here; without it the card hugs the page's left edge on a desk. */
.card.narrow {
    margin-left: auto;
    margin-right: auto;
    border-color: var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: none;
}

/* ------------------------------------------------------------------ 8. controls */

.stack-form {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    margin-bottom: var(--s-4);
}

.stack-form label {
    font-weight: 600;
    font-size: var(--fs-compact);
    color: var(--ink-2);
}

.stack-form label:has(> input[type="checkbox"], > input[type="radio"]) {
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    min-height: var(--target);
    padding: var(--s-2) 0;
    font-weight: 400;
    color: var(--ink);
}

input,
select,
textarea {
    font: inherit;
    font-size: max(1rem, 16px);
    color: var(--ink);
    background: var(--sheet);
    border: 1px solid var(--control);
    border-radius: var(--radius);
}

input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"]),
select {
    min-height: var(--target);
    padding: var(--s-2) var(--s-3);
}

input[type="file"] {
    padding: var(--s-2);
}

textarea {
    padding: var(--s-3);
    line-height: 1.5;
}

input[type="checkbox"],
input[type="radio"] {
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    margin: 0.2rem 0 0;
    accent-color: var(--navy);
}

.stack-form select {
    align-self: flex-start;
    max-width: 100%;
}

/* A fieldset's minimum width is its widest content by default (a box, a long name), so on a phone it
 * could push past its card (deciding an assignment's buyer at 390, the dev page's failure modes at
 * 320). Here it may shrink to its form, and the words in it wrap as words do. */
.stack-form fieldset {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
    margin: 0;
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
}

.stack-form fieldset label {
    font-weight: 400;
}

/* Groups to fill inside a sheet's form are sections of the form, not boxes in the sheet: Enter by
 * hand's three ways to give the notice deadline, a group's space apart, and a new buyer's tier and
 * contact. Each legend reads as a field's label does, as the buyer's and the options question's do
 * (12k, whose legends share the rule). Design elevation U24. The notice period's editor in a fact's
 * Edit sheet (Enter by hand's rule is the same group) and the reminder page's Dismiss choices are
 * groups too (UI review 2026-10-29, V-17: boxes inside sheets). */
#enter-form > fieldset,
.stack-form .buyer-tier,
.stack-form .buyer-contact,
.stack-form .notice-editor,
.stack-form .dismiss-scope {
    padding: 0;
    border: 0;
}

#enter-form > fieldset {
    margin-top: var(--s-4);
}

/* A group's heading (Enter by hand's Notice deadline) stands a section's step under the field
 * before it, nearer the fields it heads than the hint above it (UI review polish U8: it sat about
 * 10px under the expiration's hint). */
#enter-form > h3 {
    margin-top: var(--s-6);
}

#enter-form > fieldset > legend,
.stack-form .buyer-tier > legend,
.stack-form .buyer-contact > legend,
.stack-form .notice-editor > legend,
.stack-form .dismiss-scope > legend,
.decide-step > fieldset > legend {
    margin-bottom: var(--s-2);
    padding: 0;
    color: var(--ink-2);
    font-size: var(--fs-compact);
    font-weight: 600;
}

.stack-form textarea {
    min-height: 22rem;
    resize: vertical;
}

.stack-form .btn {
    margin-top: var(--s-2);
    align-self: flex-start;
}

.inline-form {
    display: inline;
    margin: 0;
}

/* The primary button is navy: one per screen where it can be. The secondary is white with a border. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--target);
    padding: 0 var(--s-4);
    border: 1px solid var(--navy);
    border-radius: var(--radius);
    background: var(--navy);
    color: #ffffff;
    font-family: var(--font-text);
    font-weight: 600;
    font-size: var(--fs-base);
    text-decoration: none;
    cursor: pointer;
}

/* The colour change on hover eases in; a reduced-motion user gets it at once. */
@media (prefers-reduced-motion: no-preference) {
    .btn {
        transition: background-color 120ms ease, border-color 120ms ease;
    }
}

.btn:hover {
    background: var(--navy-strong);
    border-color: var(--navy-strong);
    color: #ffffff;
}

.btn-secondary {
    background: var(--sheet);
    border-color: var(--control);
    color: var(--ink);
}

.btn-secondary:hover {
    background: var(--paper);
    border-color: var(--ink);
    color: var(--ink);
}

/* "Add a lease" is a way in on every page, not the page's action (V2 refined): words and a plus, no
 * box until hover. The lease head's "Add document" (a jump to Documents) is one too (design elevation
 * U24); its selector out-ranks the lease head's button rules (12b) wherever they sit. */
.lease-head-action a.next-action[href="#documents"],
.add-lease {
    border-color: transparent;
    background: transparent;
    color: var(--link);
    font-weight: 600;
}

.lease-head-action a.next-action[href="#documents"]:hover,
.add-lease:hover {
    border-color: transparent;
    background: var(--paper);
    color: var(--navy-strong);
}

.btn:disabled,
.btn[aria-disabled="true"] {
    background: var(--paper);
    border: 1px dashed var(--control);
    color: var(--muted);
    cursor: not-allowed;
}

.link-button {
    font: inherit;
    padding: 0;
    border: 0;
    background: none;
    color: var(--link);
    cursor: pointer;
}

details > summary {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--target);
    cursor: pointer;
    list-style: none;
    color: var(--link);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.18em;
}

details > summary::-webkit-details-marker {
    display: none;
}

details > summary::before {
    content: "";
    flex: none;
    width: 1.1rem;
    height: 1.1rem;
    background: currentcolor;
    -webkit-mask: var(--icon-chevron) center / contain no-repeat;
    mask: var(--icon-chevron) center / contain no-repeat;
    transform: rotate(-90deg);
}

details[open] > summary::before {
    transform: none;
}

/* The chevron flips either way; it turns only without a motion preference. */
@media (prefers-reduced-motion: no-preference) {
    details > summary::before {
        transition: transform 140ms ease;
    }
}

/* A fold's title (an h2 in its summary: "Act on this alert", "Close this deal", "Stop tracking this
 * lease") is in the text face at its strong weight, not the serif, so a closed fold never reads as one
 * more section heading (finishing pass U12, U24-22), and it sits on its summary's line, level with the
 * chevron. In a card the second selector out-ranks the card's first heading's margin (7). Each fold
 * keeps its own size and colour. */
details > summary > h2,
.card details > summary > h2:first-child {
    margin: 0;
    font-family: var(--font-text);
    font-weight: 650;
    letter-spacing: 0;
}

.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    align-items: center;
    margin-top: var(--s-4);
}

/* A link beside a form's buttons (Cancel, Discard, Start over) is a 44px target, as they are. */
.form-actions > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* ------------------------------------------------------------------ 8b. form system */

/* Field widths follow what is typed, not the card around it. Dates, times and numbers are short, a
 * chosen file is a name, and an ordinary text, email or password input keeps the full width the
 * controls above give it (so do the sign-in and one-time-code inputs). These rules come after the
 * controls above and never edit them; the opt-in classes below are for text inputs that hold a short
 * value, such as a decimal or a phone number. The widths are tokens: --field-xs to --field-lg. */
input[type="date"],
input[type="time"],
input[type="number"] {
    width: var(--field-sm);
    max-width: 100%;
}

/* Tab steps through a date or time field's parts (the month, the day, the year, then its picker
 * button). Focus is inside the field, so on the picker button the field matches neither :focus-visible
 * nor :focus, only :focus-within, and section 3's ring would vanish there. The field keeps the same
 * ring whenever any of its parts has focus. */
input:is([type="date"], [type="time"]):focus-within {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

input[type="file"] {
    width: var(--field-lg);
    max-width: 100%;
}

input.field-narrow {
    width: var(--field-xs);
    max-width: 100%;
}

input.field-short {
    width: var(--field-sm);
    max-width: 100%;
}

input.field-medium {
    width: var(--field-md);
    max-width: 100%;
}

/* A form of a few short answers (a password and its repeat) stays about as wide as the sign-in form on
 * a desk rather than spanning its card: the form is narrower, and its inputs still fill it as the
 * controls give them. Opt-in on the form, so no input type is sized here. */
.form-short {
    max-width: var(--field-lg);
}

/* A label with its control and, when there is one, its help line. A row of fields stacks on a phone
 * and wraps from 400px; a grow field takes the room the others leave. */
.field {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-width: 0;
}

.field-row {
    display: flex;
    flex-direction: column;
    gap: var(--s-2) var(--s-6);
}

.stack-form > :is(.field, .field-row) ~ :is(.field, .field-row) {
    margin-top: var(--s-2);
}

.stack-form .field-row .btn {
    margin-top: 0;
    align-self: flex-start;
}

@media (min-width: 400px) {
    .field-row {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: flex-end;
    }

    .field-row > .field-grow {
        flex: 1 1 14rem;
    }

    .stack-form .field-row .btn {
        align-self: flex-end;
    }
}

/* A notice period's number and its unit ("90" "days") are one answer, so they keep one row at every
 * width, close together; on the narrowest phone the number gives up width before the unit wraps (UI
 * review 2026-10-29, V-17: at 390 each stood on a line of its own). */
.notice-editor > .field-row {
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--s-2);
}

.notice-editor > .field-row > input {
    min-width: 0;
}

/* The native file control stays visible, so the browser still prints the chosen file's name beside
 * the button; its button is dressed like the secondary button and the field's own box goes (design
 * elevation U24: a button inside a box read as the browser's default), the file's name in the second
 * ink beside it. */
input.file-field {
    padding: 0;
    border-color: transparent;
    background: transparent;
    color: var(--ink-2);
    cursor: pointer;
}

input.file-field::file-selector-button {
    min-height: var(--target);
    margin-right: var(--s-3);
    padding: 0 var(--s-4);
    border: 1px solid var(--control);
    border-radius: var(--radius);
    background: var(--sheet);
    color: var(--ink);
    font-family: var(--font-text);
    font-size: var(--fs-base);
    font-weight: 650;
    cursor: pointer;
}

input.file-field:hover::file-selector-button {
    border-color: var(--ink);
}

/* Help sits under the control it describes. A field the browser has judged invalid gets a heavier
 * border, and its help line turns into the message: words in the same slot, so the cue is never the
 * colour alone. A message the server wrote for one field uses the same slot with .field-error. */
.field-help {
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

.field-error,
.field:has(:user-invalid) > .field-help {
    color: var(--urgent);
    font-weight: 600;
}

.field:has(:user-invalid) > .field-help::after {
    content: " Check this value.";
}

input:user-invalid,
select:user-invalid,
textarea:user-invalid {
    border-color: var(--urgent);
    box-shadow: inset 0 0 0 1px var(--urgent);
}

/* A field the server refused and named (aria-invalid) is dressed the same way. The browser never
 * judged it, so :user-invalid cannot: a rule such as "that is the value you were shown" lives on the
 * server. The words are in the message the field points at, never the border alone. */
:is(input, select, textarea)[aria-invalid="true"] {
    border-color: var(--urgent);
    box-shadow: inset 0 0 0 1px var(--urgent);
}

/* ------------------------------------------------------------------ 9. tables */

.table-wrap {
    overflow-x: auto;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-compact);
}

.data-table th,
.data-table td {
    text-align: left;
    vertical-align: top;
    padding: var(--s-3) var(--s-3) var(--s-3) 0;
    border-bottom: 1px solid var(--rule);
}

/* Column names are small, quiet words over one firm hairline, in the text face. */
.data-table th {
    font-family: var(--font-text);
    font-weight: 600;
    font-size: var(--fs-meta);
    letter-spacing: 0;
    color: var(--muted);
    border-bottom: 1px solid var(--rule-strong);
}

.data-table tbody tr:hover {
    background: var(--paper);
}

/* A ledger (a statement's deals, read across to a total) keeps its columns side by side: on a phone
 * it scrolls sideways inside its .table-wrap instead of squeezing each column to a word a line. A
 * table of a few records restacks instead (section 9b). */
.data-table.ledger {
    min-width: 52rem;
}

/* A table that ends its card needs no rule under its last row: the card's edge closes it. */
.card > .table-wrap:last-child .data-table tbody tr:last-child td {
    border-bottom: 0;
}

/* The daily job's runs (UI V2 plan U15). Side by side, their four columns need about 286px at the
 * least (half a date, the "completed" chip, the word Attempts, the result's longest word), more than
 * a 320px phone's card holds, so on a phone each run is two lines: its date, status and attempts on
 * fixed tracks, then its result across the row, under a heading row of the same shape. From 560px it
 * is a table again. The roles in the markup keep it a table for a screen reader whatever its display. */
.runs-table,
.runs-table thead,
.runs-table tbody {
    display: block;
}

.runs-table tr {
    display: grid;
    grid-template-columns: 3.25rem 7rem minmax(0, 1fr);
    gap: var(--s-1) var(--s-2);
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--rule);
}

.runs-table thead tr {
    border-bottom-width: 2px;
}

/* A heading keeps the column names' hairline (.data-table th, above); a cell has no border of its
 * own, as the row draws the line. */
.runs-table th,
.runs-table td {
    padding: 0;
}

.runs-table td {
    border: 0;
}

.runs-table :is(th, td):last-child {
    grid-column: 1 / -1;
}

.card > .table-wrap:last-child .runs-table tbody tr:last-child {
    border-bottom: 0;
}

/* From 24em the date fits whole beside its status. */
@media (min-width: 24em) {
    .runs-table tr {
        grid-template-columns: 5.5rem 7rem minmax(0, 1fr);
    }
}

@media (min-width: 560px) {
    .runs-table {
        display: table;
    }

    .runs-table thead {
        display: table-header-group;
    }

    .runs-table tbody {
        display: table-row-group;
    }

    .runs-table tr {
        display: table-row;
        padding: 0;
        border: 0;
    }

    .runs-table th,
    .runs-table td {
        padding: var(--s-3) var(--s-3) var(--s-3) 0;
    }

    .runs-table td {
        border-bottom: 1px solid var(--rule);
    }
}

.data-table select {
    max-width: 22rem;
}

/* ------------------------------------------------------------------ 10. status chips and alerts */

/* The V2 tones as classes, for the chip below and any tinted block (a group's header, a callout): a
 * tone sets --tone and --tone-bg and a default icon; an icon class after it picks another glyph. */
.tone-urgent {
    --tone: var(--urgent);
    --tone-bg: var(--urgent-bg);
    --icon: var(--icon-warning);
}

.tone-action {
    --tone: var(--action);
    --tone-bg: var(--action-bg);
    --icon: var(--icon-decision);
}

.tone-send {
    --tone: var(--send);
    --tone-bg: var(--send-bg);
    --icon: var(--icon-mail);
}

.tone-evidence {
    --tone: var(--evidence);
    --tone-bg: var(--evidence-bg);
    --icon: var(--icon-eye);
}

.tone-missing {
    --tone: var(--missing);
    --tone-bg: var(--missing-bg);
    --icon: var(--icon-question);
}

.tone-ok {
    --tone: var(--ok);
    --tone-bg: var(--ok-bg);
    --icon: var(--icon-check);
}

.tone-neutral {
    --tone: var(--neutral);
    --tone-bg: var(--neutral-bg);
    --icon: var(--icon-dot);
}

.icon-check { --icon: var(--icon-check); }
.icon-eye { --icon: var(--icon-eye); }
.icon-decision { --icon: var(--icon-decision); }
.icon-cross { --icon: var(--icon-cross); }
.icon-clock { --icon: var(--icon-clock); }
.icon-dot { --icon: var(--icon-dot); }
.icon-info { --icon: var(--icon-info); }
.icon-warning { --icon: var(--icon-warning); }
.icon-pause { --icon: var(--icon-pause); }
.icon-zzz { --icon: var(--icon-zzz); }
.icon-bell-off { --icon: var(--icon-bell-off); }
.icon-mail { --icon: var(--icon-mail); }
.icon-question { --icon: var(--icon-question); }
.icon-flag { --icon: var(--icon-flag); }
.icon-calendar { --icon: var(--icon-calendar); }
.icon-search { --icon: var(--icon-search); }
.icon-plus { --icon: var(--icon-plus); }
.icon-dental { --icon: var(--icon-dental); }
.icon-medical { --icon: var(--icon-medical); }
.icon-veterinary { --icon: var(--icon-veterinary); }

/* A tinted block in its tone's colours (text on its own background, a checked pair). */
.tinted {
    background: var(--tone-bg, var(--neutral-bg));
    color: var(--tone, var(--neutral));
}

/* The V2 status chip, the one status component (the old badges became chips in the design
 * elevation's U27): its icon and a short word in sentence case ("Overdue", "On hold"). Only the tones
 * that need Nick (urgent, action) are filled; every other tone keeps its colour on the icon and sets
 * the word in ink. It is in the text face wherever it sits, a heading's serif included (a contact's
 * name with its "primary"). No border style is a third cue (the header says why): the clear border
 * turns into a visible one in forced colours, where the tint is dropped. */
/* The chip's baseline is its words (align-items: baseline, the icon centred apart from it): a flex
 * box takes the first baseline-aligned item's, and the icon, first and wordless, put the chip's
 * baseline at its foot, so a chip on a line of text (a verdict) sat above the words beside it. */
.chip {
    display: inline-flex;
    align-items: baseline;
    gap: 0.375rem;
    padding: 0.125rem 0.625rem 0.125rem 0.5rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background: transparent;
    color: var(--ink-2);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: 0;
    line-height: 1.4;
    text-transform: none;
    white-space: nowrap;
}

.chip:is(.tone-urgent, .tone-action) {
    background: var(--tone-bg);
    color: var(--tone);
}

/* Unfilled, a chip has no box to pad: its icon starts on the text edge, where a filled pill's edge
 * starts, so a column of chips keeps one left edge either way. */
.chip:not(.tone-urgent, .tone-action) {
    padding-left: 0;
    padding-right: 0;
}

/* A chip that took a badge's place may carry a phrase, not a status word: a reading's verdict
 * ("Can't set the deadline by itself"), an amendment value's "replaced by ..., effective ...", a
 * document's reading in the lease page's side column, a fact's value (a deal's flags, a backup's
 * results). There it wraps its words inside the pill when its line is too narrow, as the badge did,
 * rather than run off a phone's card. */
.verdict > .chip,
.amendment-status > .chip,
.doc-line .chip,
.facts dd > .chip {
    white-space: normal;
}

/* An icon on its own: an empty element (aria-hidden) beside the words it illustrates. */
.icon,
.chip::before {
    content: "";
    display: inline-block;
    flex: none;
    width: 1rem;
    height: 1rem;
    background: currentcolor;
    -webkit-mask: var(--icon, var(--icon-dot)) center / contain no-repeat;
    mask: var(--icon, var(--icon-dot)) center / contain no-repeat;
}

/* -0.29em centres the 1.25em box on the text face's capitals (Atkinson's middle sits 0.334em up). */
.icon {
    width: 1.25em;
    height: 1.25em;
    vertical-align: -0.29em;
}

/* A chip's icon is in its tone's colour; a filled chip's is in the word's. */
.chip::before {
    align-self: center;
    background: var(--tone, var(--neutral));
}

.chip:is(.tone-urgent, .tone-action)::before {
    background: currentcolor;
}

/* Status lines: their tint and icon, no coloured edge stripe (so the tones below set no border
 * colour; the clear border still turns visible in forced colours). */
.alert {
    position: relative;
    margin: 0 0 var(--s-3);
    padding: var(--s-3) var(--s-4) var(--s-3) 2.75rem;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--sheet);
}

.alert::before {
    content: "";
    position: absolute;
    top: var(--s-3);
    left: var(--s-3);
    width: 1.35rem;
    height: 1.35rem;
    background: currentcolor;
    -webkit-mask: var(--icon-info) center / contain no-repeat;
    mask: var(--icon-info) center / contain no-repeat;
}

.alert-info {
    background: var(--evidence-bg);
    color: var(--evidence);
}

.alert-success {
    background: var(--ok-bg);
    color: var(--ok);
}

/* after the generic .alert::before, so the check wins over the information icon */
.alert-success::before {
    -webkit-mask-image: var(--icon-check);
    mask-image: var(--icon-check);
}

.alert-warning {
    background: var(--action-bg);
    color: var(--action);
}

.alert-warning::before {
    -webkit-mask-image: var(--icon-warning);
    mask-image: var(--icon-warning);
}

.alert-error {
    background: var(--urgent-bg);
    color: var(--urgent);
}

.alert-error::before {
    -webkit-mask-image: var(--icon-cross);
    mask-image: var(--icon-cross);
}

/* A status line with its Undo (corrections plan U8): the words and the quiet button run on as one
 * line in the tint, and the button's 44px target reaches into the line's padding without growing
 * the line (the client link's negative margin), so the icon stays beside the first line. */
.undo-line p {
    display: inline;
}

.undo-line .todo-link {
    margin-block: calc((1lh - var(--target)) / 2);
}

/* htmx request indicators (htmx's own injected styles are disabled for the CSP) */
.htmx-indicator {
    opacity: 0;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    opacity: 1;
}

/* ------------------------------------------------------------------ 10b. busy forms */

/* static/busy.js marks a form busy while the browser waits for the server, and the button that was
 * pressed with it. The button's own text never changes, so the words that show come from its
 * data-busy-label; the script also writes them into one live region for screen readers. That region
 * is clipped, not display: none, so it stays in the accessibility tree. */
.busy-status {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The pressed button: a solid, quiet look that is never the dashed look of a blocked button, with a
 * cursor that says "wait" and not "no". It follows .btn:hover and .btn:disabled in source order on
 * purpose. The real text turns transparent (it keeps its place, and a screen reader still reads it)
 * and the label is drawn over it in ::after. The button is a column so the real text and the label
 * share one width and the wider one sets it: a long button keeps its size and a short one grows only
 * as far as the label needs. A line height of zero lets the label supply the height. */
.btn.is-busy,
.btn.is-busy:hover {
    position: relative;
    flex-direction: column;
    background: var(--paper);
    border: 1px solid var(--control);
    color: transparent;
    line-height: 0;
    cursor: progress;
}

.btn.is-busy::after {
    content: attr(data-busy-label);
    /* the empty alternative keeps the label out of the button's name; the line above is the fallback */
    content: attr(data-busy-label) / "";
    color: var(--ink-2);
    line-height: 1.25;
}

/* Only the pressed button looks alive. */
[data-busy] .btn:not(.is-busy) {
    opacity: 0.55;
    cursor: progress;
}

/* A small spinner is an extra for people who have not asked for less motion; the label says the same. */
@media (prefers-reduced-motion: no-preference) {
    .btn.is-busy::before {
        content: "";
        position: absolute;
        inset-block: 0;
        left: 0.4rem;
        width: 0.65rem;
        height: 0.65rem;
        margin-block: auto;
        border: 2px solid var(--control);
        border-top-color: var(--ink-2);
        border-radius: 50%;
        animation: busy-spin 800ms linear infinite;
    }

    /* room beside the label for the spinner, so the two never touch */
    .btn.is-busy::after {
        margin-left: 0.5rem;
    }

    @keyframes busy-spin {
        to {
            transform: rotate(1turn);
        }
    }
}

/* ------------------------------------------------------------------ 11. small components */

/* A list of facts is a ledger at every width. On a phone it takes Email sent's shape (12c): the quiet
 * label in a fixed 7.5rem column, a long one wrapping inside it, level with its value's first line
 * (finishing pass U12, U24-21); a chip or a note inside the value wraps in the value's column. From
 * 560px the label column takes its longest label and the labels the text's size. The lease summary
 * and the decide pages draw their own ledger (12b). */
.facts {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    align-items: baseline;
    gap: var(--s-2) var(--s-4);
    margin: 0 0 var(--s-3);
}

.facts dt {
    color: var(--muted);
    font-size: var(--fs-small);
}

.facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

@media (min-width: 560px) {
    .facts {
        grid-template-columns: max-content minmax(0, 1fr);
        align-items: normal;
        gap: var(--s-1) var(--s-6);
    }

    .facts dt {
        font-size: inherit;
    }
}

/* A list of records with one list of facts each (every backup, every restore): a rule between two
 * records, so the second's first line never reads as the first's last. */
.facts-entry + .facts-entry {
    padding-top: var(--s-3);
    border-top: 1px solid var(--rule);
}

/* The changes waiting for approval share one card: a rule between two, none over the first, which
 * opens the card (the page head stands on the paper above it, layout pass U3). */
.approval + .approval {
    border-top: 1px solid var(--rule);
    padding-top: var(--s-4);
    margin-top: var(--s-4);
}

code {
    font-family: var(--font-text);
    font-variant-numeric: slashed-zero;
    overflow-wrap: anywhere;
}

code.secret,
code.uri {
    display: block;
    margin: var(--s-1) 0;
    padding: var(--s-2) var(--s-3);
    background: var(--paper);
    border: 1px solid var(--control);
    border-radius: var(--radius);
    overflow-wrap: anywhere;
}

code.secret {
    font-size: 1.125rem;
    letter-spacing: 0.08em;
}

.count-list,
.link-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2) var(--s-6);
    list-style: none;
    padding: 0;
}

.link-row {
    margin: var(--s-2) 0 0;
}

/* A link that stands on a line of its own (a download, a way back, "Use a recovery code") is a 44px
 * target, not a line of text: link-row lists them side by side, link-line is a paragraph holding one,
 * and cell-link is the link that opens a table row's record (a statement's deal). */
.link-line {
    margin: 0;
}

.link-row a,
.link-line > a,
.cell-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* A fact whose value is a link alone ("Download PDF" on a document page) holds it as a link-line, so
 * its row is a 44px target tall; beside it from 560px the fact's name sits level with the link's
 * words. */
.facts dt:has(+ dd.link-line) {
    align-self: center;
}

/* In a table row on a desk a cell's link reads level with the other cells' first lines: its 44px box
 * reaches into the cell's padding instead of pushing its words down. The pull is sized for one line,
 * so a link with more of its cell under it (a statement's client name over its premises), which may
 * wrap, takes its reach from padding round its words: the pull then takes back only that padding, at
 * one line or three, and its last line never runs into the line below (UI review 2026-10-29 V-01). */
@media (min-width: 720px) {
    .data-table td > .cell-link {
        margin-block: calc((1lh - var(--target)) / 2);
    }

    .data-table td > .cell-link:has(~ *) {
        padding-block: calc((var(--target) - 1lh) / 2);
    }
}

.issue-list {
    margin: var(--s-1) 0 0;
    padding-left: 1.1rem;
    color: var(--ink-2);
    font-size: var(--fs-compact);
}

.issue-list li {
    margin-bottom: var(--s-2);
}

.block-list {
    margin: var(--s-2) 0 0;
    padding-left: 1.1rem;
}

.block-list li {
    margin-bottom: var(--s-2);
}

/* A sent email is shown as the letter it was: a white sheet at reading width. The letter is set in the
 * serif, as the lease's own words are (design elevation U18): words that leave the app read as a
 * letter, not as the app's own text, and its figures (an address, a date) have no slashed zero. */
.email-preview {
    max-width: var(--measure);
    white-space: pre-wrap;
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-variant-numeric: lining-nums proportional-nums;
    line-height: 1.6;
    background: #ffffff;
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: var(--lift);
    padding: var(--s-4) var(--s-6);
    margin: var(--s-2) 0 var(--s-4);
}

/* The locked footer under the draft's text box: the same sheet as the email, set back a step (paper
 * ground, softer ink, no lift) and as wide as the box above it, so it reads as added for you and
 * not as something to type in. */
.email-preview--footer {
    max-width: none;
    margin: 0;
    padding: var(--s-3) var(--s-4);
    background: var(--paper);
    color: var(--ink-2);
    box-shadow: none;
}

/* ------------------------------------------------------------------ 11b. date tile, page head, empty state */

/* A calendar date without its leaf: the month in small sentence-case words in the accent over the day
 * in the serif, and no box (a box in a sheet is a card in a card: design elevation U20, tells.md S2).
 * Decorative (aria-hidden): the same date is always written out beside it. Red only for the month of an
 * overdue date; grey for a past one; today's in the accent. The month and the year are the brief's
 * smallest text (--fs-meta), never under it. */
.dtile-wrap {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-1);
    vertical-align: top;
}

.dtile {
    display: inline-flex;
    flex-direction: column;
    align-items: stretch;
    min-width: 3.25rem;
    color: var(--ink);
    line-height: 1;
    text-align: center;
}

.dtile-m {
    padding: 0.25rem 0.4rem 0;
    background: transparent;
    color: var(--navy);
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
}

.dtile-d {
    padding: 0.25rem 0.4rem 0.15rem;
    font-family: var(--font-serif);
    font-size: 1.75rem;
    font-weight: 600;
    font-variant-numeric: lining-nums tabular-nums;
}

.dtile-y {
    padding: 0 0.125rem 0.3rem;
    color: var(--muted);
    font-size: var(--fs-meta);
    font-weight: 500;
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
}

.dtile--large .dtile-d {
    font-size: 2.25rem;
}

.dtile--small {
    min-width: 2.75rem;
}

.dtile--small .dtile-d {
    font-size: 1.25rem;
}

.dtile--past {
    color: var(--muted);
}

.dtile--past .dtile-m {
    background: transparent;
    color: var(--muted);
}

.dtile--today {
    color: var(--navy);
}

.dtile--today .dtile-m {
    background: transparent;
    color: var(--navy);
}

.dtile--today .dtile-y {
    color: var(--muted);
}

.dtile--overdue .dtile-m {
    background: transparent;
    color: var(--urgent);
}

.dtile-label {
    color: var(--muted);
    font-size: var(--fs-small);
}

/* the relative hint that sits beside a written-out date ("in 12 days") */
.when {
    margin-left: 0.35em;
    color: var(--muted);
    font-size: var(--fs-small);
}

/* Who entered a fact by hand, and when: a quiet line under the value. */
.byline {
    display: block;
    color: var(--muted);
    font-size: var(--fs-small);
}

/* A lead line, or the paragraph explaining the page, straight under the title sits --s-2 from it. A
 * status line (.alert), an empty state and the print-only line keep their own gaps (below). */
:where(.page-head) + p:where(:not(.alert, .empty, .print-only)) {
    margin-top: var(--s-2);
}

/* On the paper (a head outside a card) the head block (the head, its lead and explainer, the page's
 * status lines) ends one section gap above the first section. The head has no padding of its own, a
 * status line sits --s-4 under it as under a lead line, and the sections take the gap above them: it
 * collapses with the head block's last margin, always smaller, and with the gap under the section
 * before. The sections that are not cards themselves take it too: the page view's two columns, the
 * draft's email and its facts, a page's one sheet (U24), and the form deciding an assignment, which
 * holds its sheet. */
.site-main > .page-head + .alert {
    margin-top: var(--s-4);
}

.site-main > .page-head ~ :is(.card, .runway, .queue-strip),
.site-main > .page-head ~ :is(.page-layout, .email-review, .decide-form, .one-sheet) {
    margin-top: var(--section-gap);
}

/* The print-only "as of" line under a head takes no room on screen, so a status line after it
 * keeps the --s-4 a status line sits under a head; on paper the line itself sits --s-2 under the
 * title. The outcomes page keeps its head block in a wrapper of its own (div.opp-head, 12f). */
:is(.site-main, .opp-head) > .page-head + .print-only {
    margin-top: var(--s-2);
}

:is(.site-main, .opp-head) > .page-head + .print-only + .alert {
    margin-top: var(--s-4);
}

.crumbs {
    margin: 0 0 0.5rem;
    color: var(--muted);
    font-size: var(--fs-small);
}

.crumbs a {
    text-decoration: none;
}

.crumbs a:hover,
.crumbs a:focus-visible {
    text-decoration: underline;
}

/* The crumbs' separator is a decorative slash (aria-hidden), quiet. */
.crumbs > span[aria-hidden="true"] {
    padding: 0 0.125rem;
    color: var(--muted);
}

/* Under a touch pointer each crumb is a 44px target (UI review 2026-10-29 V-48): padding above and
 * below its words, in a box of its own, so crumbs that wrap on a phone stand a target apart. A mouse
 * keeps the small line, whose spacing passes WCAG 2.5.8. */
@media (pointer: coarse) {
    .crumbs a {
        display: inline-block;
        padding-block: calc((var(--target) - 1lh) / 2);
    }
}

.page-head-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0.75rem;
}

/* A title may be one long word (a dropped file's name on its card): it breaks rather than run past
 * the card on a phone (UI V2 plan U15). */
.page-head-row h1 {
    margin: 0;
    overflow-wrap: anywhere;
}

/* The practice type beside a lease's name: a quiet label in the text face, not a second pill. */
.practice-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 500;
    text-transform: capitalize;
}

.page-head-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-3);
    margin-top: var(--s-4);
}

/* Leases' "Add lease documents" sits on the title's line as a secondary button: an intake action never
 * outranks the leases that need Nick. */
.page-head:has(> .page-head-actions > a.btn[href="/drops"]) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1rem;
}

.page-head-actions:has(> a.btn[href="/drops"]) {
    margin-top: 0;
}

.page-head-actions > a.btn[href="/drops"] {
    border-color: var(--control);
    background: var(--sheet);
    color: var(--ink);
}

.page-head-actions > a.btn[href="/drops"]:hover {
    border-color: var(--ink);
    background: var(--paper);
    color: var(--ink);
}

/* The lead line under a page's heading: who or what the page is about (a lease's premises, the
 * client and premises on Enter by hand) or what the page is for (Add lease documents), --s-2 under the
 * title and no wider than the measure. It is a fact line in the text face, as an address is on Today
 * and Leases: in the serif, even a step under the lead size, it read as a second title under the
 * serif one (design elevation U16, U24). */
.lede {
    max-width: var(--measure);
    margin: var(--s-1) 0 0;
    color: var(--ink-2);
    font-size: var(--fs-compact);
}

/* The paragraph explaining a page, under its title or its lead line, keeps to the measure too, in the
 * muted grey. */
:where(.page-head, .page-head + .lede) + p:where(:not(.alert, .empty, .print-only)) {
    max-width: var(--measure);
    color: var(--muted);
}

/* A notice under the lead line (received, discarded, a file dropped again, a read that failed)
 * keeps the gap the page head leaves, instead of sitting flush under the paragraph. */
.lede + .alert {
    margin-top: var(--s-4);
}

/* The line under a lead line (Enter by hand's term or "Type what the lease says", the landlord
 * contact form's and an assignment's muted lines) and the form under a list of links (the alert
 * page's "Writes an email..." under a sent draft) keep a paragraph's gap: neither the lead line nor
 * the list has a bottom margin of its own, so they sat flush (UI V2 plan U15). */
.lede + p,
.link-row + .stack-form {
    margin-top: var(--s-3);
}

.empty {
    margin: 0;
    padding: var(--s-3) 0;
    color: var(--muted);
    font-size: var(--fs-reading);
}

/* an all-clear list ("Nothing waiting on you.") is good news, so it gets the check */
.empty--calm {
    color: var(--ok);
}

.empty--calm::before {
    content: "";
    display: inline-block;
    width: 1.1em;
    height: 1.1em;
    margin-right: var(--s-2);
    vertical-align: -0.2em;
    background: currentcolor;
    -webkit-mask: var(--icon-check) center / contain no-repeat;
    mask: var(--icon-check) center / contain no-repeat;
}

/* ------------------------------------------------------------------ 11c. shared components */

/* The macros in _components.html (UI V2 plan U17); the units that build Today, Leases, the lease page
 * and the fact list use them. The chip itself is in section 10. */

/* A group's heading: its tone's tint, its icon, the title in the serif and a count or note at the end.
 * Today's groups (12g) put it in a disclosure's summary and add a chevron after the count. */
.group-head {
    display: flex;
    align-items: center;
    gap: var(--s-2) var(--s-3);
    margin: 0;
    padding: var(--s-2) var(--s-3);
    border-radius: var(--radius);
    font-size: var(--fs-h3);
}

.group-title {
    flex: 1 1 auto;
    min-width: 0;
}

.group-count {
    flex: none;
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 650;
}

/* Facts as rows: the label over the value on a phone, beside it from 560px; a missing value reads
 * "Not recorded" in the muted colour, and the source is quiet words after the value, its tag's border
 * clear. */
.fact-rows {
    margin: 0 0 var(--s-3);
}

.fact-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0 var(--s-4);
    padding: var(--s-2) 0;
    border-bottom: 1px solid var(--rule);
}

.fact-row:last-child {
    border-bottom: 0;
}

.fact-label {
    color: var(--muted);
    font-size: var(--fs-small);
}

.fact-value {
    margin: 0;
    overflow-wrap: anywhere;
}

.fact-empty {
    color: var(--muted);
}

.fact-source {
    display: inline-block;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 999px;
    color: var(--muted);
    font-size: var(--fs-meta);
    white-space: nowrap;
}

@media (min-width: 560px) {
    .fact-row {
        grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
        align-items: baseline;
    }

    .fact-label {
        font-size: inherit;
    }

    /* A value to check or type as a row of its sheet (finishing pass U7, U24-17): its head (the
     * label and the reading's chip, 12i) over the value on a phone; from here beside it, in the
     * lease summary's columns (12b), so a box lines up with a ledger's values. The head flows as
     * words, so a long label wraps with its chip after its last word, level with the box's words;
     * what sits under the value (the AI's quote, an error) stays in the value's column, no wider
     * than a box alone, and close under it: rows part by a larger step (12i, 12k), so a fold reads
     * as its own box's. Deciding an assignment (12k) and the filing card (12i, U8). */
    .card .value-row {
        display: grid;
        grid-template-columns: minmax(9rem, 14rem) minmax(0, 1fr);
        align-items: baseline;
        gap: var(--s-1) 0;
        max-width: calc(14rem + var(--field-lg));
    }

    .value-row > :not(.value-head) {
        grid-column: 2;
    }

    .value-row > .value-head {
        display: block;
        grid-row: span 2;
        padding-right: var(--s-4);
    }
}

/* The next action: one button, a link or a one-button form; its icon sits before the words. */
.next-action-form {
    display: inline-flex;
    margin: 0;
}

.next-action {
    gap: var(--s-2);
}

.next-action .icon {
    width: 1.1em;
    height: 1.1em;
}

/* The hero banner: the picture is decoration, every word sits on the scrim. On a phone the picture is a
 * slim strip over the words; from 720px it fills the banner and the words sit on a scrim panel that
 * fades out to its right. With no picture, or one that fails to load (its alt is empty, so nothing is
 * drawn in its place), the banner is the scrim alone and reads the same. It keeps the lake at the
 * height of its words: Today's greeting is the title, a step down, the date the aside, quiet, the
 * summary line the lead, in the serif; a warning or the message line an action came back with sits
 * under them as an alert on its own tint. A hairline like every sheet. It is Today's head block, so the
 * groups start one section gap under it. */
.banner {
    position: relative;
    display: flex;
    flex-direction: column;
    margin: 0 0 var(--section-gap);
    overflow: hidden;
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    background: var(--scrim);
    color: var(--scrim-ink);
}

/* Matt's lake is about three times as wide as it is tall, so in every banner it fills the width and is
 * cropped top and bottom: 45% keeps the peaks, the treeline and a band of water. Should the banner grow
 * taller than the picture's shape (a long warning), the crop moves sideways instead, and 70% keeps the
 * pines on the right. */
.banner-img {
    display: block;
    width: 100%;
    height: 4rem;
    object-fit: cover;
    object-position: 70% 45%;
}

.banner-body {
    position: relative;
    padding: 1rem;
    background: var(--scrim);
}

.banner-title {
    margin: 0;
    color: var(--scrim-ink);
    font-size: 1.5rem;
}

.banner-aside {
    margin: 0.125rem 0 0;
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

.banner-lead {
    margin: 0.5rem 0 0;
    font-family: var(--font-serif);
    font-size: var(--fs-lead);
    font-weight: 600;
}

.banner-next {
    margin: var(--s-1) 0 0;
    color: var(--ink-2);
}

.banner-body > .alert {
    margin: var(--s-3) 0 0;
}

@media (min-width: 720px) {
    .banner {
        min-height: 0;
    }

    .banner-img {
        position: absolute;
        inset: 0;
        height: 100%;
        object-position: 70% 52%;
    }

    /* The panel runs the banner's full height, its words centred in it, so no strip of the picture
     * shows above or below them. */
    .banner-body {
        display: flex;
        flex: 1 0 auto;
        flex-direction: column;
        justify-content: center;
        width: min(100%, 34rem);
        padding: 1.5rem 2rem;
    }

    .banner-title {
        font-size: 1.875rem;
    }

    .banner-lead {
        font-size: 1.25rem;
    }

    .banner-body::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: 100%;
        width: 6rem;
        background: linear-gradient(90deg, var(--scrim), transparent);
    }
}

/* A picture from the ART table (art() in _components.html, web/art.py; design elevation U11): decoration
 * beside words that already read on their own, never behind them. Its width and height attributes are its
 * slot's size, so the box keeps the picture's shape while the file loads and nothing moves when it lands;
 * on a narrower screen it shrinks to its column at the same shape. A spot with no picture in the table
 * draws no img at all, so the page keeps its finished-without-art layout and reserves nothing. Each
 * page's own section places its picture and hides it where it would crowd. */
.art {
    position: relative;
    display: block;
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-sheet);
}

/* A picture that fails to load (blocked, say): Chrome draws a frame and a broken-picture mark in its box
 * even with an empty alt, so a cover in the colour of what the picture sits on hides them, and the box
 * reads as the ground. A loaded picture is a replaced element and draws no ::after, so the cover exists
 * only while the picture is missing. --art-ground is the paper, or a card's white inside a card. */
.art::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--art-ground, var(--paper));
}

.card .art {
    --art-ground: var(--sheet);
}

/* The banner's picture takes the same cover, in the scrim it sits on, so a blocked lake leaves the scrim
 * alone (its words sit later in the banner, above the cover). */
.banner-img::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--scrim);
}

/* ------------------------------------------------------------------ 12a. welcome */

/* The first-run panel on Today (_welcome.html): its paragraphs keep a reading width. The checklist
 * itself is 13b. */
.welcome p {
    max-width: 38rem;
}

/* ------------------------------------------------------------------ 9b. rows that restack on a phone */

/* Opt-in (the tables that carry class restack and explicit roles): narrow, each row is a block of the
 * sheet it sits in, its column names over the values and a hairline between rows (a box of its own in
 * a sheet is a card in a card: design elevation U21, as Leases' rows in U15); wide, it is an ordinary
 * table again. */
.restack,
.restack tbody,
.restack tr,
.restack td {
    display: block;
}

.restack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}

.restack tr {
    position: relative;
    padding: var(--s-3) 0;
    border-top: 1px solid var(--rule);
}

.restack tr:first-child {
    padding-top: 0;
    border-top: 0;
}

.restack td {
    padding: var(--s-1) 0;
    border: 0;
}

.restack td::before {
    content: attr(data-label);
    display: block;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 650;
}

.restack td:empty {
    display: none;
}

@media (min-width: 720px) {
    .restack {
        display: table;
    }

    .restack thead {
        position: static;
        width: auto;
        height: auto;
        overflow: visible;
        clip-path: none;
        display: table-header-group;
    }

    .restack tbody {
        display: table-row-group;
    }

    .restack tr {
        display: table-row;
        margin: 0;
        padding: 0;
        border: 0;
        background: transparent;
    }

    .restack td {
        display: table-cell;
        padding: var(--s-3) var(--s-3) var(--s-3) 0;
        border-bottom: 1px solid var(--rule);
    }

    .restack td::before {
        content: none;
    }

    .restack td:empty {
        display: table-cell;
    }
}

/* ------------------------------------------------------------------ 12a (cont.) runway */

/* The next 24 months of deadlines as a drawing, one row per lease, at the top of Leases (UI V2 plan
 * U6): a filled diamond for the next action date, a ring for the lease expiry, a hatched dashed bar
 * for dates on hold. The shapes differ so that no colour is needed to tell them apart. It is
 * decorative (the markup is aria-hidden and the list of leases below, with their dates, is the
 * real one), so it is hidden on a phone, where each lease's card shows its key date, and shown
 * from 960px. It is a sheet of its own on the paper, between the page head and the list (layout
 * pass U3), drawn as a card is: below 960px nothing is left of it, not even an empty card.
 *
 * Every row is the same grid: the name, the Past track, 24 month columns, an end cap for dates further
 * out. A class per month (rw-m0 is the current month) picks the column, so no inline style is needed.
 * The Past track (P1 fixes plan U8) holds the leases already past due, left of the today rule. It is
 * zero wide unless a row is past due (rw-grid--past), and the grid has no column gap, so without one
 * the drawing is exactly as it was.
 *
 * Its rows and legend sit 1.5rem in from its edges, as the list's table does from 1024px, so the two
 * halves of the one sheet (below) share their left and right edges (design elevation U15).
 *
 * The end cap and the Past track are one width, wide enough for a month and year in the bold
 * figures with room on both sides: at 4rem the Later dates touched the window's end line and the
 * Past ones the today rule (UI review polish U8). */
.runway {
    --rw-label: 10.5rem;
    --rw-cap: 4.75rem;
    --rw-past: 0px;
    display: none;
    margin: 0 0 var(--section-gap);
    padding: 0 1.5rem;
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: none;
}

.rw-row {
    position: relative;
    display: grid;
    grid-template-columns: var(--rw-label) var(--rw-past) repeat(24, minmax(0, 1fr)) var(--rw-cap);
    align-items: center;
    min-height: 2.75rem;
    border-top: 1px solid var(--rule);
}

/* Everything shares one grid row, so markers in the same month overlap instead of being pushed down. */
.rw-row > * {
    grid-row: 1;
}

/* Where the window ends, a stronger line runs through every row (one pseudo-element across the 24 month
 * columns). The months are named in the head and draw no line of their own down the drawing (design
 * elevation U15): the markers carry their dates, and the today rule and that end line frame them. */
.rw-row::before {
    content: "";
    grid-column: 3 / 27;
    grid-row: 1;
    align-self: stretch;
    background: linear-gradient(90deg, transparent calc(100% - 1px), var(--control) 0);
    pointer-events: none;
}

/* The today rule: the left edge of the first month, down the whole drawing. */
.rw-row::after {
    content: "";
    grid-column: 3;
    grid-row: 1;
    align-self: stretch;
    border-left: 2px solid var(--ink);
    pointer-events: none;
}

.rw-row:not(.rw-head):hover {
    background: var(--paper);
}

.rw-head {
    min-height: 0;
    align-items: end;
    border-top: 0;
}

.rw-head + .rw-row {
    border-top-color: var(--ink);
}

/* Month headers: the month, and over it the year on the first column and on every January, in
 * sentence case, so every month's name sits on one line and every year on the line above (UI review
 * polish U8: with the year under its month, the line read "2026 Nov"). Only the major months show at
 * the narrowest width, where a column is about 30px. The runway's words are the brief's smallest
 * text, never under it (design elevation U21). */
.rw-corner,
.rw-h,
.rw-cap-h,
.rw-past-h {
    padding: var(--s-2) 0 var(--s-1);
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
    font-weight: 500;
    letter-spacing: 0;
    line-height: 1.15;
    text-transform: none;
    white-space: nowrap;
}

.rw-corner {
    grid-column: 1;
    justify-self: end;
    padding-right: var(--s-2);
    color: var(--ink);
    font-weight: 650;
}

.rw-h {
    display: none;
    flex-direction: column-reverse;
    justify-content: flex-start;
    align-self: stretch;
    padding-left: 0.35rem;
}

.rw-h-q {
    display: flex;
    border-left: 1px solid var(--control);
}

.rw-h-q .rw-h-m {
    color: var(--ink-2);
}

.rw-h-yr {
    color: var(--ink);
    font-weight: 650;
}

.rw-cap-h {
    grid-column: 27;
    justify-self: center;
}

/* With a Past column, "Today" moves into it to stay beside the today rule, under the column's own
 * head: the head takes the top line (the years'), "Today" the bottom one (the months' names). */
.rw-grid--past {
    --rw-past: var(--rw-cap);
}

.rw-grid--past .rw-corner {
    grid-column: 2;
}

.rw-past-h {
    grid-column: 2;
    justify-self: center;
    align-self: start;
}

/* A client's name fills its row's height (at least 44px), its words in the middle, so a one-line
 * name is as easy to hit as a two-line one (UI V2 plan U15). */
.rw-name {
    grid-column: 1;
    align-self: stretch;
    display: flex;
    align-items: center;
    padding: var(--s-1) var(--s-3) var(--s-1) 0;
    color: var(--ink);
    font-family: var(--font-text);
    font-size: var(--fs-compact);
    font-weight: 500;
    line-height: 1.2;
    text-decoration: none;
    text-wrap: balance;
}

.rw-name:hover {
    color: var(--navy-strong);
    text-decoration: underline;
}

/* A marker sits in one month column; its shape is centred there and its date label hangs to the
 * right, or to the left in the last three months, where the right side would leave the window. */
.rw-mk {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 1.5rem;
}

.rw-shape {
    display: block;
    flex: none;
}

.rw-next .rw-shape {
    width: 0.8rem;
    height: 0.8rem;
    background: var(--navy);
    transform: rotate(45deg);
}

.rw-expiry .rw-shape {
    width: 1rem;
    height: 1rem;
    border: 3px solid var(--ink);
    border-radius: 50%;
    background: var(--sheet);
}

/* Dates on hold are a paused span, not a deadline: a grey dashed outline (3:1 on the sheet, so the
 * shape reads) over a light hatch, lighter than the diamond and ring it sits among (design elevation
 * U15; it was the drawing's heaviest mark). */
.rw-hold .rw-shape {
    width: 100%;
    height: 1.125rem;
    margin-left: 3px;
    border: 2px dashed var(--control);
    background: repeating-linear-gradient(135deg, var(--sheet) 0 0.3rem, var(--rule-strong) 0.3rem 0.4rem);
}

.rw-lab {
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(50% + 0.85rem);
    display: flex;
    align-items: center;
    color: var(--ink-2);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
}

.rw-flip .rw-lab {
    right: calc(50% + 0.85rem);
    left: auto;
}

.rw-hold .rw-lab {
    left: calc(100% + var(--s-2));
    color: var(--navy-strong);
    font-weight: 650;
}

/* A bar longer than 20 months has no room for its words, and the legend names it. */
.rw-nolab .rw-lab {
    display: none;
}

/* Two markers of one lease in the same month or within two months (a label is about two months
 * wide, so beside each other they would collide): the row grows and they sit one above the other. */
.rw-row--pair {
    min-height: 3.75rem;
}

.rw-row--pair .rw-mk {
    align-self: start;
    margin-top: var(--s-2);
}

.rw-row--pair .rw-stack {
    align-self: end;
    margin-top: 0;
    margin-bottom: var(--s-2);
}

/* The Past column: a warning triangle, in the urgent tone, for a lease Leases marks At risk or Action
 * needed with a confirmed date already passed, and the month of the latest one (the Later cap's
 * look, mirrored). The triangle is a mask over its background, which forced colours repaint in the
 * text colour (section 15), so its shape stays. */
.rw-past-mk {
    grid-column: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    color: var(--urgent);
}

.rw-past .rw-shape {
    width: 1.125rem;
    height: 1.125rem;
    background: var(--urgent);
    -webkit-mask: var(--icon-warning) center / contain no-repeat;
    mask: var(--icon-warning) center / contain no-repeat;
}

.rw-past-lab {
    font-size: var(--fs-meta);
    font-variant-numeric: tabular-nums;
    font-weight: 650;
    line-height: 1;
    white-space: nowrap;
}

/* The end cap: an arrow for a lease with dates beyond the window, and the first of them. */
.rw-later {
    grid-column: 27;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    color: var(--muted);
}

.rw-arrow {
    display: block;
    width: 1.5rem;
    height: 0.8rem;
    background: var(--ink);
    clip-path: polygon(0 36%, 62% 36%, 62% 0, 100% 50%, 62% 100%, 62% 64%, 0 64%);
}

.rw-later-lab {
    font-size: var(--fs-meta);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
}

.rw-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-1) var(--s-6);
    padding: var(--s-2) 0;
    border-top: 1px solid var(--rule);
    color: var(--muted);
    font-size: var(--fs-small);
}

.rw-foot p {
    margin: 0;
}

.rw-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-1) var(--s-6);
}

/* The leases the drawing doesn't show: those the row cap left out, and those with no confirmed date. */
.rw-notes {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
}

.rw-key {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
}

.rw-key.rw-next .rw-shape {
    margin: 0 0.15rem;
}

.rw-key.rw-hold .rw-shape {
    width: 2rem;
    margin-left: 0;
}

.rw-key.rw-now .rw-shape {
    width: 0;
    height: 1rem;
    border-left: 2px solid var(--ink);
}

/* The markers by month and the hold bars by length. */
.rw-m0 { grid-column-start: 3; }
.rw-m1 { grid-column-start: 4; }
.rw-m2 { grid-column-start: 5; }
.rw-m3 { grid-column-start: 6; }
.rw-m4 { grid-column-start: 7; }
.rw-m5 { grid-column-start: 8; }
.rw-m6 { grid-column-start: 9; }
.rw-m7 { grid-column-start: 10; }
.rw-m8 { grid-column-start: 11; }
.rw-m9 { grid-column-start: 12; }
.rw-m10 { grid-column-start: 13; }
.rw-m11 { grid-column-start: 14; }
.rw-m12 { grid-column-start: 15; }
.rw-m13 { grid-column-start: 16; }
.rw-m14 { grid-column-start: 17; }
.rw-m15 { grid-column-start: 18; }
.rw-m16 { grid-column-start: 19; }
.rw-m17 { grid-column-start: 20; }
.rw-m18 { grid-column-start: 21; }
.rw-m19 { grid-column-start: 22; }
.rw-m20 { grid-column-start: 23; }
.rw-m21 { grid-column-start: 24; }
.rw-m22 { grid-column-start: 25; }
.rw-m23 { grid-column-start: 26; }

.rw-span-1 { grid-column-end: span 1; }
.rw-span-2 { grid-column-end: span 2; }
.rw-span-3 { grid-column-end: span 3; }
.rw-span-4 { grid-column-end: span 4; }
.rw-span-5 { grid-column-end: span 5; }
.rw-span-6 { grid-column-end: span 6; }
.rw-span-7 { grid-column-end: span 7; }
.rw-span-8 { grid-column-end: span 8; }
.rw-span-9 { grid-column-end: span 9; }
.rw-span-10 { grid-column-end: span 10; }
.rw-span-11 { grid-column-end: span 11; }
.rw-span-12 { grid-column-end: span 12; }
.rw-span-13 { grid-column-end: span 13; }
.rw-span-14 { grid-column-end: span 14; }
.rw-span-15 { grid-column-end: span 15; }
.rw-span-16 { grid-column-end: span 16; }
.rw-span-17 { grid-column-end: span 17; }
.rw-span-18 { grid-column-end: span 18; }
.rw-span-19 { grid-column-end: span 19; }
.rw-span-20 { grid-column-end: span 20; }
.rw-span-21 { grid-column-end: span 21; }
.rw-span-22 { grid-column-end: span 22; }
.rw-span-23 { grid-column-end: span 23; }
.rw-span-24 { grid-column-end: span 24; }

@media (min-width: 960px) {
    .runway {
        display: block;
    }
}

/* From 1024px, where the list is a sheet too, the runway and the list are one sheet: the list opens
 * under the runway's legend, a hairline between them (adjacent sections share a sheet, as Today's
 * groups do). */
@media (min-width: 1024px) {
    .runway:has(+ .card.lease-list) {
        margin-bottom: 0;
        border-bottom-width: 0;
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
    }
}

/* From 1280px a column is wide enough for every month to be named, and the names fit on one line;
 * the label column takes the longest client name in the book, so names stay on one line at the demo
 * window. */
@media (min-width: 1280px) {
    .runway {
        --rw-label: 16rem;
    }

    .rw-h {
        display: flex;
    }
}

/* ------------------------------------------------------------------ 12a (cont.) Leases list */

/* The list of leases under the runway (UI V2 plan U7): the status filters, a Sort form for a phone,
 * then the table. The filters are text tabs on one rule, each with its status's icon in its tone and
 * its count: the one in use in ink with the navy underline, counts quiet, as the fact list's filters
 * are. On a phone the row scrolls sideways instead of wrapping into four rows above the first lease,
 * running out to the screen's edges (design elevation U24): a tab cut by the screen's edge says the
 * row goes on, where one cut at the page's margin read as clipped. Its first tab still starts on the
 * text's edge. From 720px it wraps again, inside the margin. */
.lease-filters ul {
    display: flex;
    flex-wrap: nowrap;
    gap: 0 1.25rem;
    margin: 0 calc(-1 * var(--s-4)) var(--s-4);
    padding: 0 var(--s-4);
    list-style: none;
    overflow-x: auto;
    border-bottom: 1px solid var(--rule);
    scroll-padding-inline: var(--s-4);
    scrollbar-width: thin;
}

.lease-filters li {
    flex: none;
}

.lease-filters a {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: var(--target);
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--ink-2);
    font-size: var(--fs-small);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}

.lease-filters a:hover {
    border-color: var(--control);
    box-shadow: inset 0 -2px 0 var(--rule-strong);
    color: var(--ink);
}

/* Only the statuses that ask something of Nick (At risk, Action needed, On hold) colour their icon;
 * the rest are the quiet grey, as Today's group heads are (design elevation U15, after Matt's quieter
 * Today). The filter in use keeps its tone (below). */
.lease-filters .icon {
    color: var(--muted);
}

.lease-filters :is(.tone-urgent, .tone-action) .icon {
    color: var(--tone);
}

.lease-filters a[aria-current="true"],
.lease-filters a[aria-current="true"]:hover {
    border-color: transparent;
    background: transparent;
    box-shadow: inset 0 -2px 0 var(--navy);
    color: var(--ink);
    font-weight: 600;
}

.lease-filters a[aria-current="true"] .icon {
    color: var(--tone, var(--neutral));
}

@media (min-width: 720px) {
    .lease-filters ul {
        flex-wrap: wrap;
        margin-inline: 0;
        padding-inline: 0;
        overflow-x: visible;
        scrollbar-width: auto;
    }

    .lease-filters li {
        flex: 0 1 auto;
    }
}

/* The phone's way to sort (the table's headings do it from 1024px): a menu, a tick to reverse it
 * and a button, sent as a plain GET form. */
.lease-sort {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-3);
    margin: 0 0 var(--s-4);
}

.lease-sort label {
    font-weight: 600;
    font-size: var(--fs-compact);
    color: var(--ink-2);
}

.lease-sort select {
    flex: 1 1 11rem;
    min-width: 0;
    max-width: 100%;
}

.lease-sort .lease-sort-reverse {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--target);
    font-weight: 400;
    color: var(--ink);
}

.lease-sort-reverse input {
    margin: 0;
}

/* Below 1024px each lease is a card of its own on the paper, so the list's card steps back: no
 * border, padding, ground or shadow, and no box inside a box (layout pass U5). The filters, the Sort
 * form, the "couldn't work out" line and an empty filter's line sit on the paper with the cards; the
 * 1024px rule puts the card back around the table. */
.card.lease-list {
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
}

/* Below 1024px the leases are rows of one white sheet (the table's body), a hairline between them, as
 * Today's groups are: each the client and its practice type, its status chip and its next date
 * (named over it), the next action and where it is. The headings stay for assistive technology, out
 * of sight; every cell's aria-colindex keeps each cell read with its own heading. */
.lease-table,
.lease-table tbody,
.lease-table td {
    display: block;
}

.lease-table tbody {
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    background: var(--sheet);
}

.lease-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}

.lease-table tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "client"
        "place"
        "status"
        "key"
        "next";
    align-items: start;
    gap: var(--s-2) var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-top: 1px solid var(--rule);
}

.lease-table tr:first-child {
    border-top: 0;
}

/* A lease with no address on record has no last line, so no gap is left for one. */
.lease-table tr:has(> .lease-place.is-empty) {
    grid-template-areas:
        "client"
        "status"
        "key"
        "next";
}

/* A row keeps the sheet's white under a pointer or a tap: the table's row tint (section 9) would turn
 * it into the paper. The 1024px rule gives the table's rows their tint back. */
.lease-table tbody tr:hover {
    background: var(--sheet);
}

.lease-table td {
    min-width: 0;
    padding: 0;
    border: 0;
}

.lease-client {
    grid-area: client;
}

.lease-status {
    grid-area: status;
}

.lease-table .lease-status {
    padding-top: var(--s-1);
}

/* The next date: what it is, quiet in the text face, over the date in the serif's tabular figures,
 * heavier, a sure date and an unsure one alike (design elevation U15: dates in the serif, the unsure
 * one told by its muted ink and its icon); under a notice date, the lease's end in a quiet line. */
.lease-date {
    grid-area: key;
    font-family: var(--font-serif);
    font-size: 1rem;
    font-variant-numeric: lining-nums tabular-nums;
}

.lease-date-what,
.lease-date-day,
.lease-date-ends {
    display: block;
}

.lease-date-what {
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
}

.lease-date-day {
    font-weight: 600;
}

.lease-date-ends {
    color: var(--muted);
    font-size: var(--fs-small);
}

.lease-next {
    grid-area: next;
}

.lease-place {
    grid-area: place;
    color: var(--muted);
    font-size: var(--fs-small);
}

.lease-table .lease-place.is-empty {
    display: none;
}

/* The location under the client's name (from 1024px, below) is as quiet as its own column. */
.lease-client-place {
    display: none;
    color: var(--muted);
}

/* The client's name, in ink, keeps its 44px target in padding that reaches into the row, with its
 * words on the row's first line: the negative margin takes the padding back, as Today's client link
 * does, so a name on two lines pushes nothing and the practice type sits right under it. */
.lease-name {
    display: inline-flex;
    align-items: center;
    min-height: 0;
    padding-block: calc((var(--target) - 1lh) / 2);
    margin-block: calc((1lh - var(--target)) / 2);
    color: var(--ink);
    font-weight: 600;
    text-decoration: none;
}

.lease-name:hover,
.lease-name:focus-visible {
    color: var(--navy-strong);
    text-decoration: underline;
}

.lease-type {
    display: flex;
    align-items: center;
    gap: var(--s-1);
    color: var(--muted);
    font-size: var(--fs-meta);
}

/* A date never breaks apart, in a date cell or in a next action's words: "Next reminder" wraps
 * before "Fri 2 Oct 2026", never before its year. */
.lease-date time,
.lease-next time {
    white-space: nowrap;
}

/* From 390px the chip and the next date share a line: the widest chip (about 151px) and the widest
 * date (about 125px) fit side by side in the row there. The chip's column is the widest chip's width,
 * so every lease's next date starts at one place down the list. */
@media (min-width: 390px) {
    .lease-table tr {
        grid-template-columns: 9.5rem minmax(0, 1fr);
        grid-template-areas:
            "client client"
            "place place"
            "status key"
            "next next";
    }

    .lease-table tr:has(> .lease-place.is-empty) {
        grid-template-areas:
            "client client"
            "status key"
            "next next";
    }
}

/* A date that isn't sure is quiet and keeps to one line: the date a reading proposed, a dash when
 * there is none, or a conflict's count ("2 dates"), in the muted ink, then the small question icon
 * in the same ink. Its words ("not confirmed", "in conflict" with each side's date) are out of sight
 * for a screen reader, and the wrapper's title shows them under a pointer (the help cursor says one
 * is there). A sure date stays ink, so the icon and the ink both mark the unsure ones, never colour
 * alone. */
.date-unsure {
    color: var(--muted);
    white-space: nowrap;
    cursor: help;
}

.date-unsure .icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.15em;
}

.date-none {
    color: var(--muted);
}

.lease-next .next-action-form,
.lease-next .btn {
    max-width: 100%;
}

/* A next action to press is a white button on every row (polish pass: drawn as bold words on the
 * calmer rows, "Review facts" read as neither a button nor a link). */
.lease-next .btn {
    justify-content: flex-start;
    padding: 0 0.75rem;
    border-color: var(--control);
    background: var(--sheet);
    color: var(--ink);
    font-size: var(--fs-compact);
    font-weight: 600;
}

.lease-next .btn:hover {
    border-color: var(--ink);
    background: var(--paper);
}

/* With nothing to press, a next action says what happens next in small muted words ("Next reminder
 * Fri 2 Oct 2026", "Back on ...", "Not known"): one quiet shape beside the column's buttons. */
.lease-next-words {
    color: var(--muted);
    font-size: var(--fs-small);
}

/* A next action with no button but a screen (a snoozed reminder's, Matt's look) is a link, still a
 * 44px target, kept in the gaps around it as the client's name does, so its words sit as close to the
 * lines above and below as a white button's edge. */
.lease-next > a:not(.btn) {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    margin-block: calc((1lh - var(--target)) / 2);
}

/* That link reads as the same small muted words. It keeps the links' light underline at rest, so
 * something besides colour says it is one, and turns ink with a full underline under a pointer or the
 * keyboard, as Today's quiet links do. */
a.lease-next-words:hover,
a.lease-next-words:focus-visible {
    color: var(--ink);
    text-decoration-thickness: 2px;
    text-decoration-color: currentcolor;
}

/* The sortable headings are links from 1024px, in the headings' own size and colour (layout pass
 * U5), with an underline under a pointer or the keyboard. Each is an inline link, so its words wrap
 * and sit on the lines a plain heading's name sits on, and whatever follows them stays beside the
 * last word. Its 44px target is padding around its words' 1.3em box (the text face's ascent and
 * descent, as its fallback's overrides say), which takes no room and stays inside the cell's own
 * padding. The active one carries an arrow (the chevron, turned up for ascending) beside its name,
 * and aria-sort on its cell says the same. Below that the headings are out of sight, so each is its
 * plain name: a hidden link would still take the keyboard's focus. */
.sort-link {
    display: none;
    padding-block: calc((var(--target) - 1.3em) / 2);
    color: inherit;
    text-decoration: none;
}

.sort-link:hover,
.sort-link:focus-visible {
    color: var(--navy-strong);
    text-decoration-line: underline;
    text-decoration-thickness: 2px;
}

/* The arrow, and the faint glyph the other sortable headings show at rest (so a touch screen, with no
 * pointer to hover, still shows which headings sort), follow the name's last word, centred on its
 * capitals as an icon is. Heading text balances its lines, so neither is left alone on one. */
.sort-arrow,
.sort-link::after {
    display: inline-block;
    width: 0.95em;
    height: 0.95em;
    margin-left: var(--s-1);
    vertical-align: -0.15em;
}

.sort-arrow {
    background: currentcolor;
    -webkit-mask: var(--icon-chevron) center / contain no-repeat;
    mask: var(--icon-chevron) center / contain no-repeat;
}

.sort-arrow--up {
    transform: rotate(180deg);
}

.sort-link::after {
    content: "";
    background: var(--control);
    -webkit-mask: var(--icon-sort) center / contain no-repeat;
    mask: var(--icon-sort) center / contain no-repeat;
}

th[aria-sort] .sort-link::after {
    content: none;
}

.lease-empty {
    margin-top: 0;
}

/* An empty filter's way back, "Show all leases", is a 44px target, as search's no-match line's is
 * (section 12l). */
.lease-empty > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

@media (min-width: 1024px) {
    .lease-sort,
    .sort-name {
        display: none;
    }

    .sort-link {
        display: inline;
    }

    /* The list is a card again around the table, as .card draws one from 720px: a flat one, its
     * padding tight around the filters and the table. Under the runway it is the same sheet's lower
     * part (the runway section joins them). */
    .card.lease-list {
        padding: 0.5rem 1.5rem 0.25rem;
        border: 1px solid var(--rule);
        background: var(--sheet);
        box-shadow: none;
    }

    /* The page head's rule (11b) puts a section gap above every sheet after the head; this one has
     * the same weight and comes later, so the list sits flush under the runway. */
    .runway + .card.lease-list {
        margin-top: 0;
        border-top-left-radius: 0;
        border-top-right-radius: 0;
    }

    .lease-filters ul {
        margin-bottom: 0.5rem;
    }

    .lease-table {
        display: table;
    }

    .lease-table thead {
        position: static;
        width: auto;
        height: auto;
        overflow: visible;
        clip-path: none;
        display: table-header-group;
    }

    .lease-table tbody {
        display: table-row-group;
        border: 0;
        border-radius: 0;
        background: none;
    }

    .lease-table tr {
        display: table-row;
        margin: 0;
        padding: 0;
        border: 0 none var(--rule);
        background: transparent;
    }

    .lease-table tbody tr:hover {
        background: var(--paper);
    }

    /* Every cell starts at the top, so each row's first lines share one line across the table
     * (layout pass U5); what is 44px tall reaches into the cell's padding to stay on it (below). */
    .lease-table td {
        display: table-cell;
        padding: 0.75rem 1rem 0.75rem 0;
        border-bottom: 1px solid var(--rule);
        vertical-align: top;
    }

    /* The last column ends at the card's padding, as the first starts at it: a right padding of its
     * own would push the table past the card at 1024 and 1100px. */
    .lease-table :is(th, td):last-child {
        padding-right: 0;
    }

    /* The headings: one size and colour for the plain names and the sort links (a table's quiet
     * column names, section 9), one bottom line for their words, and no word left alone on a line
     * should a heading ever wrap (the names are short enough for one: "Next action", "Next date"). */
    .lease-table th {
        vertical-align: bottom;
        text-wrap: balance;
    }

    .lease-table .lease-status {
        padding-top: 0.75rem;
    }

    .lease-next .btn {
        white-space: nowrap;
    }

    .lease-type {
        margin-top: 0.125rem;
    }

    /* The next action's 44px box (a button, a one-button form, Matt's look link or a link to a
     * screen) reaches into the cell's padding, as a statement's cell link does (section 11), so its
     * words sit on the row's first line, where words with nothing to press already sit. Each keeps
     * the width its words give it. */
    .lease-next > :is(.btn, .next-action-form, a) {
        margin-block: calc((1lh - var(--target)) / 2);
    }

    /* Up to 1280px the location sits under the client's name, and its column is left out. */
    .lease-client-place {
        display: block;
        margin-top: var(--s-1);
        font-size: var(--fs-small);
    }

    .lease-table th[aria-colindex="2"],
    .lease-table .lease-place,
    .lease-table .lease-place.is-empty {
        display: none;
    }
}

/* From 1100px the client's name keeps a column wide enough for two lines at most; below that the
 * chips, buttons and dates take every pixel the table has. */
@media (min-width: 1100px) {
    .lease-table .lease-client {
        min-width: 11.5rem;
    }
}

@media (min-width: 1280px) {
    .lease-client-place {
        display: none;
    }

    .lease-table th[aria-colindex="2"],
    .lease-table .lease-place,
    .lease-table .lease-place.is-empty {
        display: table-cell;
    }

    .lease-table .lease-client {
        min-width: 13rem;
    }
}

/* From 1440px the page is at its full width whatever the scroll bar (layout pass U5): a client's name
 * keeps to one line (the demo book's longest is about 235px, inside 15rem less the cell's padding)
 * and the location gets room for two, while the widest next action Lease Watch sends ("Decide the
 * assignment") still leaves the whole table inside its card. */
@media (min-width: 1440px) {
    .lease-table .lease-client {
        min-width: 15rem;
    }

    .lease-table .lease-place {
        min-width: 11rem;
    }
}

/* ------------------------------------------------------------------ 12b. lease page */

/* The lease page (UI V2 plan U8), read top to bottom on a phone: the head, an assignment's callout,
 * Next up, the timeline, the summary with Amendments, Assignments and the terms, then the side
 * column (Contact, Emails, Documents), then Stop tracking. From 1024px the side column sits beside
 * the summary under the full-width top, and from 1280px beside the top as well. The grid's areas
 * follow the source: nothing is reordered.
 * The lead line under a page's heading (.lede) is section 11b's. */

/* The head: the crumbs, the client with its practice type and status chip, the premises, then the
 * main button (beside the words from 720px, under them on a phone), with the print-only line and the
 * page's status line under them. It stands on the paper, not in a card (layout pass U3), and ends
 * one section gap above the callout or Next up. */
.lease-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.75rem 1.5rem;
    align-items: center;
    margin: 0 0 var(--section-gap);
}

.lease-head .page-head-row {
    align-items: center;
}

.lease-head .page-head p:last-child {
    margin-bottom: 0;
}

.lease-head > .print-only,
.lease-head > .alert {
    grid-column: 1 / -1;
    margin: 0;
}

.lease-head-action:empty {
    display: none;
}

/* A jump to Next up (a lease waiting on "still active?": Nick's "Is it still active?", Matt's "See
 * the question") waits for 720px: below it Next up follows the head directly and asks the same
 * question with its answers (UI review 2026-10-29 V-23). */
.lease-head-action:has(> a[href="#next-up"]) {
    display: none;
}

.lease-head-action .next-action-form,
.lease-head-action .next-action {
    width: 100%;
}

/* Matt's way to the same work: its words as a link to look, with the eye. */
.lease-look {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--target);
    font-weight: 650;
}

@media (min-width: 720px) {
    .lease-head {
        grid-template-columns: minmax(0, 1fr) auto;
    }

    .lease-head-action:has(> a[href="#next-up"]) {
        display: block;
    }

    .lease-head-action .next-action-form,
    .lease-head-action .next-action {
        width: auto;
        min-width: 11rem;
    }
}

.lease-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "top" "main" "side";
    column-gap: var(--section-gap);
}

.lease-top {
    grid-area: top;
    min-width: 0;
}

.lease-main {
    grid-area: main;
    min-width: 0;
}

.lease-side {
    grid-area: side;
    min-width: 0;
}

/* The abstract is one sheet: the summary, Amendments, Assignments and the terms follow each other
 * divided by full-width rules, not four cards. The side column likewise: Contact, Emails, Documents,
 * then the lease's number as the sheet's last row (below). */
.lease-main > .card,
.lease-side > .card {
    margin: 0;
    border-top-width: 0;
    border-radius: 0;
}

.lease-main > .card:first-child,
.lease-side > .card:first-child {
    border-top-width: 1px;
    border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
}

.lease-main > .card:last-child {
    margin-bottom: var(--section-gap);
    border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
}

@media (min-width: 1024px) {
    .lease-grid {
        grid-template-columns: minmax(0, 1fr) 21rem;
        grid-template-areas: "top top" "main side";
        align-items: start;
    }
}

/* The top (Next up and the timeline, the direction's hero) keeps the page's full width at every desk
 * width, so the timeline has room for its line to scale; the side column starts under it, beside
 * the summary (design elevation U16; layout pass U6 had started it beside the top from 1280px). */

/* An assignment waiting for Nick's decision: a sheet on the action tone's tint, above Next up. */
.lease-callout.is-open {
    margin: 0 0 var(--section-gap);
    padding: var(--s-4);
    background: var(--action-bg);
    border: 1px solid transparent;
    border-radius: var(--radius-sheet);
    box-shadow: var(--shadow);
}

.lease-callout.is-open p {
    margin: 0 0 var(--s-2);
}

.lease-callout.is-open p:last-child {
    margin-bottom: 0;
}

/* Next up: Today's date tile beside what comes next, on a plain sheet: what comes next in the serif,
 * its label a quiet word, in its tone when the lease needs Nick (urgent or action), as Today's group
 * heads are since Matt's quieter Today (the status chip above says the same in a word). */
.next-up {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-1) var(--s-4);
    align-items: start;
    background: var(--sheet);
}

/* The hero (design elevation U16): Next up is the header line of the timeline's sheet, a hairline
 * under it, as adjacent sections share a sheet on Today and Leases. A provisional timeline (hatched,
 * dashed) stays a sheet of its own; the "still active?" card's one line joins the sheet, since Next up
 * asks the question (UI review polish U1). */
.next-up:has(+ .timeline:not(.timeline--hold, .timeline--needs_entry)) {
    margin-bottom: 0;
    border-bottom-width: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.next-up + .timeline:not(.timeline--hold, .timeline--needs_entry) {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* Below 360px the tile sits above the words, which then take the sheet's width (UI review 2026-10-29
 * V-28: beside it they had about 190px at 320, so a Set aside line wrapped to four). */
@media (min-width: 360px) {
    .next-up:has(> .next-up-tile) {
        grid-template-columns: auto minmax(0, 1fr);
    }
}

.next-up-tile {
    padding-top: 0.15rem;
}

.card .next-up-title {
    margin: 0;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
}

.next-up:is(.tone-urgent, .tone-action) .next-up-title {
    color: var(--tone);
}

.next-up-say {
    margin: 0.125rem 0 0;
    font-family: var(--font-serif);
    font-size: var(--fs-lead);
    font-weight: 600;
}

.next-up-detail {
    margin: var(--s-1) 0 0;
    color: var(--muted);
}

/* Set aside (corrections plan U7): what can be put back, under Next up's words past a hairline, its
 * heading the quiet label Next up's is; each line's reverse is a quiet button (.todo-link). */
.set-aside {
    margin-top: var(--s-3);
    padding-top: var(--s-3);
    border-top: 1px solid var(--rule);
}

.set-aside h3 {
    margin: 0;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
}

/* The lease summary (lease abstract U1; grouped in the polish pass, Matt's direction A): a head (the
 * card's title and, while facts wait, one quiet link to review them), then its groups, each a section of
 * the sheet under a small heading in the text face, a hairline between groups running the sheet's width
 * as Today's groups' do. Inside a group the facts are a ledger: each fact's quiet label in a column of
 * its own, the same width in every group so the values line up down the card, the value beside it, its
 * source tag a quiet column at the right. A fact waiting or in conflict shows the fact list's chip and a
 * link to decide it, so nothing rests on colour, on screen or on paper. The facts a group has not
 * recorded share one muted line that names them ("Not recorded: attention line, copy-to"). On paper the
 * card stays whole by the print block's .card rule: it holds none of the lists that let a card break.
 * The label column is the main column's (UI review polish U4), so Amendments, Assignments and their
 * empty lines start their values on the summary's edge. */
.lease-main {
    --summary-label: 7.5rem;
}

@media (min-width: 560px) {
    .lease-main {
        --summary-label: 10rem;
    }
}

.summary-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0 var(--s-4);
}

.card .summary-head > h2 {
    margin-bottom: 0;
}

/* Beside the title: "6 facts to review" while facts wait, in the evidence ink the To review chips wear,
 * then "See every fact" (corrections plan U1), each a 44px target. Under the pointer or the keyboard the
 * first takes a link's full navy, as a plain link does. */
.summary-links {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 var(--s-4);
}

.summary-links > p {
    margin: 0;
    font-size: var(--fs-small);
}

.summary-todo {
    font-weight: 700;
}

.summary-links a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.summary-todo > a {
    color: var(--evidence);
}

.summary-todo > a:hover,
.summary-todo > a:focus-visible {
    color: var(--navy-strong);
}

.summary-term {
    margin: var(--s-2) 0 0;
    font-weight: 600;
}

.summary-group {
    margin: var(--s-3) calc(-1 * var(--card-pad)) 0;
    padding: var(--s-4) var(--card-pad) 0;
    border-top: 1px solid var(--rule);
}

/* A group's heading is a label for what follows, not a title over it: the text face, a step over the
 * fact labels and bold, in the second ink. */
.summary-group h3 {
    margin: 0 0 var(--s-2);
    color: var(--ink-2);
    font-family: var(--font-text);
    font-size: var(--fs-compact);
    font-weight: 700;
    letter-spacing: 0;
}

.summary-group-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0 var(--s-4);
}

.summary-group-head > h3 {
    margin-bottom: 0;
}

/* The ledger: on a phone the label column holds 7.5rem of words, as every facts list's does (12c), a
 * long label wrapping inside it; from 560px, 10rem; below 360px the label sits above its value (the
 * ledgers' shared rule, after Amendments' below). A rent schedule takes the group's width under its
 * label (a table can't sit in the value column). The list is the container the source tags measure
 * (below): a value can't measure itself. */
.summary-facts {
    display: grid;
    align-items: baseline;
    gap: var(--s-2) var(--s-4);
    margin: 0;
    container: summary-facts / inline-size;
}

.summary-facts > dt {
    color: var(--muted);
    font-size: var(--fs-small);
}

.summary-facts > dd {
    min-width: 0;
    margin: 0;
    overflow-wrap: anywhere;
}

.summary-facts > .summary-wide {
    grid-column: 1 / -1;
}

/* Every accepted value is in the serif's regular figures, as the fact list's values are (the same
 * facts), so a date is no louder than a rent or an address and no figure takes the text face's slashed
 * zero (design elevation U16); the words around a value ("in N days", what isn't recorded) stay in the
 * text face. */
.summary-value {
    color: var(--ink);
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-variant-numeric: lining-nums tabular-nums;
}

/* A value, its chip and its links share a line while it has room, each wrapping whole. */
.summary-main {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 var(--s-3);
}

/* A fact's link to decide it ("Review", "Decide") or to the timeline: small, after its chip, and a
 * 44px target (UI V2 plan U15). A worked-out expiration's "Check the expiration date" is one too, on a
 * line of its own after its note (contradictions plan U7). */
.summary-act {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-size: var(--fs-small);
}

/* A missing fact's reason ("This lease is no longer tracked ..."), in italics as a state. */
.summary-state {
    color: var(--ink-2);
    font-size: var(--fs-small);
    font-style: italic;
}

/* A notice deadline waiting on "still active?": whose answer, upright, as a 44px link to Next up's
 * question (UI review 2026-10-29 V-23). Not a .summary-act, so paper keeps the words. */
.summary-wait {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-size: var(--fs-small);
}

/* What changed under a value since it was worked out (contradictions plan U7): a line of its own after
 * the tag and Change, in the words' face, as a missing fact's reason is but upright. */
.summary-note {
    display: block;
    flex-basis: 100%;
    color: var(--ink-2);
    font-size: var(--fs-small);
}

/* What a group has not recorded: one line naming its facts. Muted, upright and a step under the values
 * (design elevation U24: a lone "Not recorded" read as weighty as a value): the words say it, so no
 * second cue is needed. */
.summary-missing {
    margin: var(--s-2) 0 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

.summary-group h3 + .summary-missing {
    margin-top: 0;
}

/* The source tag after a value: small muted words, no box, free to wrap (a byline says who and when).
 * Under the value while its column is narrow; once the value column has room for both (26rem: the
 * list less the label column and the gap), at the right end of the value's first line, right-aligned,
 * so the tags read as one quiet column down the card. A rent schedule's tag sits under its table, at the same right edge. A byline is
 * stored in a sentence's lower case; the tag starts it with a capital. */
.summary-fact .fact-source {
    display: block;
    padding: 0;
    border: 0;
    border-radius: 0;
    color: var(--muted);
    font-size: var(--fs-meta);
    white-space: normal;
}

.summary-fact .fact-source::first-letter {
    text-transform: uppercase;
}

/* A value's Change (corrections plan U1) closes its line, after the source tag: small words with the
 * links' light underline, opening the fact on the fact list. While the list is narrow the value keeps
 * its own line and the tag shares the next with Change: an inline block, so a byline still takes its
 * capital. Once the list has room, the tag and Change end the value's line, and under a rent schedule
 * they sit at the table's right edge. A value with no tag (the practice) starts Change at its line's
 * start. In Amendments, Change stands in a value's status line as Review does, a 44px target there.
 * On paper it goes. */
.change-link {
    font-size: var(--fs-meta);
}

.fact-source + .change-link {
    margin-left: var(--s-2);
}

.summary-fact > .fact-source:has(+ .change-link) {
    display: inline-block;
}

/* Under a touch pointer Change is a 44px target (UI review 2026-10-29 V-48): padding above and below
 * its words, in a box of its own, so its line grows to the target. Nothing takes the padding back: a
 * row a step below may open with a 44px "Review", and two rows' links must never overlap. In
 * Amendments the padding fills the 44px line Change already has. A mouse keeps the small words,
 * whose spacing passes WCAG 2.5.8. */
@media (pointer: coarse) {
    .change-link {
        display: inline-block;
        padding-block: calc((var(--target) - 1lh) / 2);
    }
}

@container summary-facts (min-width: 37rem) {
    .summary-fact:has(> .summary-main) {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        column-gap: var(--s-4);
    }

    .summary-main {
        flex: 1 1 0;
        min-width: 0;
    }

    .summary-fact .fact-source {
        max-width: 12rem;
        margin-left: auto;
        text-align: right;
    }

    .summary-fact:has(> .rent-schedule) {
        text-align: right;
    }

    .summary-fact > .rent-schedule {
        text-align: start;
    }
}

/* The card's one source line (finishing pass U3), when every value came from the spreadsheet: a section
 * of its own under the groups, its rule running the sheet's width as theirs do, so it reads as the whole
 * card's source and not the last group's. Muted and small (.muted.small). */
.summary-source {
    margin: var(--s-4) calc(-1 * var(--card-pad)) 0;
    padding: var(--s-3) var(--card-pad) 0;
    border-top: 1px solid var(--rule);
}

/* The decide pages' ledger (assignment choose and preview, finishing pass U7), drawn as the lease
 * summary's rows once were: its rules run the sheet's width, as Today's rows' do (design elevation U24):
 * the list reaches into the card's padding and each row takes it back, so the words keep their places.
 * The label and the value each draw their half of the row's rule, so no gap between the columns may
 * part them: the label's own right padding is the space between the two, and its column is that much
 * wider to keep the label's room. On a phone the label column holds 7.5rem of words, as Email sent's
 * does (12c), a long label wrapping inside it (finishing pass U12, U24-21); from 560px, 8rem to 13rem.
 * This list's gap and alignment out-rank the shared list's (11). */
.facts.summary-list {
    grid-template-columns: calc(8.5rem + var(--card-pad)) minmax(0, 1fr);
    align-items: normal;
    gap: 0;
    margin: var(--s-3) calc(-1 * var(--card-pad)) 0;
}

.summary-list > dt,
.summary-list > dd {
    padding: var(--s-3) var(--card-pad) var(--s-3) 0;
    border-top: 1px solid var(--rule);
}

.summary-list > dt {
    margin: 0;
    padding-right: var(--s-4);
    padding-left: var(--card-pad);
    color: var(--muted);
    font-size: var(--fs-small);
}

.summary-list > dt:first-of-type,
.summary-list > dt:first-of-type + dd {
    border-top: 0;
}

@media (min-width: 560px) {
    .facts.summary-list {
        grid-template-columns: minmax(calc(9rem + var(--card-pad)), calc(14rem + var(--card-pad))) minmax(0, 1fr);
    }
}

/* The landlord contacts Nick keeps by hand (lease abstract U6), the summary's last group: each a line of
 * its ledger, its role in the label column and, beside it, who, then phone and email, then the note.
 * The note about option notices sits under them, or "No landlord contacts", muted and small as the
 * summary's "Not recorded" lines are. Nick's Edit and Remove sit quiet under each contact, Remove's
 * question opening on a line of its own (the To do list's .todo-confirm slip), and "Add a contact"
 * beside the group's heading; each is a 44px target (UI V2 plan U19). On paper they go. */
.landlord-list {
    display: grid;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.landlord-contact {
    display: grid;
    align-items: baseline;
    gap: 0 var(--s-4);
}

.landlord-role {
    color: var(--muted);
    font-size: var(--fs-small);
}

.landlord-body {
    min-width: 0;
    overflow-wrap: anywhere;
}

.landlord-who,
.landlord-reach,
.landlord-note {
    display: block;
}

.landlord-note,
.landlord-notice,
.landlord-empty {
    color: var(--muted);
    font-size: var(--fs-small);
}

.landlord-notice {
    margin: var(--s-3) 0 0;
}

.landlord-empty {
    margin: 0;
}

.landlord-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--s-2);
}

.landlord-actions > a,
.landlord-add {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.landlord-actions > a {
    padding: 0 var(--s-1);
    color: var(--ink-2);
    font-size: var(--fs-meta);
}

.landlord-add {
    font-size: var(--fs-small);
}

.landlord-actions > a:hover,
.landlord-actions > a:focus-visible {
    color: var(--ink);
}

/* Remove reads as Edit's neighbour, not as a fold: its question opening under it says enough. */
.landlord-actions .todo-confirm > summary::before {
    display: none;
}

/* The Amendments section (lease abstract U7), after the summary: one block per amendment, a rule above
 * each, its title and date, then its values on the facts grid. Each value is followed by its status as
 * a word on a chip (a rent schedule's chip sits above its table), so nothing rests on colour. The
 * earlier amendments fold into a disclosure above the latest. A value's term and the "being read" line
 * are quiet, a failed reading is not, and "nothing was read" is in italics as a state. On paper the
 * card may break between amendments, each staying whole (print block). A lease with none says "No
 * amendments", as the Assignments section says "No assignments". */
.amendment {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.amendment-title {
    margin: 0;
    font-size: var(--fs-h3);
}

.amendment-date {
    margin: var(--s-1) 0 var(--s-3);
    color: var(--muted);
}

.amendment-status {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-1) var(--s-2);
}

/* The document's name heading an amendment and a value's Review stand on lines of their own: 44px
 * targets (UI V2 plan U15). */
.amendment-title > a,
.amendment-status > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* Beside a value with a Review, the value's name drops to its words, as in the summary above. */
@media (min-width: 560px) {
    .amendment-values > dt:has(+ dd > .amendment-status > a) {
        padding-top: calc((var(--target) - 1lh) / 2);
    }
}

.amendment-figure {
    margin-right: var(--s-2);
}

/* Amendments' and Assignments' values sit on the summary's ledger (UI review polish U4): its label
 * column and gap, its label size, its serif for the values, so the sheet has one value edge and one
 * face for what a document states. */
.amendment-values,
.assignment-values {
    gap: var(--s-2) var(--s-4);
}

/* Below 360px each ledger's label sits above its value, so a value takes the sheet's width (UI review
 * 2026-10-29 V-28: beside a 7.5rem label an address broke into five lines at 320); a label opens its
 * pair, a step under the value before it. From 360px the label column is back. */
.summary-facts,
.landlord-contact,
.amendment-values,
.assignment-values {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0;
}

:is(.summary-facts, .amendment-values, .assignment-values) > dd + dt {
    margin-top: var(--s-2);
}

@media (min-width: 360px) {
    .summary-facts,
    .landlord-contact,
    .amendment-values,
    .assignment-values {
        grid-template-columns: var(--summary-label) minmax(0, 1fr);
        row-gap: var(--s-2);
    }

    :is(.summary-facts, .amendment-values, .assignment-values) > dd + dt {
        margin-top: 0;
    }
}

.amendment-values > dt,
.assignment-values > dt {
    font-size: var(--fs-small);
}

.amendment-figure,
.assignment-figure {
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-variant-numeric: lining-nums tabular-nums;
}

.amendment-term,
.amendment-note {
    color: var(--muted);
}

.amendment-note {
    margin: 0 0 var(--s-2);
}

.amendment-failed {
    color: var(--ink);
}

.amendment-empty {
    font-style: italic;
}

.amendments-earlier {
    margin-top: var(--s-2);
}

/* The Assignments section (lease abstract U8), after Amendments: one block per assignment document, a
 * rule above each, its title, who uploaded it, then where it stands in one sentence (a "holds emails"
 * chip first while it holds the lease's emails), the values it states on the facts grid with the AI's
 * quote and its check as a word on a chip, and Nick's actions. The whole card is swapped by a poll
 * while a document is read, so nothing here animates. */
.assignment {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.assignment-title {
    margin: 0;
    font-size: var(--fs-h3);
    overflow-wrap: anywhere;
}

/* The document's name heading an assignment is a 44px target, as an amendment's is (UI V2 plan U15). */
.assignment-title > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.assignment-uploaded,
.assignment-decided,
.assignment-earlier,
.assignment-note,
.assignment-options {
    margin: var(--s-1) 0 0;
    color: var(--muted);
}

/* A confirmed one's link in the chain (U9): seller to buyer, in the ink of the title. */
.assignment-chain {
    margin: var(--s-2) 0 0;
    overflow-wrap: anywhere;
}

.assignment-status {
    margin: var(--s-2) 0 var(--s-3);
}

/* Every value Verified (UI review polish U2): one quiet line over the list, after the chip, and the
 * rows draw none. */
.assignment-verified {
    margin: 0 0 var(--s-2);
    color: var(--muted);
}

/* On the lease page each value's quote folds under a quiet line (polish pass), as the technical
 * details do: small and muted, so eight of them read as notes, not as eight more links. */
.assignments .decide-quote > summary {
    color: var(--muted);
    font-size: var(--fs-small);
}

.assignment-quote,
.assignment-check {
    display: block;
    margin-top: var(--s-1);
    color: var(--muted);
    overflow-wrap: anywhere;
}

.assignment-figure {
    margin-right: var(--s-2);
    overflow-wrap: anywhere;
}

.assignment-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2) var(--s-3);
    margin-top: var(--s-3);
}

.assignment-actions .field-help,
.assignment-reject,
.assignment-change,
.assignment-void,
.assignment-void-why {
    flex-basis: 100%;
}

/* A section with nothing in it is its heading and its empty line ("No amendments", "No emails yet").
 * An empty Amendments or Assignments is one ledger line under the summary, drawn as the summary's
 * rows are (design elevation U16): its name in the rows' label face, in their label column, and "No
 * amendments" in the muted ink an empty row's words take. */
.lease-main > .card > h2 + .empty:last-child,
.lease-side > .card > h2 + .empty {
    padding: 0;
}

/* No gap between the name and its words: the name's width is the summary's label column and its gap,
 * so the words start on the summary's value column at every width. */
.card:is(.amendments, .assignments):has(> h2 + p.empty:last-child) {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0;
    padding-top: 1rem;
    padding-bottom: 1rem;
}

.card:is(.amendments, .assignments) > h2:has(+ p.empty:last-child) {
    flex: 0 0 auto;
    min-width: calc(var(--summary-label) + var(--s-4));
    margin: 0;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 600;
}

.card:is(.amendments, .assignments) > h2 + p.empty:last-child {
    padding: 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

@media (min-width: 560px) {
    .card:is(.amendments, .assignments) > h2:has(+ p.empty:last-child) {
        font-weight: 400;
    }
}

/* Terms and outcomes: a line per term (its number, where it stands, its expiration with the byline of
 * one Nick entered, and its outcomes), a rule between them; the link moves to the line's end when the
 * line has room. The number and the status share one box, so every line's expiration starts on one
 * edge (UI review 2026-10-29 V-29): on a phone the box is a line of its own, the date under it at the
 * left edge; from 560px a column as wide as the summary's labels, the date on the values' edge. */
.term-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.term-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-1) var(--s-4);
    padding: var(--s-2) 0;
    border-top: 1px solid var(--rule);
}

.term-line:first-child {
    border-top: 0;
}

.term-what {
    display: flex;
    flex: 1 0 100%;
    flex-wrap: wrap;
    gap: 0 var(--s-2);
}

.term-name {
    font-weight: 600;
}

.term-status {
    color: var(--muted);
}

.term-expires {
    font-variant-numeric: tabular-nums;
}

/* The term's expiration is set as the summary's dates are: the serif's regular figures. */
.term-expires time {
    font-family: var(--font-serif);
    font-variant-numeric: lining-nums tabular-nums;
}

.term-outcomes {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* Where the line has room the link takes its end; on a phone it wraps to a line of its own at the
 * left edge, where every other link of the page starts. */
@media (min-width: 560px) {
    .term-outcomes {
        margin-left: auto;
    }

    .term-what {
        flex: none;
        width: var(--summary-label);
    }
}

/* The side column: Contact, Emails and Documents as smaller sheets, each list a stack of entries with a
 * rule between them, then the lease's number and its technical details. */
.lease-side .card h2 {
    font-size: var(--fs-h3);
}

.contact-list,
.email-list,
.doc-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.contact-line + .contact-line,
.email-line + .email-line,
.doc-line + .doc-line {
    margin-top: var(--s-3);
    padding-top: var(--s-3);
    border-top: 1px solid var(--rule);
}

.contact-line p,
.doc-line p {
    margin: 0;
}

.contact-name {
    font-weight: 650;
}

.contact-note,
.contact-role {
    color: var(--muted);
    font-size: var(--fs-small);
}

.contact-reach {
    overflow-wrap: anywhere;
}

.contact-more {
    margin: var(--s-2) 0 0;
}

/* The side column's own links are 44px targets: they stand alone on their lines. */
.contact-more a,
.email-what,
.doc-name {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.lease-contact .alert {
    margin: var(--s-3) 0 0;
}

.email-line {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

.email-what {
    font-weight: 650;
}

.email-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-1) var(--s-3);
}

.email-when {
    color: var(--muted);
    font-size: var(--fs-small);
}

/* The documents list: the kind and its reading status, the file, its facts on one quiet line, the AI
 * reading and when it came; the whole list is swapped by a poll while a document is read, so nothing
 * here animates. The upload form follows under a rule. */
.doc-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-1) var(--s-3);
}

.doc-name {
    font-weight: 650;
}

.doc-file {
    color: var(--ink-2);
    overflow-wrap: anywhere;
}

.doc-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--s-3);
}

.doc-facts,
.doc-ai,
.doc-when {
    color: var(--muted);
    font-size: var(--fs-small);
}

.doc-reason {
    color: var(--urgent);
}


.doc-file-link a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.documents-banner .alert {
    margin: 0 0 var(--s-3);
}

/* "Add a document" is a fold at the foot of the Documents section (polish pass): one line under the
 * list, the form under it once opened, its rules a line of help under the field each concerns. */
.upload {
    margin-top: var(--s-4);
}

.upload > form {
    margin-top: var(--s-2);
}

/* Every control of the upload form runs the column's width, so its fields share both edges. */
.upload :is(.file-field, select, input[type="date"]) {
    width: 100%;
    max-width: 100%;
}

/* The page's tail sits on its sheets (design elevation U24: it floated off their edges): the lease's
 * number and its technical details are the side sheet's last row, and Stop tracking's closed fold
 * starts on the sheets' text edge (below). */
.lease-ref-block {
    margin: 0 0 var(--section-gap);
    padding: var(--s-3) var(--card-pad);
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-top-width: 0;
    border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
}

.lease-ref {
    margin: 0;
}

/* Stop tracking: low and out of the way at the foot of the page. It is a rare, deliberate act: a
 * quiet disclosure on the paper until it is opened, then a sheet. */
.card.lease-stop:not(:has(details[open])) {
    padding: 0 var(--card-pad);
    border-color: transparent;
    background: transparent;
}

.lease-stop summary h2 {
    color: var(--ink-2);
    font-size: var(--fs-small);
}

.lease-stop > details > summary {
    color: var(--ink-2);
}

/* ------------------------------------------------------------------ 12. review and conflict pages, the page view */

.page-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
    align-items: start;
}

/* The two columns are plain columns, not cards: a card's top rule and padding would sit on one
 * column and not the other, and the two headings would start at different heights. Stacked, a rule
 * parts the picture from its text; side by side, the columns share one top. */
.page-col {
    min-width: 0;
}

.page-col + .page-col {
    padding-top: var(--s-6);
    border-top: 1px solid var(--rule);
}

.page-col-title {
    margin: 0 0 var(--s-3);
}

@media (min-width: 900px) {
    .page-layout {
        grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    }

    .page-col + .page-col {
        padding-top: 0;
        border-top: 0;
    }

    /* Beside the picture the text is a panel of its own height, so it scrolls (and the edge shadows
     * below say so); stacked on a phone it runs at full length with the page. */
    .page-text {
        max-height: 48rem;
        overflow-y: auto;
    }
}

/* Previous and next again at the foot, so a reader at the end of a page does not climb back up. A
 * lone link keeps its side: previous on the left, next on the right. */
.page-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    margin-top: var(--s-6);
    padding-top: var(--s-6);
    border-top: 1px solid var(--rule);
}

.page-nav-next {
    margin-left: auto;
}

/* A page image sits in a letter-portrait frame. The frame is reserved before the picture arrives
 * (so a lazy image cannot shove the section a reader just jumped to), and other page shapes are
 * letterboxed, never cropped. */
.page-image {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 8.5 / 11;
    object-fit: contain;
    background: #ffffff;
    border: 1px solid var(--rule);
    box-shadow: none;
    position: relative;
    min-height: 10rem;
}

/* A picture that failed to load (the page renderer answers 503 when busy and 502 when it stopped)
 * would show as the browser's bare broken-image icon. Only a failed image draws pseudo-elements, so
 * this bordered box, which says which page it was, never touches how a real picture looks. It
 * covers the image's own border so the dashed edge is the only one. */
.page-image::after {
    content: attr(alt) " could not be loaded.";
    position: absolute;
    inset: -1px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s-4);
    background: var(--sheet);
    border: 1px dashed var(--control);
    color: var(--ink-2);
    font-family: var(--font-text);
    font-weight: 500;
    text-align: center;
}

/* The text panel shows a soft shadow along an edge only while there is more to scroll that way: two
 * covers in the panel's own colour are attached to the content (local), so they ride over the
 * shadows at the ends of the text, and two shadows are attached to the box (scroll). Gradients only,
 * no script. Forced colours blank backgrounds, so the border (kept below) marks the panel there. */
.page-text {
    white-space: pre-wrap;
    word-break: break-word;
    font-size: var(--fs-compact);
    background-color: var(--sheet);
    background-image:
        linear-gradient(var(--sheet) 30%, transparent),
        linear-gradient(transparent, var(--sheet) 70%),
        linear-gradient(rgb(18 22 28 / 0.22), transparent),
        linear-gradient(transparent, rgb(18 22 28 / 0.22));
    background-position: top, bottom, top, bottom;
    background-size: 100% 2.5rem, 100% 2.5rem, 100% 1.25rem, 100% 1.25rem;
    background-repeat: no-repeat;
    background-attachment: local, local, scroll, scroll;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    padding: var(--s-3);
}

.page-text mark {
    background: var(--action-bg);
    color: var(--urgent);
    outline: 1px dashed var(--urgent);
}

.page-text .invisible-text {
    color: var(--muted);
    text-decoration: underline dotted;
}

.section-help {
    margin: 0 0 var(--s-2);
}

/* The fact list (UI V2 plan U9): the head, the status strip, then the list of every fact beside the
 * pane that shows one. Mobile first: on a phone the list shows alone, and a page open on a fact
 * (.is-open) shows the pane alone, full width, with "Back to list". From 900px the two sit side by
 * side and "Back to list" goes. Nothing is reordered: the list comes before the pane in the
 * markup, so a keyboard reaches the rows first. */
.fact-review {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
    align-items: start;
}

.fact-review:not(.is-open) > .fact-pane,
.fact-review.is-open > :is(.fact-tools, .fact-side) {
    display: none;
}

.fact-tools,
.fact-side {
    min-width: 0;
}

/* "Skip to the fact": out of sight until a keyboard reaches it, then a button-sized link above the
 * list. On a phone the pane shows only when the page is open on it, so the link is not drawn. */
.fact-skip:not(:focus) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.fact-skip:focus {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    margin: 0 0 var(--s-3);
    padding: 0 var(--s-4);
    border-radius: var(--radius);
    background: var(--navy);
    color: #ffffff;
}

.fact-review:not(.is-open) .fact-skip {
    display: none;
}

/* The toolbar (design elevation U17, Matt's notes): the filters as one segmented row, then the bulk
 * accept as one quiet line under them, both starting on the list's edge (U24: beside the filters it
 * started on no edge the page has). On a desk it runs across the page over the list and the pane, so
 * the filters have room for every status; on a phone it heads the list and goes with it. */
.fact-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--s-2) var(--s-6);
}

/* The filters take their line's width, so on a phone the two columns end on the list's right edge as
 * its rows do; from 560px the row inside keeps its own width. */
.fact-filters {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
}

/* "Accept these N verified values": navy words with a check, no panel (a box only under the pointer,
 * as "Add a lease" in the header), and beside them every fact it would accept, small and muted,
 * wrapping as far as they need and never cut or folded: they are what one click accepts. The
 * per-fact Accept stays the one navy button. */
.bulk-form {
    flex: 1 1 100%;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0 var(--s-4);
    margin: 0;
}

.bulk-form .btn {
    flex: none;
    gap: 0.375rem;
    margin: 0 0 0 -0.5rem;
    padding: 0 0.5rem;
    border-color: transparent;
    background: transparent;
    color: var(--link);
    font-size: var(--fs-small);
    font-weight: 600;
}

.bulk-form .btn:not(.is-busy)::before {
    content: "";
    flex: none;
    width: 1rem;
    height: 1rem;
    background: currentcolor;
    -webkit-mask: var(--icon-check) center / contain no-repeat;
    mask: var(--icon-check) center / contain no-repeat;
}

.bulk-form .btn:hover {
    border-color: var(--rule-strong);
    background: var(--sheet);
    color: var(--navy-strong);
}

/* The names' first line is level with the button's words (a 44px box, its words in the middle). */
.bulk-names {
    flex: 1 1 22rem;
    min-width: 0;
    margin: 0;
    padding-top: calc((var(--target) - 1lh) / 2);
    font-size: var(--fs-meta);
    line-height: 1.45;
}

/* The filters: one compact segmented control, one hairline box with the segments parted by
 * hairlines, the one in use with a navy underline, each with its count. Each segment draws the
 * hairlines on its top and left, pulled under the box's own edge where it starts a row (borders, not
 * a gap over a coloured box, so forced colours keep them). On a phone the control is two columns,
 * two filters a row and an odd last one across both, so every filter shows (UI review polish U5:
 * cut, the boxed four read as complete; wrapped, they ran three and one at 390, UI review 2026-10-29
 * V-03); from 560px it is one row, which scrolls sideways where it is wider than its space (a lease
 * with every status). */
.fact-filters ul {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    margin: 0;
    padding: 0;
    list-style: none;
    overflow: hidden;
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius);
    background: var(--sheet);
}

.fact-filters li {
    margin: -1px 0 0 -1px;
    border-top: 1px solid var(--rule);
    border-left: 1px solid var(--rule);
}

.fact-filters li:last-child:nth-child(odd) {
    grid-column: 1 / -1;
}

@media (min-width: 560px) {
    .fact-filters ul {
        display: flex;
        flex-wrap: nowrap;
        width: max-content;
        max-width: 100%;
        overflow-x: auto;
        scrollbar-width: thin;
    }

    .fact-filters li {
        flex: none;
    }
}

.fact-filters a {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.375rem;
    min-height: var(--target);
    padding: 0 0.875rem;
    color: var(--ink-2);
    font-size: var(--fs-small);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}

.fact-filters a:hover {
    background: var(--paper);
    color: var(--ink);
}

/* The filter in use: the navy underline and the text in ink, as Leases' filters, no tint (design
 * elevation U19: marked twice, not three times). */
.fact-filters a[aria-current="true"],
.fact-filters a[aria-current="true"]:hover {
    background: transparent;
    box-shadow: inset 0 -2px 0 var(--navy);
    color: var(--ink);
    font-weight: 600;
}

.fact-filters a[aria-current="true"] .filter-count {
    color: var(--ink-2);
}

.filter-count {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
}

/* One row per fact: its name with its status chip, then its value (where it came from is the pane's
 * to say: polish pass F1); its one-line definition is the row's tip (below). The whole row is the
 * link. The list starts level with the pane, under the toolbar. */
.fact-list {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow: hidden;
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: none;
}

.fact-item + .fact-item {
    border-top: 1px solid var(--rule);
}

.fact-link {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    min-height: var(--target);
    padding: 0.75rem 1rem;
    color: var(--ink);
    text-decoration: none;
}

.fact-link:hover,
.fact-link:focus-visible {
    background: var(--paper);
    color: var(--ink);
}

/* The row the pane shows: a navy rule down its edge, a shape that reads without colour too, and
 * aria-current says it (design elevation U19: the tint went, one mark is enough). */
.fact-link[aria-current="page"] {
    box-shadow: inset 4px 0 0 var(--navy);
}

.fact-link:focus-visible {
    outline-offset: -2px;
}

.fact-main {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* The name and its status chip (layout pass U7): the chip keeps a slot of its own in every row, so
 * the chips form a column down the list instead of sitting beside a short name and under a long
 * one. From 24em it is the right-hand slot, top-aligned, and the name wraps before it; on the
 * narrowest phones a chip as long as "Needs your input" would leave the name a few letters' room,
 * so there it sits under the name in every row. The chip never wraps (.chip). */
.fact-top {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    justify-items: start;
    gap: var(--s-1) var(--s-2);
}

@media (min-width: 24em) {
    .fact-top {
        grid-template-columns: minmax(0, 1fr) auto;
    }
}

/* The row's value leads, as the summary's do (U16): the fact's name a small quiet label over it. */
.fact-name {
    color: var(--ink-2);
    font-size: var(--fs-small);
    font-weight: 600;
}

/* A fact's one-line definition is a tip (Matt's notes). At rest it is clipped, not removed, so a
 * screen reader reads it in place, after the row or the pane's name. It shows as a dark tip while its
 * row's link, or the info mark beside the pane's name, has the pointer or the focus (interestfor), or
 * once the mark is tapped (popovertarget); Escape closes it. It hangs under what opened it, from its
 * left edge (anchor positioning; a browser without it centres the open tip in the window). */
.fact-tip:not(:popover-open) {
    display: block;
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.fact-tip:popover-open {
    max-width: min(24rem, calc(100vw - 2rem));
    padding: var(--s-2) var(--s-3);
    border: 0;
    border-radius: var(--radius);
    background: var(--ink);
    color: #ffffff;
    font-size: var(--fs-meta);
    line-height: 1.45;
}

@supports (position-area: bottom) {
    .fact-tip:popover-open {
        inset: auto;
        margin: var(--s-1) 0;
        position-area: bottom span-right;
        position-try-fallbacks: flip-block;
    }

    /* The pane's tip stands over the name, so it never covers the value under it. */
    .pane-head > .fact-tip:popover-open {
        position-area: top span-right;
    }

    /* A row's tip starts under the row's name, over the row's lower edge. */
    .fact-item > .fact-tip:popover-open {
        margin: -0.5rem 0 0 1rem;
    }
}

/* A row's value in the serif's figures, as the pane sets it large: the list and the pane say a value
 * the same way, and no figure takes the text face's slashed zero. */
.fact-shown {
    margin-top: 0.125rem;
    color: var(--ink);
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    line-height: 1.35;
    font-variant-numeric: lining-nums tabular-nums;
    overflow-wrap: anywhere;
}

/* A byline ("Entered by hand by Nick on ...") is longer than the pane's line: it may wrap here. */
.fact-tag .fact-source {
    white-space: normal;
}

/* Under To decide, the one line saying how many facts are left out and showing them all (polish pass
 * F1): the foot of the list's sheet, on the paper's tint, its link a 44px target. */
.fact-list:has(+ .fact-rest) {
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}

.fact-rest {
    margin: 0;
    padding: var(--s-1) var(--s-4);
    border: 1px solid var(--rule);
    border-top: 0;
    border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
    background: var(--paper);
    color: var(--muted);
    font-size: var(--fs-small);
}

.fact-rest > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* Colour is spent where Nick has something to do (design elevation U17, as Leases' filters, U15): a
 * settled fact's chip (accepted, rejected, not found, not recorded) keeps its icon and word in the
 * muted ink, in the list and in the pane; to review, a conflict and "Needs your input" keep their
 * tone. */
.fact-item:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .chip::before,
.fact-pane:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .pane-status .chip::before {
    background: var(--muted);
}

/* ...and its word in the muted ink at the text's weight, as the draft's checked facts (U19). */
.fact-item:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .chip,
.fact-pane:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .pane-status .chip {
    color: var(--muted);
    font-weight: 400;
}

/* A fact with nothing recorded is listed, quietly. */
.fact-item.is-quiet .fact-name,
.fact-item.is-quiet .fact-shown {
    color: var(--ink-2);
    font-weight: 500;
}

.fact-empty-filter {
    margin: 0;
}

/* An empty filter's "Show all" is a 44px target, as Leases' and search's ways back are. */
.fact-empty-filter > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* The pane: one fact, or "All N decided". It is a container, so the review card inside lays its
 * page image beside the words only when the pane itself is wide enough, whatever the window. */
.fact-pane {
    container: fact-pane / inline-size;
    min-width: 0;
    padding: var(--s-4);
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    box-shadow: none;
}

/* The pane's bar (crisp pass): where the fact sits in the list ("Fact 4 of 29") on the left and
 * Previous and Next on the right, one row above the fact's name. On a phone (design elevation U24:
 * the navigation filled the first screen, louder than the crumbs) "Back to list" leads the row
 * beside Previous and Next, all three in the crumbs' size, and the count takes the line under them;
 * from 900px "Back to list" goes and the count is back on the left (below). The count is the bar's
 * one reorder: it is plain words, so the Tab order is the screen's, and a screen reader hears where
 * the fact sits before the steps. Where the steps don't fit (the narrowest phones) they wrap, still on
 * the right. */
.pane-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--s-3);
    margin: 0 0 var(--s-3);
}

.pane-back {
    display: flex;
    align-items: center;
    min-height: var(--target);
    font-size: var(--fs-small);
    font-weight: 600;
    text-decoration: none;
}

.pane-back:hover,
.pane-back:focus-visible {
    text-decoration: underline;
}

/* "All N decided" has no bar: its "Back to list" stands alone over the heading, as wide as its words. */
.fact-done > .pane-back {
    width: fit-content;
    margin-bottom: var(--s-2);
}

/* The chevron's drawing sits inside its box, so the box steps left by as much and the drawing
 * stands on the pane's text edge. */
.pane-back::before {
    content: "";
    width: 1.1em;
    height: 1.1em;
    margin-left: -0.3em;
    margin-right: var(--s-1);
    background: currentcolor;
    -webkit-mask: var(--icon-chevron) center / contain no-repeat;
    mask: var(--icon-chevron) center / contain no-repeat;
    transform: rotate(90deg);
}

.pane-nav {
    display: flex;
    gap: var(--s-2);
    margin-left: auto;
}

/* Previous and Next are quiet steps through the list (design elevation U17): navy words with a
 * chevron, a 44px target, no box, so the fact's own decision is the pane's one set of buttons. */
.pane-step {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    min-height: var(--target);
    padding: 0 var(--s-2);
    font-size: var(--fs-small);
    font-weight: 600;
    text-decoration: none;
}

.pane-step:hover {
    text-decoration: underline;
}

.pane-step[rel="prev"]::before,
.pane-step[rel="next"]::after {
    content: "";
    flex: none;
    width: 1rem;
    height: 1rem;
    background: currentcolor;
    -webkit-mask: var(--icon-chevron) center / contain no-repeat;
    mask: var(--icon-chevron) center / contain no-repeat;
}

.pane-step[rel="prev"]::before {
    transform: rotate(90deg);
}

.pane-step[rel="next"]::after {
    transform: rotate(-90deg);
}

.pane-nav > .pane-step:last-child {
    margin-right: calc(-1 * var(--s-2));
}

.pane-count {
    flex-basis: 100%;
    order: 1;
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

/* The fact list's title is a step under the value it decides (34px), so the value leads the page:
 * the lead size, not the page title's (design elevation U17). */
.page-head:has(~ .fact-review) h1 {
    font-size: clamp(1.5rem, 1.3rem + 0.6vw, 1.75rem);
}

/* Under the bar, the fact's name, the info mark that shows its definition, then its chip, on one line
 * where they fit. The value follows straight under them, large (Matt's notes: the proposed value
 * leads), with no rule between. */
.pane-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-1) var(--s-3);
    margin: 0 0 var(--s-3);
}

.pane-head h2 {
    margin: 0;
}

.pane-status {
    margin: 0;
}

/* The info mark: a quiet 16px mark in a 44px target, drawn 8px after the name. */
.def-mark {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--target);
    height: var(--target);
    margin: -0.875rem -0.875rem -0.875rem -1.125rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--muted);
    cursor: help;
}

.def-mark::before {
    content: "";
    width: 1rem;
    height: 1rem;
    background: currentcolor;
    -webkit-mask: var(--icon-info) center / contain no-repeat;
    mask: var(--icon-info) center / contain no-repeat;
}

.def-mark:hover,
.def-mark:focus-visible {
    color: var(--ink);
}

/* The pane's heading already names the fact, so the card's own field heading is not repeated
 * (the card keeps it for a reader of the markup alone). */
.fact-pane .review-item > div > .review-main > h3,
.fact-pane .conflict > h3 {
    display: none;
}

.fact-pane .review-item,
.fact-pane .conflict {
    padding: 0;
    border-top: 0;
}

.fact-pane .review-item + .review-item {
    margin-top: var(--s-6);
    padding-top: var(--s-6);
    border-top: 1px solid var(--rule);
}

.fact-tag {
    margin: 0 0 var(--s-3);
}

.fact-done h2 {
    margin: 0 0 var(--s-2);
    color: var(--ok);
}

.fact-done h2::before {
    content: "";
    display: inline-block;
    width: 1.1em;
    height: 1.1em;
    margin-right: var(--s-2);
    vertical-align: -0.15em;
    background: currentcolor;
    -webkit-mask: var(--icon-check) center / contain no-repeat;
    mask: var(--icon-check) center / contain no-repeat;
}

@media (min-width: 720px) {
    .fact-pane {
        padding: var(--s-6);
    }
}

@media (min-width: 900px) {
    .fact-review.has-pane {
        grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr);
        gap: var(--s-4) var(--section-gap);
    }

    .fact-review > .fact-tools {
        grid-column: 1 / -1;
    }

    /* Beside a pane the bulk line takes the list's and the pane's columns, as the status strip does:
     * its button over the list, the names it accepts starting on the pane's edge (design elevation
     * U24). */
    .fact-review.has-pane .bulk-form {
        display: grid;
        grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr);
        column-gap: var(--section-gap);
    }

    .fact-review.has-pane .bulk-form .btn {
        justify-self: start;
    }

    .fact-review:not(.is-open) > .fact-pane,
    .fact-review.is-open > .fact-side {
        display: block;
    }

    .fact-review.is-open > .fact-tools {
        display: flex;
    }

    .fact-review:not(.is-open) .fact-skip {
        display: inline-flex;
    }

    .pane-back {
        display: none;
    }

    .pane-count {
        flex-basis: auto;
        order: 0;
    }
}

/* On a window that can hold it, at least 1024px wide and 700px tall, the pane stays in view while
 * the list scrolls past it, and a fact taller than the window scrolls inside the pane (layout pass
 * U7). Its scroll padding lands a fragment or a focused message clear of its edge, and its gutter
 * is always kept, so a scrollbar coming and going never flips the pane's own 46rem width query.
 * Narrower or shorter windows (125 percent zoom on a 1366 by 768 laptop is about 1093 by 520) keep
 * the pane scrolling with the page, and so does paper, which is narrower than 1024px. */
@media (min-width: 1024px) and (min-height: 700px) {
    .fact-pane {
        position: sticky;
        top: var(--s-4);
        max-height: calc(100dvh - 2 * var(--s-4));
        overflow-y: auto;
        scroll-padding-block: var(--s-4);
        scrollbar-gutter: stable;
    }

    /* The page beside the words keeps its sticky, inside the pane's scroll, but no frame of its own:
     * a scroll box inside the scrolling pane is a trap, as the quote's would be (below). At its 22rem
     * it is about 32rem tall, inside the pane's scrollport on any window this tall, and if a larger
     * text size makes it taller, its foot shows as the pane scrolls to the item's end. */
    .fact-pane .review-layout > .review-image {
        max-height: none;
        overflow: visible;
    }
}

/* One proposed value is a check sheet: the field, the value in big type, the chip that says
 * whether the words were found, the quote with the match highlighted, and the real page. */
.review-item {
    padding: var(--s-6) 0;
    border-top: 1px solid var(--rule);
}

.review-item:first-of-type {
    border-top: 0;
}

.review-item h3 {
    margin: 0 0 var(--s-1);
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
}

.review-value {
    margin: var(--s-1) 0 var(--s-2);
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 1.5rem;
    line-height: 1.2;
    font-variant-numeric: lining-nums tabular-nums;
    overflow-wrap: anywhere;
}

/* In the pane the value leads (Matt's notes): 34px on a desk, straight under the fact's name; a
 * conflict's readings set theirs a step smaller (below), level with each other. */
.fact-pane .review-value {
    margin: 0 0 var(--s-3);
    color: var(--ink);
    font-size: 1.75rem;
    line-height: 1.15;
    letter-spacing: -0.01em;
}

@media (min-width: 720px) {
    .fact-pane .review-value {
        font-size: 2.125rem;
    }
}

/* Narrow (the pane's own width, not the window's): one column, and the page waits behind "Enlarge
 * page N" (.page-peek) between the quote and the buttons; the column beside the words (.review-image)
 * is not drawn. Wide enough, the container rule below swaps them. */
.review-layout > .review-image {
    display: none;
}

.review-image p {
    margin: var(--s-2) 0 0;
}

/* The verdict chip with its sentence, under the value (crisp pass): the chip, then the sentence in a
 * column of its own beside it, small and muted, wrapping inside that column rather than under the
 * chip. Where the two don't fit side by side (a phone, a side of a conflict, a long chip) the
 * sentence goes whole under the chip. Every verdict lays out so: a quote's check, a notice clause's
 * numbers, "Likely supersedes". The chip may still wrap its own words on the narrowest phone. */
.verdict {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-1) var(--s-3);
    margin: 0 0 var(--s-2);
}

.verdict > .chip + .small {
    flex: 1 1 15rem;
    min-width: 0;
    color: var(--muted);
}

/* A notice clause's working line: how the deadline was counted, a weekend or holiday rolled
 * back. It is arithmetic Nick checks against the lease, so it reads as a worked line, not prose. */
.working-line {
    margin: var(--s-3) 0;
    padding: var(--s-2) var(--s-3);
    background: var(--paper);
    border-left: 3px solid var(--navy);
    border-radius: var(--radius);
    font-variant-numeric: tabular-nums;
}

/* The page that enlarges in place (_page_pop.html, Matt's notes). "Enlarge page N", where the pane is too
 * narrow for the page beside the words and on each side of a conflict, is a button set as a link; the
 * thumbnail is a button that is the picture. Either opens the popover: a sheet over the dimmed page,
 * its bar of the page's number, the link to the page with its text, and Close above the page. The
 * sheet is as wide as the window allows, up to 60rem, so the lease's own type reads at about its
 * printed size (UI review 2026-10-29 V-20: sized from the window's height, a letter page drew at
 * about 340px on a phone); the page scrolls under the bar, which stays put, and on a phone it keeps
 * a readable 40rem and pans sideways. */
.page-peek {
    margin: var(--s-2) 0;
}

.page-open {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    padding: 0;
    border: 0;
    background: none;
    color: var(--link);
    font: inherit;
    font-weight: 600;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
    text-decoration-color: color-mix(in srgb, currentcolor 40%, transparent);
    cursor: pointer;
}

.page-open:hover,
.page-open:focus-visible {
    color: var(--navy-strong);
}

.page-zoom {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
}

.page-zoom:hover .page-image {
    border-color: var(--control);
}

/* The thumbnail cut down to the quoted words (finishing pass U11): a frame of the excerpt's own
 * shape, which a whole page, served when no excerpt can be made, still fits (.page-image contains). */
.page-crop {
    aspect-ratio: 3 / 2;
}

.page-pop {
    width: min(calc(100vw - 2rem), 60rem);
    max-height: calc(100dvh - 2rem);
    padding: var(--s-3) var(--s-4) var(--s-4);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sheet);
    background: var(--sheet);
    box-shadow: var(--lift);
    color: var(--ink);
    overflow: auto;
}

.page-pop::backdrop {
    background: color-mix(in srgb, var(--ink, #172238) 45%, transparent);
}

/* Open only: a closed popover's display stays the browser's (none). */
.page-pop:popover-open {
    display: flex;
    flex-direction: column;
}

.page-pop-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--s-4);
    margin: 0 0 var(--s-2);
}

.page-pop-title {
    margin: 0;
    font-weight: 600;
}

.page-pop-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

.page-pop-close {
    margin-left: auto;
}

/* The enlarged page says it is loading instead of opening on a blank white sheet (UI research gaps
 * plan U2): the words (.muted.small) and the picture share one cell, the picture on top. Its white
 * ground is taken off here only, so the words show through until the page (an opaque PNG) paints over
 * them; a picture that fails draws its own cover (.page-image::after, inset -1px), which hides them
 * too. The frame is the part that scrolls, with a pixel of room so the cover's edge isn't clipped. */
.page-pop-frame {
    display: grid;
    padding: 1px;
    overflow: auto;
}

.page-pop-wait {
    grid-area: 1 / 1;
    place-self: center;
    margin: 0;
}

.page-pop-frame > .page-image {
    grid-area: 1 / 1;
    min-width: 40rem;
    background: transparent;
}

/* A fact not waiting: who decided it, beside where it came from; then how it changes. */
.fact-who {
    font-weight: 650;
}

.fact-who + .fact-source {
    margin-left: var(--s-1);
}

.review-value.is-empty {
    color: var(--muted);
}

.fact-change {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.fact-change > p:last-child {
    margin-bottom: 0;
}

/* A conflict's lead says nothing changes until Nick picks, then the reminders it holds. */
.conflict-lead {
    margin: 0 0 var(--s-3);
}

.conflict-holds {
    display: block;
    margin-top: var(--s-1);
    color: var(--ink-2);
    font-size: var(--fs-small);
}

.fact-done .done-way {
    margin: var(--s-3) 0;
}

/* The actions are one group (crisp pass): Accept, then the Edit and Reject folds beside it on one
 * row from the same left edge, a small gap apart, each a 44px target (a fold's summary is section 8's
 * details row, its chevron inside its own edge). A form that stands before them (Accept after
 * checking the page, Accept anyway) takes the row's full width, and so does an open fold's form,
 * under the row (the @supports block after these rules keeps its button in place; a browser without
 * it starts a row with the open fold, as before). Tab still goes Accept, Edit, Reject. */
.review-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-4);
    margin-top: var(--s-3);
}

.review-actions > .stack-form,
.review-actions > details[open] {
    flex-basis: 100%;
}

.review-actions .stack-form {
    margin-bottom: 0;
}

.review-actions > details[open] > .stack-form {
    margin-top: var(--s-1);
    padding-bottom: var(--s-2);
}

/* Edit and Reject are drawn as buttons beside Accept (design elevation U17: one shape for the three
 * choices), the secondary button's white and hairline, with no chevron (it read as "go to"); an
 * open fold's button takes the navy tint, which says it is open. On a phone the three share the row
 * in equal parts; from 560px each is as wide as its words. */
.review-actions > .inline-form,
.review-actions > details:not([open]) {
    flex: 1 1 0;
    min-width: 0;
}

.review-actions > .inline-form > .btn {
    width: 100%;
}

.review-actions > details > summary::before {
    display: none;
}

.review-actions > details > summary {
    justify-content: center;
    padding: 0 var(--s-4);
    border: 1px solid var(--control);
    border-radius: var(--radius);
    background: var(--sheet);
    color: var(--ink);
    font-weight: 600;
    text-decoration: none;
}

.review-actions > details > summary:hover {
    border-color: var(--ink);
    background: var(--paper);
}

.review-actions > details[open] > summary {
    border-color: var(--navy);
    background: var(--navy-tint);
    color: var(--navy-strong);
}

@media (min-width: 560px) {
    .review-actions > .inline-form,
    .review-actions > details:not([open]) {
        flex: none;
    }

    .review-actions > .inline-form > .btn {
        width: auto;
    }

    .review-actions > details > summary {
        width: fit-content;
    }
}

/* An open fold leaves its button where it was (UI review 2026-10-29, V-15: the button just pressed
 * moved out from under the pointer to a line of its own, and the other fold's button landed under
 * the form like one of the form's own). Where the browser can style a fold's content, the open fold
 * steps aside (display: contents): its summary stays in the row with the closed one's share of it,
 * and its content, ordered after every button, runs the row's full width under them. The order
 * moves only the drawing: Tab and a screen reader go Edit, its form, then back up to Reject, the
 * trade Matt chose over a moving button. The fold's opening (14) still plays on the content. */
@supports selector(::details-content) {
    .review-actions > details[open] {
        display: contents;
    }

    /* On a phone the open fold's summary, now the row's item itself, takes the closed fold's share.
     * A flex item never starts narrower than its own padding and edge, which the summary has (2 x
     * --s-4 and 1px each side) and a form or a closed fold doesn't, so while a fold is open those
     * start from the same width, or the open button would take that much more than its share. */
    .review-actions > details[open] > summary {
        flex: 1 1 0;
        min-width: 0;
    }

    .review-actions:has(> details[open]) > :is(.inline-form, details:not([open])) {
        flex-basis: calc(2 * var(--s-4) + 2px);
    }

    .review-actions > details[open]::details-content {
        order: 1;
        flex-basis: 100%;
    }

    @media (min-width: 560px) {
        .review-actions > details[open] > summary {
            flex: none;
        }

        .review-actions:has(> details[open]) > :is(.inline-form, details:not([open])) {
            flex-basis: auto;
        }
    }
}

/* The quoted words with the page around them, shown whole at every width (layout pass U7): the
 * text is bounded in Python (the match, and src/review/cards.py CONTEXT_CHARS either side), so it
 * needs no cap, and a scroll box would cut it mid-line inside a pane that scrolls itself. It reads as
 * the lease's own words: no box, a ruled inset, the matched words highlighted. */
.quote-context {
    white-space: pre-wrap;
    word-break: break-word;
    font-family: var(--font-serif);
    font-size: var(--fs-compact);
    line-height: 1.55;
    font-variant-numeric: lining-nums;
    background: transparent;
    border: 0;
    border-left: 2px solid var(--rule-strong);
    border-radius: 0;
    padding: 0.25rem 0 0.25rem 1rem;
    margin: var(--s-2) 0;
    color: var(--ink-2);
}

/* The quote's two links (the page with its text, the PDF) stand apart by a gap, not a middle dot. */
.quote-links {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--s-4);
}

/* "Enlarge page N" opens the line in the line's own size and weight: one line, one size. */
.quote-links > .page-open {
    font-weight: inherit;
}

.quote-context mark {
    background: linear-gradient(transparent 12%, var(--highlight) 12% 90%, transparent 90%);
    color: var(--ink);
    font-weight: 600;
    padding: 0 0.125rem;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
}

/* The page beside the words once the pane is wide enough to give both room (the pane's width,
 * not the window's: beside the list the pane is much narrower than the screen). */
@container fact-pane (min-width: 46rem) {
    .review-layout {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
        column-gap: var(--s-6);
        align-items: start;
    }

    /* The picture stays beside the item while its buttons scroll, and scrolls inside its own
     * frame when it is taller than the window (a short laptop, or 125 percent zoom). */
    .review-layout > .review-image {
        display: block;
        position: sticky;
        top: var(--s-4);
        max-height: calc(100dvh - 2 * var(--s-4));
        margin: 0;
        overflow: auto;
    }

    .review-layout .page-peek,
    .review-layout .quote-links > .page-open {
        display: none;
    }
}

/* A link to #conflicts (written before a conflict opened on its own fact) lands with the fact's name,
 * its chip and the pane's bar still in view above it, not scrolled past inside the sticky pane. */
#conflicts {
    scroll-margin-top: 8rem;
}

/* Two sources disagree: the sides sit next to each other when the card is wide enough to read
 * two pages, and stack when it is not, whatever the page does around it. */
.conflict {
    container-type: inline-size;
    padding: var(--s-6) 0;
    border-top: 1px solid var(--rule);
}

/* On the old conflicts page (conflicts.html, a refused resolve's landing) each conflict is a card of
 * its own, so the card's edge parts it from the next: no rule and no padding of its own. */
.card > .conflict {
    padding: 0;
    border-top: 0;
}

.conflict-sides {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
    margin: var(--s-3) 0;
}

/* Side by side, each caption keeps room for two lines (below), so the values start level. */
@container (min-width: 44rem) {
    .conflict-sides {
        grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
    }

    .conflict-side > .side-kicker {
        min-height: calc(2lh + var(--s-2));
    }
}

/* A conflict's readings (Matt's notes): columns on the sheet with no box, a hairline over each and
 * the likely one's rule in ink, each value set large under its small caption. Side by side, each
 * caption keeps room for two lines, so the two values start level and face each other (a third line
 * is rare and pushes only its own side down). */
.conflict-side {
    display: flex;
    flex-direction: column;
    min-width: 0;
    border-top: 1px solid var(--rule-strong);
}

/* Side by side the columns are one height, so each side's page links drop to its foot and stand
 * level with the other side's, whatever the quotes' lengths. */
.conflict-side > .link-line {
    margin-top: auto;
    padding-top: var(--s-2);
}

.conflict-side > :last-child {
    margin-bottom: 0;
}

.conflict-side > .side-kicker {
    margin: 0 0 var(--s-1);
    padding: var(--s-2) 0 0;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
    font-weight: 500;
    line-height: 1.45;
}

.conflict-side-likely {
    border-top: 2px solid var(--ink-2);
}

.fact-pane .conflict-side .review-value {
    margin-bottom: var(--s-2);
    font-size: 1.625rem;
}

@media (min-width: 720px) {
    .fact-pane .conflict-side .review-value {
        font-size: 1.875rem;
    }
}

/* The status strip sits between the head and the list: the line a decision landed with (it takes
 * focus), then "N of M decided" and how many are still to review, then the bar. The bar is progress,
 * not a promise of a fixed total: a new reading can add facts to decide, and the words carry the
 * numbers whatever the bar looks like. It is a compact band (layout pass U7) on the paper, part of
 * the page's head, not a sheet of its own (design elevation U17: one sheet around one line read as a
 * card kit), so the list starts sooner: on a phone the three stack in source order, and from 900px
 * they share one row (below). */
.queue-strip {
    scroll-margin-top: var(--s-6);
    margin: 0 0 var(--s-6);
    padding: 0;
}

.queue-flash:empty {
    display: none;
}

.queue-flash > .alert {
    margin: 0 0 var(--s-2);
}

.queue-flash > .alert:focus {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

.queue-line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2) var(--s-4);
}

/* What is left leads (polish pass F1), "6 to decide", at the values' 17px, not a size of its own
 * (design elevation U19: one size fewer); "N of M decided" is the quiet progress beside it, and
 * takes the lead back once nothing is left (.is-done, below). */
.queue-left,
.queue-strip.is-done .queue-count {
    margin: 0;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--fs-reading);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

.queue-count {
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

/* The native element keeps its meaning for assistive technology; only its drawing is replaced. Each
 * vendor pseudo-element is its own rule, because one selector a browser does not know voids a whole
 * selector list. Progress is a slim filled track, no outline: the track in the rule colour, the
 * filled part navy. */
/* The bar is a quiet track under the count it repeats (U19: no longer the page's heaviest stroke). */
.queue-bar {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 0.25rem;
    margin: var(--s-2) 0 0;
    border: 0;
    border-radius: 999px;
    background: var(--rule);
    color: var(--navy);
}

.queue-bar::-webkit-progress-bar {
    border-radius: 999px;
    background: var(--rule);
}

.queue-bar::-webkit-progress-value {
    border-radius: 999px;
    background: var(--navy);
}

.queue-bar::-moz-progress-bar {
    background: var(--navy);
}

/* Finished: the all-clear check and the calm colour the empty states already use. */
.queue-strip.is-done .queue-count {
    color: var(--ok);
}

.queue-strip.is-done .queue-count::before {
    content: "";
    display: inline-block;
    width: 1.1em;
    height: 1.1em;
    margin-right: var(--s-2);
    vertical-align: -0.2em;
    background: currentcolor;
    -webkit-mask: var(--icon-check) center / contain no-repeat;
    mask: var(--icon-check) center / contain no-repeat;
}

.queue-strip.is-done .queue-bar {
    color: var(--ok);
}

.queue-strip.is-done .queue-bar::-webkit-progress-value {
    background: var(--ok);
}

.queue-strip.is-done .queue-bar::-moz-progress-bar {
    background: var(--ok);
}

/* From 900px, where the list and the pane go side by side under it, the band is one row: the status
 * line, the count, then the bar taking the rest. A long status line pushes the bar, then the count,
 * onto the next row, still in source order (nothing is reordered). Over a list with its pane the
 * band takes their columns (design elevation U24): the status line across both, the count over the
 * list, the bar starting on the pane's edge. */
@media (min-width: 900px) {
    .queue-strip {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--s-2) var(--s-6);
    }

    .queue-flash {
        display: flex;
        flex: 1 1 20rem;
        flex-direction: column;
        gap: var(--s-2);
        min-width: 0;
    }

    .queue-flash > .alert {
        margin: 0;
    }

    .queue-bar {
        flex: 1 1 8rem;
        width: auto;
        margin: 0;
    }

    .queue-strip:has(+ .fact-review.has-pane) {
        display: grid;
        grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr);
        gap: var(--s-2) var(--section-gap);
    }

    .queue-strip:has(+ .fact-review.has-pane) > .queue-flash {
        grid-column: 1 / -1;
    }
}

/* ------------------------------------------------------------------ 12c. alert, email draft, contacts */

/* The alert page (UI V2 plan U19): the date tiles, the facts, then "Email the client" (each draft with
 * its status chip, link-row) and the folded "Act on this alert". The links on lines of their own are
 * link-line targets (section 11), and the snooze and dismiss forms keep a narrow form's width
 * (form-short, 8b). The tiles are all one size and share a top line (layout pass U8): a tile that
 * names its year is a line taller at its foot, not pushed up. On a phone they wrap two to a row. */
.alert-dates {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--s-4) var(--s-6);
    margin: var(--s-4) 0 var(--s-6);
}

/* The alert's heading (UI review 2026-10-29, V-32): its date and its distance are each a whole piece
 * and the pair a third (alert-when), so the pair keeps one line where it fits on one. Balanced, not
 * pretty (3): when the pair takes a line of its own, the words before it share theirs, never leaving
 * "by" alone on a phone. Balancing can't split the date here, as it can a bare one: it is whole. */
.page-head-row h1:has(> .alert-when) {
    text-wrap: balance;
}

/* "Act on this alert" stays closed until wanted: the email is the main thing on this page. */
.act-on > summary {
    padding: 0;
    text-decoration: none;
}

.act-on[open] > summary {
    margin-bottom: var(--s-3);
}

/* Two forms in one place are ruled apart, so the second's first label never reads as part of the
 * first: Snooze and Dismiss under "Act on this alert", and a contact's details and its do-not-contact
 * form on the contacts page. */
.act-on > .stack-form + .stack-form,
.contact-card > .stack-form + .stack-form {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

/* Dismiss's choices are a group, not a box (UI review 2026-10-29, V-17). What only one choice uses
 * stands under that radio, indented to its words (the radio and its gap, 8): the other reminder
 * kinds' tick under the rest of this term, so it never reads as a fourth choice, and the reason
 * under the whole lease, labelled as a field is. */
.dismiss-under {
    margin-left: calc(1.25rem + var(--s-3));
}

.stack-form .dismiss-under > label {
    font-weight: 600;
}

/* The email draft (UI V2 plan U11) opens read-first: the head (who it is for, its kind and status,
 * "Nothing is sent until you press Send", then any reasons it can't go), the email as it will go,
 * "Facts used in this email" and the actions. On a phone they follow each other and the actions are
 * a bar that stays at the bottom of the screen, clear of the home indicator, while the email and its
 * facts are read; from 1024px the email sits on the left and its facts, then Send, in a column
 * beside it. The grid's areas follow the source: nothing is reordered. The head and its lines stand
 * on the paper (layout pass U3), the line under the title close under it (below). */

/* Whose email, its kind and premises, then its status chip: one quiet line of text, as wide as the
 * page, that wraps as text does. It sits close under the title: 0.25rem above it instead of the shared
 * --s-2 (11b), and the title keeps no margin of its own under it. Then "Nothing is sent until you
 * press Send" in the text face. */
.draft-about {
    max-width: none;
    margin-top: 0.25rem;
    margin-bottom: var(--s-3);
    color: var(--muted);
}

.page-head:has(+ .draft-about) h1 {
    margin-bottom: 0;
}

.draft-about a {
    font-weight: 650;
}

.draft-about .chip {
    margin-left: var(--s-1);
}

.send-note {
    margin: 0.5rem 0 var(--s-3);
    color: var(--ink-2);
    font-family: var(--font-text);
    font-size: 1rem;
    font-weight: 400;
}

.send-blocks {
    margin: var(--s-3) 0 0;
}

.send-blocks .inline-form,
.block-list .fix-link {
    margin-left: var(--s-1);
}

/* A fix is a link (or Regenerate, a button) after the reason it puts right, a full-height target. */
.fix-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-weight: 650;
}

.email-review {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    grid-template-areas: "email" "facts" "actions";
    column-gap: var(--section-gap);
}

.email-sheet {
    grid-area: email;
    min-width: 0;
}

/* "The email" and, while it can be changed, "Edit draft" beside it, as the email's own header. */
.email-sheet-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-2) var(--s-4);
    margin-bottom: var(--s-3);
}

.email-sheet-head h2 {
    margin: 0;
}

/* "Edit draft" keeps its 44px target, but half of it reaches past the heading's line, so the head is
 * as tall as "The email" and both sheets' headings stand on one line (design elevation U18). */
.email-sheet-head > .btn {
    margin-block: -0.625rem;
}

.facts-used {
    grid-area: facts;
    min-width: 0;
}

.facts-used h2 {
    margin-bottom: 0.25rem;
}

.facts-used > .muted.small {
    margin-bottom: 0.5rem;
    font-size: var(--fs-meta);
}

.email-actions,
.email-actions-note {
    grid-area: actions;
}

/* The facts sheet and the actions under it share a column (finishing pass U4) that draws no box of
 * its own: each keeps its place in the grid above, and on a phone Send stays the bar at the foot of
 * the screen. Only on a desk tall enough to keep it in view is it a box (below). */
.email-side {
    display: contents;
}

/* To, CC, From and Subject over the letter, its memo head rather than a list of facts: on a phone each
 * label over its value, so the recipient's address keeps the line's whole width; from 560px the label
 * in a narrow column with the value beside it. The values are the email's own, in its face (U18). */
.email-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin: 0 0 var(--s-3);
}

/* One rule, under the last row, between the head and the letter: the rows group by their spacing
 * (U19). */
.email-head > div {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: baseline;
    gap: 0 var(--s-3);
    padding: 0.375rem 0;
}

.email-head > div:last-child {
    padding-bottom: 0.625rem;
    border-bottom: 1px solid var(--rule);
}

@media (min-width: 560px) {
    .email-head > div {
        grid-template-columns: 4.5rem minmax(0, 1fr);
    }
}

.email-head dt {
    color: var(--muted);
    font-size: var(--fs-small);
}

.email-head dd {
    margin: 0;
    overflow-wrap: break-word;
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-variant-numeric: lining-nums proportional-nums;
}

/* In the draft's card the email is not a box inside a box (layout pass U8): the body sits on the sheet
 * under the Subject rule, as the letter itself, with no ground, border or lift, at a letter's measure
 * (about 70 characters a line, design elevation U18) while the To and Subject rows run the sheet's
 * width as its memo head. The sheet itself (.email-preview, 11), the locked footer and the printed
 * copy are unchanged. */
.email-sheet .email-preview {
    max-width: 38rem;
    margin-bottom: 0;
    padding: 1rem 0 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

/* Each fact the email uses, a ledger's row. On a phone the label in Email sent's 7.5rem column with the
 * value beside it, then the state's chip, what is wrong and its fix, each on a line of its own across
 * the row (finishing pass U12, U24-21). From 560px the label with its chip at the right edge on one
 * line, the value across the row under them. The value is in the serif (one face for every value,
 * design elevation U18); rows tight enough that Send stands in the demo window under them. */
.facts-used-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.used-fact {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    align-items: baseline;
    gap: 0 0.75rem;
    padding: 0.375rem 0;
    border-bottom: 1px solid var(--rule);
}

.used-fact:last-child {
    border-bottom: 0;
}

.used-label {
    grid-column: 1;
    color: var(--muted);
    font-size: var(--fs-meta);
}

.used-fact > .chip {
    grid-column: 1 / -1;
    justify-self: start;
}

/* The recipient takes the row's width under its label: a name and an address kept whole don't fit the
 * phone ledger's value column (finishing pass U12, U18-32). */
.used-fact[data-fact="recipient"] > .used-value {
    grid-column: 1 / -1;
}

.used-value {
    min-width: 0;
    overflow-wrap: break-word;
    color: var(--ink);
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-weight: 500;
    font-variant-numeric: lining-nums tabular-nums;
}

/* A checked fact keeps its icon and word, both in the muted ink at the text's weight: status colour
 * and weight are for a fact that holds the email (as the fact list's settled facts, design
 * elevation U17). */
.used-fact[data-state="ok"] .chip {
    color: var(--muted);
    font-weight: 400;
}

.used-fact[data-state="ok"] .chip::before {
    background: var(--muted);
}

.used-note {
    color: var(--ink-2);
    font-size: var(--fs-small);
}

.used-note,
.used-fix {
    grid-column: 1 / -1;
}

@media (min-width: 560px) {
    .used-fact {
        grid-template-columns: minmax(0, 1fr) auto;
    }

    .used-fact > .chip {
        grid-row: 1;
        grid-column: 2;
        justify-self: end;
    }

    .used-value {
        grid-column: 1 / -1;
    }
}

.used-fact.is-unconfirmed {
    padding: 0.75rem;
    border-left: 0;
    border-radius: var(--radius);
    background: var(--action-bg);
}

/* A date in the letter that a line here gives links to that line (finishing pass U5): the letter's own
 * ink under a dotted underline, unlike a link's line and the quote's highlighter; the line it opens is
 * tinted while it is the page's target, the tint running a step past its edges so its words keep their
 * place (an unchecked line's tint too). Forced colours keep the dotted line and outline the target
 * (15); paper prints the date as plain text (16). */
a.letter-date {
    color: inherit;
    text-decoration-style: dotted;
    text-decoration-thickness: 2px;
}

.used-fact:target {
    background: var(--navy-tint);
    box-shadow: -0.5rem 0 var(--navy-tint), 0.5rem 0 var(--navy-tint);
}

/* Send (navy, with the mail glyph) on its own, the width of its column; a blocked Send keeps its place,
 * disabled, with the note saying why under it. On a phone it is a bar that sticks to the bottom of the
 * screen inside the email review, its padding taking the home indicator's height on top. The page
 * ends on it: no margin or page padding under it, so its band meets the footer's rule (design
 * elevation U24: a strip of the paper showed between them); from 1024px it is the facts sheet's
 * closing row and both come back. */
.email-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-3);
    margin: 0 calc(-1 * var(--s-4));
    padding: var(--s-3) var(--s-4) calc(var(--s-3) + env(safe-area-inset-bottom));
    background: var(--sheet);
    border-top: 1px solid var(--rule);
    box-shadow: none;
}

.site-main:has(> .email-review:last-child .email-actions) {
    padding-bottom: 0;
}

.send-form {
    display: flex;
    flex: 1 1 100%;
    margin: 0;
}

.send-button {
    flex: 1 1 auto;
    gap: var(--s-2);
}

.email-actions .action-note,
.email-actions-note {
    flex: 1 1 100%;
    margin: 0;
}

@media (min-width: 720px) {
    .card.email-sheet,
    .card.facts-used {
        padding: 1.5rem;
    }
}

/* On a desk the email's sheet is as wide as its letter and the facts take the rest, at least 26rem
 * (an address on one line), and Send is the facts sheet's closing row, under a rule: the check and the
 * act it gates read as one thing (design elevation U18). Matt's view, with no Send, keeps the sheet
 * whole. */
@media (min-width: 1024px) {
    .email-review {
        grid-template-columns: minmax(0, 46rem) minmax(26rem, 1fr);
        grid-template-areas: "email facts" "email actions" "email .";
        grid-template-rows: auto auto 1fr;
    }

    .card.facts-used {
        margin-bottom: 1rem;
    }

    .email-side:has(> .email-actions) > .card.facts-used {
        margin-bottom: 0;
        padding-bottom: var(--s-3);
        border-bottom: 0;
        border-bottom-right-radius: 0;
        border-bottom-left-radius: 0;
    }

    .site-main:has(> .email-review:last-child .email-actions) {
        padding-bottom: var(--s-12);
    }

    .email-actions {
        position: static;
        margin: 0 0 var(--s-4);
        padding: var(--s-3) 1.5rem 1.5rem;
        background: var(--sheet);
        border: 1px solid var(--rule);
        border-top: 0;
        border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
        box-shadow: none;
    }

}

/* On a desk window that can hold it, at least 1024px wide and 700px tall (as the fact list's pane), the
 * column stays in view while the email scrolls past it: it spans the grid's rows, sticks under the top
 * of the window and is never taller than the window less that gap, so a facts list longer than that
 * scrolls inside its sheet while Send stays at the column's foot, its own margin the gap under it
 * (finishing pass U4, critic row U18-37). Shorter windows keep the column scrolling with the page, and
 * so does paper, which is narrower than 1024px. */
@media (min-width: 1024px) and (min-height: 700px) {
    .email-side {
        position: sticky;
        top: var(--s-4);
        display: flex;
        flex-direction: column;
        grid-column: 2;
        grid-row: 1 / -1;
        max-height: calc(100dvh - var(--s-4));
    }

    .email-side > .card.facts-used {
        min-height: 0;
        overflow: auto;
    }
}

/* Edit mode: the composer is the edit view's card itself, not a box inside it (design elevation U18:
 * the card gives the sheet, its padding and its edge). */
.composer {
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* The text box in the letter's face, so what is typed looks as the email reads (design elevation U18).
 * It grows with the letter over a floor (UI research gaps plan U1), up to a ceiling where a longer letter
 * scrolls inside it again. On a phone the box showed 7 lines of a 20-line letter (UI review 2026-10-29
 * V-46); its ceiling is the screen less room for the label and help line above it (about 4rem) and the
 * sticky Save bar below (about 5rem), so the bar never sits on the box's last lines once the box is in
 * view. A browser without field-sizing sizes the box by its rows (draft.html) between the same floor and
 * ceiling, and a height dragged with the resize handle stops the growing. */
.composer textarea {
    width: 100%;
    min-height: 16rem;
    max-height: calc(100dvh - 11rem);
    field-sizing: content;
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-variant-numeric: lining-nums proportional-nums;
    line-height: 1.55;
}

/* The text box and the footer added under it at the letter's measure, so the text wraps about where the
 * read view's letter does (U18), and the subject above it as wide as the box (UI review 2026-10-29
 * V-47: it took the card's whole width). */
.composer textarea,
.composer .email-preview--footer,
.composer input[type="text"] {
    max-width: 40rem;
}

.action-note {
    flex: 1 1 100%;
    margin: 0;
    color: var(--action);
    font-size: var(--fs-small);
    font-weight: 600;
}

/* Narrow: Save and Cancel stay at the bottom of the screen while the text is edited, clear of the
 * home indicator. The bar is sticky inside the form (nothing moves outside it), so Save is still the
 * form's first button for the Enter key, and it is one line of text at most. It reaches the card's
 * edges (the card's padding, now the composer has none of its own). */
.composer .form-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    margin: var(--s-4) calc(-1 * var(--s-4)) 0;
    padding: var(--s-3) var(--s-4) calc(var(--s-3) + env(safe-area-inset-bottom));
    background: var(--sheet);
    border-top: 1px solid var(--rule);
}

/* A refused save puts focus on its message and the browser scrolls it into view; the sticky bar above
 * must not end up sitting on its last line, so the message keeps the bar's height clear. */
.composer [role="alert"][tabindex="-1"] {
    scroll-margin-bottom: 7rem;
}

/* A link of its own under a field ("Client contacts"): a full-height target. */
.field-link {
    margin: 0;
}

.field-link a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* Regenerate sits under the composer, apart from Save, with its warning above the button. */
.regenerate-form {
    margin-top: var(--s-6);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.regenerate-form .field-help {
    margin: 0;
}

@media (min-width: 720px) {
    /* On a desk, where Save is static under the box, the box's floor is higher and its ceiling most of the
     * window, so the whole of a letter reads without scrolling inside the box and Save stays a short scroll
     * away (UI research gaps plan U1). */
    .composer textarea {
        min-height: 22rem;
        max-height: 70dvh;
    }

    .composer .form-actions {
        position: static;
        margin: var(--s-4) 0 0;
        padding: 0;
        background: none;
        border-top: 0;
    }
}

/* Email sent (R20): the status line, when it went and to whom, the lease and the dates the email gave,
 * what's next, the other items still waiting and Back to Today. The confirmation is a calm white sheet
 * with the check, not a green band; the facts and the way on sit side by side on a desk (below), so
 * Back to Today comes up into the window. */
.sent-head {
    border-color: var(--rule);
    border-left: 1px solid var(--rule);
    background: var(--sheet);
}

.sent-head .crumbs a {
    color: var(--link);
}

.sent-title-row {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

.sent-title-row h1 {
    margin: 0;
    color: var(--ink);
    font-size: 1.875rem;
}

.sent-title-row .icon {
    width: 1.75rem;
    height: 1.75rem;
    color: var(--ok);
}

/* The status line in the text's weight, so "Email sent" alone leads (design elevation U18). */
.sent-line {
    margin: 0.5rem 0 0.25rem;
    color: var(--ink);
    font-family: var(--font-text);
    font-size: 1rem;
    font-weight: 400;
}

/* When it went: the Sent chip (the send's record, settled, so its icon in the muted ink and the
 * title's check the one in colour), the time in ink, then whom to. A sentence keeps one face. */
.sent-when {
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

.sent-when .chip {
    margin-right: var(--s-1);
}

.sent-when .chip::before {
    background: var(--muted);
}

.sent-when .when {
    color: var(--ink-2);
}

.sent-when a,
.sent-next-list a,
.others-more a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-weight: 650;
}

/* A link inside a sentence keeps its 44px target in padding, so the sentence keeps its own leading. */
:is(.sent-when, .sent-next-list) a {
    display: inline;
    min-height: 0;
    padding-block: 0.625rem;
}

.sent-lease h2 {
    margin-bottom: var(--s-1);
}

/* The lease's name heading its card is the way to the lease page, on a line of its own: a 44px
 * target (UI V2 plan U15). */
.sent-lease h2 > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* The lease's facts are a ledger at every width: on a phone the label and its value share a line
 * (a narrow label column), so What's next comes up the screen (design elevation U18). */
.sent-lease .fact-row {
    grid-template-columns: 7.5rem minmax(0, 1fr);
    align-items: baseline;
}

.sent-lease .fact-value time {
    font-family: var(--font-serif);
    font-weight: 600;
    font-variant-numeric: lining-nums tabular-nums;
}

/* What's next is guidance, not a record, so it stands on the paper: three sheets, not four (the
 * card kit, design elevation U18). */
.card.sent-next {
    padding: 0;
    background: none;
    border: 0;
}

.sent-next-list,
.others-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.sent-next-list li {
    padding: 0.5rem 0;
}

.check-in-line {
    padding-top: var(--s-2);
}

/* One other item: the client (a link to its screen, in ink) and what it asks, its Today group's chip
 * on one edge, right-aligned (design elevation U18). On a phone the chip sits on the name's line and the
 * task under them; from 720px the waiting list is a ledger: a fixed name column, the task on one edge,
 * the chips in a column of their own at the end. */
.other-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 0 var(--s-3);
    padding: 0.25rem 0 var(--s-2);
    border-bottom: 1px solid var(--rule);
}

.other-item > .chip {
    grid-row: 1;
    grid-column: 2;
    justify-self: end;
}

.other-name {
    font-weight: 650;
}

.other-name a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    color: var(--ink);
    text-decoration: none;
}

.other-name a:hover,
.other-name a:focus-visible {
    color: var(--navy-strong);
    text-decoration: underline;
}

.other-what {
    grid-column: 1 / -1;
    color: var(--muted);
    font-size: var(--fs-small);
}

/* From 720px the lease's facts and the waiting list share one second column (design elevation U18). */
@media (min-width: 720px) {
    .other-item {
        grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) 10rem;
        column-gap: var(--s-4);
        padding-bottom: 0.25rem;
    }

    .other-what {
        grid-row: 1;
        grid-column: 2;
    }

    .other-item > .chip {
        grid-column: 3;
    }

    .sent-lease .fact-row {
        grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    }
}

.others-more {
    margin: var(--s-2) 0 0;
}

.sent-back {
    margin: 0 0 var(--s-6);
}

.sent-back .btn {
    min-width: 12rem;
}

/* The Email sent picture (docs/design/art/spots.toml, "email-sent"; design elevation U29): a small
 * vignette at the head's far end, beside the check and its lines, from 1024px, where the head runs the
 * page's width. Narrower it is hidden, and being lazy it is never fetched there. The words keep the top
 * rows and the last row takes up the picture's extra height, so a picture taller than the words never
 * spreads them apart. */
.art-email-sent {
    display: none;
}

@media (min-width: 1024px) {
    .sent-head:has(> .art-email-sent) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-rows: repeat(4, auto) 1fr;
        column-gap: var(--s-8);
    }

    .sent-head:has(> .art-email-sent) > * {
        grid-column: 1;
    }

    .sent-head > .art-email-sent {
        display: block;
        grid-column: 2;
        grid-row: 1 / -1;
        width: 15rem;
    }

    /* On a desk: the head across, then the lease's facts beside What's next with Back to Today right
     * under it (design elevation U18: the way on follows its explanation), then the waiting list. The
     * lease spans What's next's row and Back to Today's flexible one, so that row is only as tall as
     * the lease needs and the button never floats. The page's own order is kept. */
    .site-main:has(> .sent-head) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 22rem;
        grid-template-rows: auto auto 1fr auto;
        column-gap: var(--section-gap);
        align-items: start;
    }

    .site-main:has(> .sent-head) > * {
        grid-column: 1 / -1;
    }

    .site-main:has(> .sent-head) > .sent-lease {
        grid-column: 1;
        grid-row: 2 / span 2;
    }

    /* What's next's heading stands level with the lease's (the sheet's edge and padding above it, the
     * lease's 44px heading line). */
    .site-main:has(> .sent-head) > .sent-next {
        grid-column: 2;
        grid-row: 2;
        padding-top: calc(1.25rem + 1px);
    }

    .site-main:has(> .sent-head) > .sent-next h2 {
        display: flex;
        align-items: center;
        min-height: var(--target);
        margin-bottom: var(--s-1);
    }

    .site-main:has(> .sent-head) > .sent-back {
        grid-column: 2;
        grid-row: 3;
    }

    .site-main:has(> .sent-head) > .sent-others {
        grid-column: 1;
        grid-row: 4;
    }

    /* The lease and the waiting list are sections of one sheet, a hairline between (U18: two sheets,
     * not four). The lease fills its cell, so the sheet stays whole when the rail beside it is the
     * taller. */
    .site-main:has(> .sent-head) > .sent-lease {
        align-self: stretch;
        margin-bottom: 0;
        border-bottom: 0;
        border-bottom-right-radius: 0;
        border-bottom-left-radius: 0;
    }

    .site-main:has(> .sent-head) > .sent-others {
        border-top-color: var(--rule);
        border-top-right-radius: 0;
        border-top-left-radius: 0;
    }

    .sent-back .btn {
        width: 100%;
    }
}

/* ------------------------------------------------------------------ 12d. sign-in, two-factor, errors */

/* The pages a person sees before they are signed in, and the error pages: one white card in the middle
 * of the soft ground (section 7 centres .card.narrow), the brand above it and nothing else. The ways on
 * under a card ("Use a recovery code", "Back to Today") are link-line targets (section 11). */
.page-auth {
    background-color: var(--paper);
}

.page-auth .site-main {
    padding-top: var(--s-8);
}

/* One hairline panel, narrower than the other narrow cards. On a phone it keeps a card's own 16px
 * inside, so the fields and the help take the width; from 720px it is roomier than a card. */
.page-auth .card.narrow {
    max-width: 27rem;
}

@media (min-width: 720px) {
    .page-auth .site-main {
        padding-top: var(--s-18);
    }

    .page-auth .card.narrow {
        padding: 2rem;
    }
}

/* No accent over a card's heading: the serif heading is the ornament, a step under a page's h1 so the
 * brand above still leads. */
.page-auth .card.narrow h1 {
    margin-bottom: 1.5rem;
    font-size: 1.5rem;
}

/* The sign-in card's tagline is its heading's lede, the page's quiet voice: in the text face at body
 * size, so "Sign in" is the card's one serif line, tight under it, and the form a section's step below. */
.page-auth .card.narrow h1:has(+ .auth-tagline) {
    margin-bottom: 0;
}

.auth-tagline {
    margin: var(--s-1) 0 var(--s-8);
    color: var(--muted);
}

/* Label to field 8px, field to field 24px. The button takes the fields' width, so the form is one
 * column of one width, and stands a step further from the last field than the fields from each other,
 * so it reads as the form's end. */
.page-auth .stack-form label {
    font-weight: 500;
}

.page-auth .stack-form input + label {
    margin-top: 1rem;
}

.page-auth .stack-form .btn {
    width: 100%;
    margin-top: 1.5rem;
}

.page-auth .card.narrow > .muted.small {
    max-width: 22rem;
    margin-top: 0.5rem;
    font-size: var(--fs-meta);
    line-height: 1.45;
}

/* The one-time code is typed on a phone from another app: big, centred, easy to check. */
.code-input {
    font-size: 1.75rem;
    letter-spacing: 0.3em;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* The recovery codes: one quiet tile each on the soft ground, two to a row where the card allows. */
.code-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
    gap: var(--s-2);
    list-style: none;
    padding: 0;
}

.code-list li {
    padding: var(--s-2) var(--s-3);
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    text-align: center;
}

.code-list code {
    font-size: 1.125rem;
    letter-spacing: 0.06em;
}

/* The sign-in picture's slot (docs/design/art/spots.toml, "sign-in"): art("sign-in") comes first in
 * login.html, and on a phone or a narrow window it is hidden, and being lazy never fetched. From 1280px
 * Matt's picture and the card are one sheet in the middle of the page, one hairline around both: the
 * picture is its first third (15rem), flush to the sheet's edges and cropped to the card's height, and
 * the empty outer columns balance each other. The sheet sits a little above the middle of the space
 * between the header and the footer. A picture that fails to load leaves that third as the ground. */
.art-sign-in {
    display: none;
}

@media (min-width: 1280px) {
    .page-auth .site-main:has(> .art-sign-in) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto auto minmax(0, 1fr);
        align-content: center;
        padding-top: var(--s-8);
        padding-bottom: var(--s-18);
    }

    .page-auth .site-main:has(> .art-sign-in) > * {
        grid-column: 3;
    }

    /* The card keeps its full width, not the narrower one its words would ask of an auto column, and
     * its left edge is the picture's. */
    .page-auth .site-main:has(> .art-sign-in) > .card.narrow {
        grid-row: 1;
        width: 27rem;
        margin: 0;
        border-left-width: 0;
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
    }

    /* No height of its own, so the card alone sizes the row; then the row's full height, inside the
     * sheet's hairline. The copies stay 400 and 800 wide: covering the card's height, the picture is
     * drawn wider than its 15rem window. */
    .page-auth .site-main > .art-sign-in {
        display: block;
        grid-column: 2;
        grid-row: 1;
        width: 15rem;
        height: 0;
        min-height: 100%;
        border: 1px solid var(--rule);
        border-right-width: 0;
        border-radius: var(--radius-sheet) 0 0 var(--radius-sheet);
        object-fit: cover;
    }
}

/* ------------------------------------------------------------------ 12e. technical details */

/* The ids, hashes and actors that Matt reconciles against: kept in the page, folded away, and quiet
 * when opened. The summary is the 44px details row from section 8 in small muted type; the lines
 * under it are small and muted too, indented to sit under the summary's words. A line that belongs
 * to a dated row starts with that row's written date, so the two can be matched. */
.tech {
    margin: var(--s-3) 0 0;
}

.tech > summary {
    color: var(--muted);
    font-size: var(--fs-small);
}

.tech[open] > summary {
    margin-bottom: var(--s-1);
}

.tech-lines {
    margin: 0 0 var(--s-2);
    padding: 0 0 0 calc(1.1rem + var(--s-2));
    list-style: none;
    color: var(--muted);
    font-size: var(--fs-small);
}

.tech-lines li {
    margin-bottom: var(--s-1);
    overflow-wrap: anywhere;
}

/* The "How this was read" disclosure on a document page, the technical block's neighbour: the model,
 * the cost and the request count are there for whoever asks, and out of the way for whoever does not. */
.how-read {
    margin: var(--s-3) 0 var(--s-4);
}

.how-read[open] > summary {
    margin-bottom: var(--s-2);
}

/* ------------------------------------------------------------------ 12f. opportunity page */

/* The status header: the title, the two chips and the jump link sit in the page head's action row, so
 * on a phone the title, the chips, the premises and the expiry and the link are all on the first screen.
 * It stands on the paper (layout pass U3) in a wrapper of its own, with the print-only line and the
 * page's lines, one section gap above the term's facts (the shared rules in 11b reach no further
 * than a head that is the page's own child); a status line sits --s-4 under the head (11b). The chips
 * share a row and "Log an outcome" takes the row under them: on a phone the row wraps that way (at
 * 320 a long status sends the second chip to a row of its own, never a chip's words to two lines),
 * and from 400px, where all three could fit on one line, a small grid keeps it so (the query below). */
.opp-head {
    margin-bottom: var(--section-gap);
}

.opp-head .page-head-actions {
    margin-top: var(--s-3);
}

/* A list of dated rows: the tile on the left, a sentence and its small print on the right. The
 * ids, actors and raw names are not here; they are in the closed technical block after the list. */
.activity {
    margin: var(--s-3) 0 0;
    padding: 0;
    list-style: none;
}

.activity-row {
    display: grid;
    grid-template-columns: 3.25rem minmax(0, 1fr);
    gap: var(--s-4);
    align-items: start;
    padding: var(--s-3) 0;
    border-top: 1px solid var(--rule);
}

.activity-row:first-child {
    padding-top: 0;
    border-top: 0;
}

.activity-body > p {
    margin: 0;
}

.activity-text {
    font-weight: 600;
}

/* A row that opens a record ("Alert sent to Nick") is a 44px target that keeps the row as it was: its
 * box reaches up into the row's top padding, and its words stay on the sentence's line. */
.activity-text > .cell-link {
    align-items: flex-end;
    margin-top: calc(1lh - var(--target));
}

.activity-when {
    color: var(--muted);
    font-size: var(--fs-small);
}

.activity-body > .activity-extra {
    margin-top: var(--s-1);
    font-size: var(--fs-compact);
}

/* The facts carried into a renewed term: the fact, then its two buttons. */
.carried {
    margin: var(--s-3) 0 0;
    padding: 0;
    list-style: none;
}

.carried-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-6);
    padding: var(--s-3) 0;
    border-top: 1px solid var(--rule);
}

.carried-row:first-child {
    padding-top: 0;
    border-top: 0;
}

.carried-fact {
    flex: 1 1 16rem;
    margin: 0;
    overflow-wrap: anywhere;
}

.carried-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
}

/* Forms on this page read at a comfortable line length instead of the width of the page. Fields in a
 * row line up by their labels, not their bottoms: a help line under one field must not push its
 * neighbour's label out of line. */
.opp-form {
    max-width: 40rem;
}

@media (min-width: 400px) {
    .opp-form .field-row {
        align-items: flex-start;
    }

    /* The head's two chips on the first row, each its own width, and "Log an outcome" alone on the
     * next, all start-aligned (layout pass U8). The chips stay chips. */
    .opp-head .page-head-actions {
        display: grid;
        grid-template-columns: repeat(2, auto);
        justify-content: start;
        justify-items: start;
        align-items: center;
    }

    .opp-head .page-head-actions > .btn {
        grid-column: 1 / -1;
    }
}

/* A date and its time are one answer, so the log and close forms put them side by side in one layout
 * at every width (UI review 2026-10-29, V-33: the log form's help line pushed its time under the date
 * at 1440, and at 390 both forms stacked them). Each field gives up width, down to what a date
 * needs, before the pair wraps (at 320); the log form's help line runs under both. A time input has
 * no constraint the browser could judge invalid, so its help line never needs .field's message slot. */
.opp-form .when-pair {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
}

.when-pair > .field {
    flex: 1 1 8.5rem;
    max-width: var(--field-sm);
}

.when-pair > .field-help {
    flex-basis: 100%;
}

/* "If lost: why" only matters for a lost outcome, so it appears when that option is chosen. The server
 * still refuses a lost outcome without a reason, so a browser that cannot do :has() loses nothing but
 * the field's late appearance: it stays hidden and the 400 says why. It follows .field on purpose. */
.field-when-lost {
    display: none;
}

.opp-form:has(select[name="outcome"] option[value="lost"]:checked) .field-when-lost {
    display: flex;
}

/* "Close this deal" is the one irreversible action, so it stays closed until wanted, with a line saying
 * it is permanent, and its button is the secondary kind with a strong border: the navy primary belongs
 * to "Log an outcome" alone. A blocked or busy button keeps its own look. It is a section of the
 * outcome's sheet like the folds under it: its box with a heavy left edge was a box inside a sheet
 * (design elevation U24). */
.close-sheet > summary,
.fold > summary {
    flex-wrap: wrap;
    row-gap: 0;
}

.close-sheet > summary {
    padding: 0;
    text-decoration: none;
}

.close-sheet[open] > summary {
    margin-bottom: var(--s-4);
    border-bottom: 1px solid var(--rule);
}

.close-sheet > summary h2,
.fold > summary h2 {
    font-size: var(--fs-h3);
}

/* The line under a sheet's title, indented to sit under the title and not the chevron. */
.sheet-note {
    flex: 1 1 100%;
    padding-left: calc(1.1rem + var(--s-2));
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 400;
}

.close-sheet .btn-secondary:not(.is-busy, [aria-disabled="true"]) {
    border: 2px solid var(--ink);
}

/* "Edit an outcome" and "Client tier": quiet sheets (the alert page's .act-on look), one form per
 * outcome in the first. */
.edit-form {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.edit-form h3 {
    margin: 0;
}

/* ------------------------------------------------------------------ 12g. Today */

/* Today (UI V2 plan U5): the banner (11c), then the groups beside Recent activity from 1024px, under
 * it below that. The groups are sections of one white sheet, each opened by its summary (the
 * group_header macro): the icon, the title in the serif, the count and a chevron that turns when the
 * group opens. A folded group's header also names its first clients. Rows are a line each: a date
 * tile when the work has a date (from 720px every row keeps the tile's column; on a phone the tile
 * sits beside the name), who and where (an "Overdue" chip beside the name), what to do and why, then
 * the one main button, navy only in the urgent groups; quieter actions sit under the words.
 * Every action is a 44px target. Groups and Recent activity are the section gap apart: on a phone
 * through the last group's own margin (no row gap adds to it), from 1024px as the column gap. */
.today-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    column-gap: var(--section-gap);
}

.today-main {
    min-width: 0;
}

/* The groups are sections of one white sheet (Matt's choice, design elevation U14), not a card each:
 * a hairline between them, the sheet's corners rounded only at its two ends and the section gap under
 * its last group. Any run of groups is one sheet: Nick's, Matt's own, and Nick's list under Matt's. */
.today-group {
    margin: 0;
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-bottom-width: 0;
    box-shadow: none;
}

.today-group:not(.today-group + .today-group) {
    border-top-left-radius: var(--radius-sheet);
    border-top-right-radius: var(--radius-sheet);
}

.today-group:not(:has(+ .today-group)) {
    margin-bottom: var(--section-gap);
    border-bottom-width: 1px;
    border-bottom-left-radius: var(--radius-sheet);
    border-bottom-right-radius: var(--radius-sheet);
}

/* The summary is the header: no link underline and no chevron of its own (the heading carries it).
 * White in every group with no rule under it (design elevation U24: the hairline between groups
 * already parts them), the title a step over the rows' client names and its count right after it. */
.today-group > .today-group-head {
    display: block;
    min-height: var(--target);
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--ink);
    text-decoration: none;
}

.today-group > .today-group-head::before {
    content: none;
}

/* The title takes the fold title's face here, whatever its level: Nick's groups are h2s, which the
 * fold-title rule (8) reaches, and his groups on Matt's Today are h3s under "Nick's list", which it
 * doesn't (UI review 2026-10-29 V-40). */
.today-group-head .group-head {
    display: grid;
    grid-template-columns: 1.25rem minmax(0, max-content) minmax(0, 1fr) 1rem;
    align-items: center;
    column-gap: var(--gutter-gap);
    min-height: 3rem;
    padding: 0 var(--inset);
    background: transparent;
    color: var(--ink);
    font-family: var(--font-text);
    font-size: var(--fs-h2);
    font-weight: 650;
    letter-spacing: 0;
}

/* A section after the first opens with a little more air above its title than a row has. */
.today-group + .today-group > .today-group-head .group-head {
    min-height: 3.5rem;
}

.today-group-head .group-head > .icon {
    justify-self: center;
    width: 1.25rem;
    height: 1.25rem;
    color: var(--muted);
}

.today-group-head .group-count {
    justify-self: start;
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.today-group-head .group-head::after {
    content: "";
    flex: none;
    width: 1rem;
    height: 1rem;
    background: var(--muted);
    -webkit-mask: var(--icon-chevron) center / contain no-repeat;
    mask: var(--icon-chevron) center / contain no-repeat;
    transform: rotate(-90deg);
}

.today-group[open] > .today-group-head .group-head::after {
    transform: none;
}

@media (prefers-reduced-motion: no-preference) {
    .today-group-head .group-head::after {
        transition: transform 140ms ease;
    }
}

/* The headers' only colour: the urgent and action groups' icon, title and chevron take their tone.
 * Every other group's icon, and every count, is the quiet grey. */
.today-group:is(.tone-urgent, .tone-action) .group-head,
.today-group:is(.tone-urgent, .tone-action) .group-head > .icon {
    color: var(--tone);
}

.today-group:is(.tone-urgent, .tone-action) .group-head::after {
    background: var(--tone);
}

/* Folded, the header names the group's first clients ("and 2 more"), on the text edge under the
 * title; open, its rows say it. */
.group-names {
    display: block;
    margin-top: -0.375rem;
    padding: 0 var(--inset) 0.75rem calc(var(--inset) + 1.25rem + var(--gutter-gap));
    color: var(--muted);
    font-size: var(--fs-small);
}

.today-group[open] .group-names {
    display: none;
}

.today-group > .alert {
    margin: var(--s-3) var(--s-3) 0;
}

.todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* On a phone a row's words run the sheet's width (design elevation U24): the date tile sits beside
 * the client's name and where, and what to do, the button and the quiet links span under them, so an
 * undated row keeps no empty gutter. The body's own box steps aside (display: contents) for its pieces
 * to join the row's grid. From 720px the tile has a gutter of its own (below). The inset is the row's
 * own, so the hairline between rows runs the sheet's width. */
.todo-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--s-3);
    padding: 1rem var(--inset);
    border-top: 1px solid var(--rule);
}

.todo-row:first-child {
    border-top: 0;
}

.todo-tile {
    padding-top: 0.125rem;
}

.todo-tile .dtile-wrap,
.todo-tile .dtile {
    width: 100%;
}

/* The tile's date in words for a screen reader, where the row's own words don't say it. */
.todo-tile-date {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.todo-body {
    display: contents;
}

.todo-who {
    grid-column: 2;
    min-width: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 0.5rem;
    margin-bottom: 0;
}

/* On a phone the name takes its line, so the Overdue chip always sits under it, never beside a
 * short name and under a long one (UI review polish U5); from 720px it sits beside the name. */
.todo-client {
    flex-basis: 100%;
    margin: 0;
    font-family: var(--font-serif);
    font-size: var(--fs-reading);
    font-weight: 600;
    line-height: 1.3;
}

/* The client's name opens the row's own screen. Its line is short, so the link's padding (taken
 * back by the negative margin, which leaves the layout as it is) makes it a 44px target. */
.todo-client a {
    display: inline-block;
    padding: 0.7rem 0;
    margin: -0.7rem 0;
    color: var(--ink);
    text-decoration: none;
}

.todo-client a:hover {
    text-decoration: underline;
}

.todo-where {
    flex-basis: 100%;
    margin: 0.125rem 0 0;
    color: var(--muted);
    font-size: var(--fs-meta);
}

/* An undated row's name, and every row's words under the name, span the row on a phone. */
.todo-row:not(:has(> .todo-tile)) .todo-who {
    grid-column: 1 / -1;
}

.todo-say {
    grid-column: 1 / -1;
    margin: 0.5rem 0 0;
    line-height: 1.45;
}

.todo-detail,
.todo-warn {
    grid-column: 1 / -1;
    margin: 0.125rem 0 0;
    font-size: var(--fs-meta);
}

.todo-detail {
    color: var(--muted);
}

/* A date in a row's words never breaks across two lines. */
.todo-body time {
    white-space: nowrap;
}

/* What must happen before the primary can work (a contact, the mailbox): the action tone with the
 * warning icon, so the cue is never the colour alone. */
.todo-warn {
    color: var(--action);
    font-weight: 600;
}

.todo-warn::before {
    content: "";
    display: inline-block;
    width: 1.05em;
    height: 1.05em;
    margin-right: var(--s-1);
    vertical-align: -0.18em;
    background: currentcolor;
    -webkit-mask: var(--icon-warning) center / contain no-repeat;
    mask: var(--icon-warning) center / contain no-repeat;
}

.todo-main {
    grid-column: 1 / -1;
    margin-top: 0.75rem;
}

.todo-main .inline-form {
    display: block;
}

/* The quiet line steps left by a quiet action's side padding, so each line's first link starts its
 * words on the text's edge while its 44px target keeps the padding. The quiet links keep their 44px
 * boxes, which reach into the row's padding instead of adding height. */
.todo-quiet {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0 1rem;
    margin: -0.5rem 0 -0.75rem calc(-1 * var(--s-1));
}

.todo-row:has(> .todo-main) > .todo-quiet {
    margin-top: 0.25rem;
}

/* A quiet action looks like a link whether it is a link or a form's button: a step under the text, and
 * a hairline underline tells it from the muted hints beside it, which a phone has no pointer to find
 * out. The pointer or the keyboard inks it. */
.todo-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    padding: 0 var(--s-1);
    border: 0;
    background: none;
    color: var(--ink-2);
    font: inherit;
    font-size: var(--fs-meta);
    text-decoration: underline;
    text-decoration-color: var(--rule-strong);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.18em;
    cursor: pointer;
}

.todo-link:hover,
.todo-link:focus-visible {
    color: var(--ink);
    text-decoration-color: currentColor;
}

.todo-link.is-busy {
    color: var(--muted);
    cursor: progress;
}

/* Stop tracking stays on the rows To do R17 chose, but last and in the muted ink, so leaving the
 * list never reads as loud as the row's own work. Its target is the quiet link's. */
.todo-link--muted {
    color: var(--muted);
}

/* "Lease ended" asks once more: a slip that opens under the row, on a line of its own. Its summary is a
 * quiet action like the links beside it, underlined the same way; its chevron says it opens. */
.todo-confirm > summary {
    padding: 0 var(--s-1);
    color: var(--ink-2);
    font-size: var(--fs-meta);
    text-decoration-color: var(--rule-strong);
}

.todo-confirm > summary:hover {
    color: var(--ink);
    text-decoration-color: currentColor;
}

.todo-confirm[open] {
    flex-basis: 100%;
}

/* The toggle stays on its row when the slip opens (UI review 2026-10-29, V-17: in Next up it dropped
 * under "Yes, still active"). Where the browser can style a fold's content, the open fold steps aside
 * (display: contents) so its summary keeps its place, and the slip alone takes the next line. It is
 * the last thing on its row everywhere it is drawn (Today's quiet line, Next up's answers, a landlord
 * contact's Edit and Remove), so nothing is reordered. A browser without it opens the fold on a line
 * of its own, as above. */
@supports selector(::details-content) {
    .todo-confirm[open] {
        display: contents;
    }

    .todo-confirm[open]::details-content {
        flex-basis: 100%;
    }
}

/* The slip is a group inset under its toggle, not a box in the row (V-17). Its left margin takes back
 * the quiet line's step, so its edge is the text's, and its padding steps in by the chevron and its
 * gap (8), so on a phone the question starts under the toggle's words. */
.todo-confirm-sheet {
    max-width: 34rem;
    margin-top: var(--s-1);
    margin-left: var(--s-1);
    padding: 0 0 var(--s-3) calc(1.1rem + var(--s-2));
}

.todo-confirm-sheet p {
    margin: 0 0 var(--s-3);
}

.todo-confirm-sheet .inline-form {
    display: block;
}

/* Matt's read-only copy of Nick's list, under his own groups. */
.today-others {
    margin-top: var(--s-6);
}

.today-others > h2 {
    margin-bottom: var(--s-1);
}

/* Nothing on the copy is a button, so each client's name, the way to look, is underlined as the
 * link it is (UI review polish U6), in the links' own hairline (section 3; the row's rule set
 * text-decoration none, which reset the line's colour and thickness too). */
.today-others .todo-client a {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: color-mix(in srgb, currentcolor 40%, transparent);
}

/* With Nick's list empty its all-clear line ends the main column, and below 1024px Recent activity
 * follows with no row gap: the line keeps the section gap a group's own margin would. */
.today-others > .empty {
    margin-bottom: var(--section-gap);
}

/* Recent activity: a quiet log, one line per action, the day it was done tight under it; beside the
 * groups it stays in view. Spacing alone parts the entries: the day closes each one. Each line opens
 * its lease, alert or dropped document, the whole line a 44px target: each entry in the second ink two
 * steps under the work's words (design elevation U24: a log, quieter than the rows), no underline
 * until the pointer or the keyboard reaches it. A one-line entry takes
 * the extra height above its words, so the day still sits tight under them. It stays a block (aligned
 * with align-content): a flex or grid line would split its words from the date inside them. */
.today-recent h2 {
    font-size: var(--fs-h3);
}

.recent-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.recent-item {
    display: block;
}

.recent-what {
    display: block;
    align-content: end;
    min-height: var(--target);
    padding: 0.75rem 0 0.125rem;
    color: var(--ink-2);
    font-size: var(--fs-small);
    line-height: 1.4;
}

a.recent-what {
    display: block;
    text-decoration: none;
}

a.recent-what:hover,
a.recent-what:focus-visible {
    color: var(--ink);
    text-decoration: underline;
}

.recent-day {
    padding-bottom: 0.75rem;
    color: var(--muted);
    font-size: var(--fs-meta);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.today-recent .empty {
    padding: 0;
}

@media (min-width: 720px) {
    .today-group > .alert {
        margin: var(--s-4) var(--s-4) 0;
    }

    /* One grid for every row (layout pass U4): an undated row keeps the tile's column too, left
     * empty (no blank tile is drawn), so the words, the quiet links and the buttons start at one
     * place down the whole page. Paper is wider than 720px, so print (16) sets its own areas with
     * selectors at least as specific as this one. A row with no button (Matt's read-only list)
     * leaves the last column empty, and its words take the width. */
    .todo-row {
        grid-template-columns: var(--gutter) minmax(0, 1fr) auto;
        grid-template-areas:
            "tile body main"
            "tile quiet quiet";
    }

    .todo-tile {
        grid-area: tile;
    }

    .todo-body {
        display: block;
        grid-area: body;
        min-width: 0;
    }

    .todo-quiet {
        grid-area: quiet;
    }

    .todo-client {
        flex-basis: auto;
    }

    /* The button stands level with the client's name, so a column of buttons reads as one shape. */
    .todo-main {
        grid-area: main;
        align-self: start;
        margin: 0;
    }

    /* The header's icon in the rows' date gutter, its title on their text edge. */
    .today-group-head .group-head {
        grid-template-columns: var(--gutter) minmax(0, max-content) minmax(0, 1fr) 1rem;
    }

    .group-names {
        padding-left: calc(var(--inset) + var(--gutter) + var(--gutter-gap));
    }

    /* One width for the rows' buttons, so their left edges line up: wide enough for the usual labels.
     * A longer one grows past it (Nick's "Log what happened", Matt's mailbox label). */
    .todo-main .btn {
        min-width: 10rem;
    }

    .todo-row:has(> .todo-main) > .todo-quiet {
        margin-top: -0.25rem;
    }

    /* A "Lease ended" fold that starts the quiet line hangs its chevron in the gutter, so its words
     * start on the text's edge like a link's. */
    .todo-quiet > .todo-confirm:first-child > summary {
        margin-left: calc(-1 * (1.1rem + var(--s-2)));
    }
}

@media (min-width: 1024px) {
    .today-layout {
        grid-template-columns: minmax(0, 1fr) 18rem;
        align-items: start;
    }

    .today-side {
        position: sticky;
        top: 1rem;
    }
}

/* ------------------------------------------------------------------ 12h. Add lease documents */

/* The drop target (drop zone U6) is the browser's own file input stretched over the whole dashed
 * box, so a file dragged anywhere onto the box lands in it; no script. The words sit behind the
 * input, which is transparent apart from its button and the chosen file's name, so the browser
 * still shows what was chosen. The input's focus ring is drawn just inside the box. In the V2 look
 * (UI V2 plan U18) the box is a well in the soft ground, led by the header's "Add a lease" plus,
 * that turns navy under the pointer or the keyboard. */
.drop-zone {
    display: grid;
    border: 2px dashed var(--control);
    border-radius: var(--radius-sheet);
    background: var(--paper);
}

.drop-zone:hover,
.drop-zone:has(input:focus-visible) {
    border-color: var(--navy);
    background: var(--navy-tint);
}

.drop-form .drop-zone-label,
.drop-zone input[type="file"] {
    grid-area: 1 / 1;
}

.drop-form .drop-zone-label {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    align-self: start;
    padding: var(--s-6) var(--s-4) 0;
    color: var(--ink);
    font-weight: 600;
    pointer-events: none;
}

/* the plus is decorative: the label's words name the box */
.drop-form .drop-zone-label::before {
    content: "";
    flex: none;
    width: 1.5rem;
    height: 1.5rem;
    background: var(--navy);
    -webkit-mask: var(--icon-plus) center / contain no-repeat;
    mask: var(--icon-plus) center / contain no-repeat;
}

/* A phone has no file to drag in, so its box says "Choose a lease PDF"; from 720px it says "Drag a
 * lease PDF here, or choose one" (UI review 2026-10-29 V-51). The words out of sight are display:
 * none, so the label's name is always the words shown. */
.drop-zone-drag {
    display: none;
}

@media (min-width: 720px) {
    .drop-zone-drag {
        display: inline;
    }

    .drop-zone-choose {
        display: none;
    }
}

/* The file button sits under the label's words, one line at every width. */
.drop-zone input[type="file"] {
    width: 100%;
    min-height: 10rem;
    padding: 5rem var(--s-4) var(--s-6);
    border: 0;
    background: transparent;
}

.drop-zone input[type="file"]:focus-visible {
    outline-offset: -4px;
}

.drop-help {
    margin: 0;
    padding-left: 1.25rem;
}

/* A file dropped again: where Lease Watch already has it, one line per place. */
.drops-already p,
.drops-already ul {
    margin: 0;
}

.drops-already ul {
    margin-top: var(--s-2);
    padding-left: 1.25rem;
}

/* The waiting list restacks like any table. A row's chip says its state in Today's tone and the
 * line under it the rest; its file name is a 44px target (cell-link, section 11). Nothing in it
 * animates, because the list is polled and replaced while a drop is read. */
.drop-detail {
    display: block;
    margin-top: var(--s-1);
    color: var(--ink-2);
}

.drop-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2);
}

/* ------------------------------------------------------------------ 12i. filing card */

/* The filing card (drop zone U7) is one form. Each destination radio sits in a block with its
 * reason; what is only that destination's (the sentence saying what Confirm does, the term, the
 * alert warning, the "already working on this deal" tick) opens under it once it is chosen. The
 * sections below it (the new client, the new lease's premises, the people) and the primary buttons
 * follow the chosen radio's value: "new", "client:<id>" (a new lease for that client) or
 * "lease:<id>". Every rule that hides something needs :has(), so a browser without it drops the
 * rule and shows the whole card; the server reads only the chosen destination's fields either way.
 * Matt's read-only card (.drop-card--readonly) hides nothing: the server leaves out what doesn't
 * apply. */
.drop-card--readonly,
.card-rest {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

/* Fields in a row line up by their labels, as on the deal page (.opp-form): a "Checked" chip and
 * "Show lease wording" under one field must not push its neighbours down. Find keeps its own
 * bottom alignment (8b), level with its box, whose help line sits under the whole row. */
@media (min-width: 400px) {
    .drop-card .field-row {
        align-items: flex-start;
    }
}

.dest-find .field-help {
    margin-top: var(--s-2);
}

.card-summary {
    margin: 0;
    color: var(--ink);
    font-weight: 600;
}

/* A drop still being read says so on the paper, under its head (layout pass U3), at the gap a status
 * line keeps under a head. */
.drop-reading {
    margin-top: var(--s-4);
}

.drop-reading p,
.drop-held p,
.drop-unread p,
.not-a-lease p {
    margin: 0;
}

.drop-held p + p,
.drop-unread form,
.drop-reading p + p {
    margin-top: var(--s-2);
}

.not-a-lease-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-4);
    margin: var(--s-2) 0;
}

/* Where it goes is a section of the card, not a box inside it: its legend reads as the section
 * headings under it do (The new client, People), and its options take the card's full width. The
 * client tier is a field of the new client, not a box in the card either (layout pass U8). Neither
 * fieldset has a border, so neither legend has a notch to sit in. */
.drop-card .card-choices,
.drop-card .dest {
    padding: 0;
    border: 0;
}

.dest > legend,
.dest > h3 {
    margin: 0 0 var(--s-2);
    padding: 0;
    color: var(--ink);
    font-family: var(--font-serif);
    font-size: var(--fs-h3);
    font-weight: 600;
    line-height: 1.2;
}

/* The destinations are rows of the card parted by hairlines, not boxes inside it (design elevation
 * U24: a tinted box holding a warning box read as boxes in a card). */
.dest-list {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--rule);
}

.dest-list--found {
    margin-top: var(--s-2);
}

.dest-option {
    padding: 0 var(--s-3);
    border-bottom: 1px solid var(--rule);
}

/* The chosen destination is marked as the fact list marks its chosen fact: a navy rule down its left
 * edge, a shape that reads without colour (forced colours outline it instead, section 15). */
.dest-option:has(.dest-radio:checked) {
    box-shadow: inset 4px 0 0 var(--navy);
}

.dest-text {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
}

/* The "Suggested" chip is as wide as its word, not the option's widest line. */
.dest-text > .chip {
    align-self: flex-start;
}

.dest-label {
    font-weight: 650;
}

.dest-reason,
.dest-note,
.dest-waiting,
.dest-found {
    color: var(--ink-2);
    font-size: var(--fs-small);
}

.dest-waiting,
.dest-found {
    margin: var(--s-2) 0 0;
}

/* From 560px what is only the chosen destination's lines up under its words, past the radio. On a
 * phone it takes the option's whole width: indented, the hold warning kept 177px for its words at
 * 390 and 130 at 320 (UI V2 plan U15). */
.dest-more {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: 0 0 var(--s-3);
}

@media (min-width: 560px) {
    .dest-more {
        padding-left: 2rem;
    }
}

/* The block's own gap spaces its lines (the sentence, the notes, the warning, the tick). */
.dest-more > p {
    margin: 0;
}

.dest-original {
    color: var(--ink-2);
}

.dest-summary {
    font-weight: 600;
}

.dest-find {
    margin-top: var(--s-2);
}

/* "Needs you" lists the values Nick must type or check himself: the missing tone and its question
 * mark, as the fact list's "Needs your input". The rule down its left edge is a border, so it stays
 * in forced colours and on paper. */
.card-needs {
    padding: var(--s-3) var(--s-4);
    border-left: 4px solid var(--missing);
    border-radius: var(--radius);
    background: var(--missing-bg);
}

.card-needs h3,
.scan-check p {
    margin: 0;
}

.card-needs h3 {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    color: var(--missing);
}

.card-needs h3::before {
    content: "";
    flex: none;
    width: 1.1em;
    height: 1.1em;
    background: currentcolor;
    -webkit-mask: var(--icon-question) center / contain no-repeat;
    mask: var(--icon-question) center / contain no-repeat;
}

.needs-list {
    margin: var(--s-2) 0;
    padding-left: 1.25rem;
}

.dest-section {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding-top: var(--s-4);
    border-top: 1px solid var(--rule);
}

.dest-section h3 {
    margin: 0;
}

/* A box that stands alone in its section (the client tier) keeps a narrow form's width on a desk
 * instead of spanning the card; boxes in a row (the premises, a person) share it. */
.drop-card--edit .dest-section > :is(.card-field, .card-choices) {
    max-width: var(--field-lg);
}

/* The new client's values come in pairs (layout pass U8): the client's name with its practice type,
 * then the tenant with its permitted use, one under the other at every width (finishing pass U8),
 * each value no wider than a box alone. The visual order is the source order, and a pair the reading
 * found one value of keeps it alone. Matt's card has the same rows (card-values). */
.drop-card .card-pair {
    display: grid;
    grid-template-columns: minmax(0, var(--field-lg));
    gap: var(--s-2);
}

/* From 560px each fact is a row of a ledger (11c's value rows; finishing pass U8, U24-17, in place
 * of the pairs side by side from 720px): every row of values and every pair stacks, one fact under
 * another and a step further apart than a fact's own lines, so the wording under a box stays with
 * it; a section's pairs part by the same step. Find's row holds no value rows: its button stays
 * beside its box. */
@media (min-width: 560px) {
    .drop-card :is(.field-row, .card-values):has(> .value-row) {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s-6);
    }

    .drop-card .dest-section {
        gap: var(--s-6);
    }
}

/* Each person is a part of the People section under a rule, not a box (design elevation U24): the
 * fieldset keeps its meaning and loses the form system's border. */
.drop-card .person-row {
    padding: 0;
    border: 0;
}

.drop-card .person-row > legend {
    padding: 0;
    color: var(--ink-2);
    font-size: var(--fs-small);
    font-weight: 650;
}

.person-row + .person-row {
    margin-top: var(--s-4);
    padding-top: var(--s-3);
    border-top: 1px solid var(--rule);
}

.person-row > h4 {
    margin: 0 0 var(--s-1);
}

.person-known {
    margin: 0;
    color: var(--ink-2);
}

/* A value shown as text: Matt's whole card, and the values Nick's card only shows. Matt's card has
 * no form, so it uses none of the form system's classes (8b); its rows simply wrap. */
.card-value {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
}

.card-values {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3) var(--s-6);
}

/* A field's name: the label over a value shown as text, and the client tier's legend, which still
 * names its radios, flush over them, and reads as the other fields' labels do (as an assignment's
 * buyer question does, 12k): it is a field's name here, not a section's. */
.card-choices > legend,
.value-label {
    margin: 0;
    padding: 0;
    color: var(--ink-2);
    font-size: var(--fs-compact);
    font-weight: 600;
}

/* A value's check rides on its label's line (layout pass U8): the label, then its chip ("Verified",
 * "Not sure: choose it"), which wraps under it when the column is narrow. The chip is the label's
 * sibling, never inside it, so it is no part of the box's name. A scan's "Check page N", "Changed by
 * you" and "Show lease wording" stay under the box, where the tab order had them. From 560px the head
 * sits beside its value (11c's value rows, finishing pass U8). */
.value-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-1) var(--s-2);
}

.value-text,
.value-check {
    margin: 0;
}

.card-help {
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

/* "Show lease wording" is small words but a 44px target (it keeps the summary's height, section 8). */
.lease-words > summary {
    font-size: var(--fs-small);
}

.lease-quote {
    margin: var(--s-2) 0;
    padding-left: var(--s-3);
    border-left: 3px solid var(--rule);
    color: var(--ink-2);
}

.lease-words p {
    margin: 0;
}

.card-actions .dest-none-note {
    margin: 0;
    color: var(--muted);
}

/* A long primary label ("File on Kestrel Orchard Veterinary's lease") wraps on a phone: give its
 * lines room and balance them, centred like a one-line label. */
.card-actions .dest-button {
    padding-block: var(--s-2);
    text-align: center;
    text-wrap: balance;
}

/* What follows the chosen destination. One selector list on purpose: a browser that can't read
 * :has() drops all of it and shows the whole card. */
.drop-card--edit .dest-option:not(:has(.dest-radio:checked)) > .dest-more,
.drop-card--edit .dest-option:has(.dest-radio[data-shown]) .dest-summary,
.drop-card--edit:has(.dest-radio:not([data-shown]):checked) .card-summary,
.drop-card--edit:has(.dest-radio:checked) .dest-none-note,
.drop-card--edit:not(:has(.dest-radio:checked)) .for-people,
.drop-card--edit:not(:has(.dest-radio[value="new"]:checked)) .for-new-client,
.drop-card--edit:not(:has(.dest-radio[value^="client:"]:checked)) .for-new-lease,
.drop-card--edit:not(:has(.dest-radio[value^="lease:"]:checked)) .for-lease,
.drop-card--edit:not(:has(.dest-radio:is([value="new"], [value^="client:"]):checked)) .for-premises,
.drop-card--edit:not(:has(.dest-radio:is([value^="client:"], [value^="lease:"]):checked)) .for-existing,
.drop-card--assignment:has(.dest-radio[value^="lease:"]:checked) .for-existing,
.drop-card--not-a-lease:not(:has(input[name="file_anyway"]:checked)) .card-rest {
    display: none;
}

/* ------------------------------------------------------------------ 12j. rent schedule */

/* A rent schedule (lease abstract U3) is one table wherever it shows: the lease summary, a review
 * card, a conflict's period-by-period comparison. It restacks on a phone (9b), so nothing scrolls
 * sideways at 320 or 390px; wide, it is a plain table with figures in tabular digits. A worked-out
 * figure is followed by the word "calculated" and a flagged period by "figures disagree": the words
 * carry the meaning, and the quieter or bolder type only helps. An amount never breaks inside
 * itself, whatever the column around it allows. */
.rent-schedule {
    max-width: 100%;
    margin: var(--s-2) 0 var(--s-3);
}

/* Its figures are in the serif's tabular figures, as every value the fact list and the summary
 * show (no slashed zero); the heads are quiet sentence-case labels over a firm rule (design
 * elevation U17: no tracked capitals). */
.rent-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-serif);
    font-size: 1rem;
    font-variant-numeric: lining-nums tabular-nums;
}

.rent-table th {
    padding: 0 var(--s-3) var(--s-2) 0;
    border-bottom: 1px solid var(--ink-2);
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-meta);
    font-weight: 600;
    text-align: left;
    vertical-align: bottom;
}

.rent-table td {
    vertical-align: top;
}

.rent-period {
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.figure {
    white-space: nowrap;
}

.figure-tag,
.figure-flag {
    display: block;
    font-size: var(--fs-small);
}

.figure-tag {
    color: var(--muted);
    font-style: italic;
    font-weight: 400;
}

.figure-flag {
    color: var(--urgent);
    font-weight: 600;
}

.rent-notes {
    margin: var(--s-2) 0 0;
    color: var(--muted);
    font-size: var(--fs-small);
}

.rent-note {
    display: block;
}

.rent-compared {
    margin: 0 0 var(--s-2);
    font-size: var(--fs-small);
}

.rent-status {
    font-weight: 600;
}

/* The comparison's periods that change something read in bold: a reader scanning for what the
 * later reading does finds them, and the status column says it in words anyway. */
.rent-status-changed .rent-status,
.rent-status-new .rent-status,
.rent-status-missing .rent-status {
    font-weight: 700;
}

.rent-conflict {
    margin: 0 0 var(--s-4);
}

/* A rent conflict leads with one sentence of what the later reading does, at the reading size, then
 * Combine, then the table (polish pass F1): the answer comes before the evidence for it. */
.rent-summary {
    margin: 0 0 var(--s-3);
    font-size: var(--fs-reading);
    line-height: 1.45;
}

.rent-conflict > .combine-form {
    margin: 0 0 var(--s-4);
}

/* While Combine is offered, the two readings fold under "Or use one reading alone", whose summary
 * reads as the fold it is (the app's chevron, details > summary). */
.conflict-alone {
    margin: 0 0 var(--s-2);
}

/* The editor: one fieldset a line. On a phone its period takes the line's width and the four
 * figure boxes sit two by two; from 720px they share one row under the period. Each box fills its
 * column (min-width 0, width 100%), so a line never grows past the form at 320px. The boxes keep
 * the pinned controls rule (16px type, 44px tall); nothing here sets their size. */
.stack-form .rent-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-2) var(--s-3);
    padding: var(--s-2) var(--s-3) var(--s-3);
}

.rent-row--invalid {
    border-color: var(--urgent);
}

.rent-cell {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
}

.rent-cell--period {
    grid-column: 1 / -1;
}

.stack-form .rent-cell input,
.stack-form .rent-cell select {
    width: 100%;
    max-width: 100%;
    align-self: stretch;
}

/* The form's first submit button (so Enter saves), out of sight; the visible Save is below. */
.default-submit {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

@media (min-width: 720px) {
    .stack-form .rent-row {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/* ------------------------------------------------------------------ 12k. deciding an assignment */

/* The choose step and the preview (lease abstract plan U9). The choose step is one form over one sheet
 * (U24): what the document says, who took the lease, the options question, then its actions as the
 * sheet's last section. Mobile first: each value stacks under its label and the reading's chip (from
 * 560px beside them, 11c's value rows), the AI's quote under the box, and every name or quote may wrap
 * anywhere, so a 390px phone never scrolls sideways. Controls keep the pinned rules (16px type,
 * 44px targets); the radios sit in labels the controls rule makes 44px tall. In the V2 look (UI V2
 * plan U19) the buyer and the options question are sections of their cards, not boxes in them; a
 * name's box keeps a narrow form's width on a desk; the chosen buyer is marked as the fact list marks
 * its chosen fact; and "Turn it down instead" is a 44px target. */
.decide-step > h2 {
    margin-top: 0;
}

/* The form holds the sheet, and a flex item's margin never collapses with its form's: the sheet drops
 * its gap so the page ends --s-4 under it, as it did when the steps were the form's own cards. */
.decide-form > .one-sheet {
    margin-bottom: 0;
}

/* The buyer and the options question: their legend reads as a field's label does (section 8), over
 * choices that take the card's width. A new buyer's tier and contact are groups the same way. */
.decide-step > fieldset {
    padding: 0;
    border: 0;
}

/* A name typed here is a line, not a paragraph: on a desk its box stops at a narrow form's width. The
 * cap keeps the 100% the field widths give (a phone's box, field-medium, fits a 320px card). */
.decide-step .field > input[type="text"] {
    max-width: min(100%, var(--field-lg));
}

/* Every value the step shows Verified (UI review polish U2): one quiet line over them, after the
 * chip, and no label carries one. */
.decide-verified {
    margin: 0 0 var(--s-3);
    color: var(--muted);
}

.decide-step .field + .field,
.decide-step .decide-mismatch,
.decide-step .decide-tick,
.decide-step .decide-terms {
    margin-top: var(--s-3);
}

/* The facts to check part by a step larger than a fact's own lines (11c), so a quote's fold reads as
 * the box's above it, not the next fact's (finishing pass U7). */
.decide-step .value-row + .value-row {
    margin-top: var(--s-6);
}

.decide-read {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    color: var(--muted);
    font-size: var(--fs-small);
    overflow-wrap: anywhere;
}

.decide-figure,
.buyer-name {
    color: var(--ink);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.decide-terms p {
    margin: 0 0 var(--s-2);
}

/* The buyers are rows parted by hairlines, as the filing card's destinations are, and the chosen
 * one is marked by the navy rule alone (design elevation U24; forced colours outline it, section 15). */
.buyer-list {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--rule);
}

.buyer-option {
    min-width: 0;
    padding: 0 var(--s-3);
    border-bottom: 1px solid var(--rule);
}

.buyer-option:has(input[name="buyer"]:checked) {
    box-shadow: inset 4px 0 0 var(--navy);
}

.buyer-text {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
    margin: 0;
    padding: var(--s-2) 0;
}

label > .buyer-text {
    padding: 0;
}

.buyer-note {
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 400;
    overflow-wrap: anywhere;
}

/* "A new client" is always the last row: the section's own hairline ends it, so it has no bottom rule
 * to stack above that one (U24). Straight under a list of buyers (Matt's, or after a Find) the list's
 * last rule is the one above it. */
.buyer-option--new {
    margin-top: var(--s-2);
    border-top: 1px solid var(--rule);
    border-bottom: 0;
}

.buyer-list + .buyer-option--new {
    margin-top: 0;
    border-top: 0;
}

.for-new-buyer {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: 0 0 var(--s-3);
}

/* A new client's fields only while "A new client" is chosen. Without :has() the rule drops and
 * they all show; the server reads them only for a new client either way. */
.decide-form:not(:has(input[name="buyer"][value="new"]:checked)) .for-new-buyer {
    display: none;
}

.buyer-find {
    margin-top: var(--s-3);
}

.buyer-find .field-help,
.buyer-found {
    margin-top: var(--s-2);
}

.decide-context dd .decide-read {
    color: var(--ink-2);
}

/* The preview's choices are a ledger as the lease summary is (12b), and open their card: no gap over
 * the first row, which the card's padding already clears (finishing pass U7). */
.card > .summary-list:first-child {
    margin-top: 0;
}

/* The preview: the planner's lines, then the one decision and "Change your choices". */
.plan-list {
    margin: 0 0 var(--s-4);
    padding-left: var(--s-6);
}

.plan-list li {
    margin-top: var(--s-2);
    overflow-wrap: anywhere;
}

.decide-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-4);
}

.stack-form .decide-actions .btn {
    margin-top: 0;
}

/* "Turn it down instead", beside the preview button, is a 44px target as the button is. */
.decide-actions > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* ------------------------------------------------------------------ 12l. Records and settings, search */

/* Records and settings (UI V2 plan U17): a section of one sheet per heading (U24), each link on a line
 * of its own with one plain line under it. The space groups them (U24, round 2): a heading sits close
 * over its first link (its 44px target is the rest of the gap), two links are further apart, two
 * sections further still. The line under the title is the shared explainer (section 11b). */
.records-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.card.records-section > h2 {
    margin-bottom: var(--s-1);
}

.records-list li {
    padding: 0 0 var(--s-4);
}

.records-list li:last-child {
    padding-bottom: 0;
}

.records-list a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-weight: 650;
}

.records-blurb {
    display: block;
    color: var(--muted);
    font-size: var(--fs-small);
}

/* The search page: the field and its button share a row, the button never wraps under it. */
.search-form {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    margin-bottom: var(--s-2);
}

.search-form label {
    font-weight: 600;
    color: var(--ink-2);
}

.search-row {
    display: flex;
    gap: var(--s-2);
}

.search-row input {
    flex: 1 1 auto;
    min-width: 0;
    max-width: var(--field-lg);
}

.search-row .btn {
    flex: none;
}

/* The lines that say why nothing is listed echo the query, which may be one long word; the no-match
 * line's way to Leases is still a 44px target. */
.search-page .empty {
    overflow-wrap: anywhere;
}

.search-page .empty a {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
}

/* While a new search is on its way the old results go quiet (UI review 2026-10-29 V-52): busy.js marks
 * the region the form's data-busy-target names, as it quiets a busy form's other buttons. The change is
 * instant, so nothing moves. */
.search-results[aria-busy="true"] {
    opacity: 0.55;
    cursor: progress;
}

/* Search results (UI V2 plan U12): each lease a row of the results card, a rule between two rows and
 * no box of its own inside the card (layout pass U5). The client's link (a 44px target) with its
 * status chip at the row's end, then the premises and what matched, at every width: on a phone the
 * chip between the name and the address parted them (design elevation U24, round 2). The page keeps
 * to the measure (U24), so on a desk the chip stays near the name it belongs to. */
.search-more {
    margin: calc(-1 * var(--s-2)) 0 var(--s-3);
    color: var(--ink-2);
}

.search-list {
    margin: var(--s-3) 0 0;
    padding: 0;
    list-style: none;
}

.search-result {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    justify-items: start;
    gap: 0 var(--s-3);
}

.search-result .chip {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
}

/* The space is between two rows, around their rule; the card's padding is the space above the first
 * row and under the last. */
.search-result + .search-result {
    margin-top: var(--s-3);
    padding-top: var(--s-3);
    border-top: 1px solid var(--rule);
}

.search-client {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    font-weight: 650;
    overflow-wrap: anywhere;
}

.search-place,
.search-why {
    grid-column: 1 / -1;
    margin: 0;
    color: var(--ink-2);
    font-size: var(--fs-small);
    overflow-wrap: anywhere;
}

.search-place.is-empty {
    color: var(--muted);
}

.search-matched {
    color: var(--ink);
    font-weight: 650;
}

/* ------------------------------------------------------------------ 13. notice timeline */

/* The notice timeline (demo kit U4), drawn to the scale of time by the design elevation (U16). Every
 * state is also said in words; the shapes only repeat it. The card is a container, so the timeline
 * takes the shape its own width allows, whatever the page does around it:
 * - the list runs down a hairline rail on a narrow card, and across in even columns from 44rem;
 * - over the list, a small strip draws the dates to the scale of time, at the view model's whole
 *   percent (data-at, one rule each below; no inline style);
 * - from 58rem, when every step's words fit beside the line (tl-chart--hung), the strip is the line
 *   itself and the steps hang on it at their true places: Deliver by above, the deadline below, so
 *   dates a few business days apart never crowd. A step there is at most 24% of the line wide, the
 *   reach the view model allowed it (_REACH in src/alerts/timeline.py);
 * - where the dates bunch (the notice years away, or only weeks), the view model zooms the line to
 *   the notice: Today or the lease's end sits past a break at the line's end (tl-break).
 * Each date is written once, large, in the serif's figures (the date tiles are gone): the deadline's
 * is the largest and navy, and its mark the one filled navy mark. The reminders are small quiet
 * lines under their milestone. */
.timeline {
    container-type: inline-size;
}

/* The heading and its one-line explanation share a line where they fit. */
.tl-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-1) var(--s-3);
}

.timeline .tl-head > h2,
.timeline .tl-head > p {
    margin: 0;
}

.tl-status {
    margin: var(--s-3) 0 0;
}

/* The "still active?" card says only when its dates come, quietly: Next up above asks the question. */
.timeline--triage .tl-status {
    color: var(--muted);
}

/* A hold or a missing date draws no rail: the card itself says "provisional" (hatched, dashed). */
.card.timeline--hold,
.card.timeline--needs_entry {
    padding: var(--s-4);
    border: 2px dashed var(--control);
    border-radius: var(--radius);
    background: repeating-linear-gradient(135deg, transparent 0 0.6rem, rgb(18 22 28 / 0.035) 0.6rem 1.2rem);
}

/* Places on the scale: the view model's whole percent on the timeline's steps, marks, months and
 * run (data-at), as --at. */
[data-at="0"] { --at: 0%; }
[data-at="1"] { --at: 1%; }
[data-at="2"] { --at: 2%; }
[data-at="3"] { --at: 3%; }
[data-at="4"] { --at: 4%; }
[data-at="5"] { --at: 5%; }
[data-at="6"] { --at: 6%; }
[data-at="7"] { --at: 7%; }
[data-at="8"] { --at: 8%; }
[data-at="9"] { --at: 9%; }
[data-at="10"] { --at: 10%; }
[data-at="11"] { --at: 11%; }
[data-at="12"] { --at: 12%; }
[data-at="13"] { --at: 13%; }
[data-at="14"] { --at: 14%; }
[data-at="15"] { --at: 15%; }
[data-at="16"] { --at: 16%; }
[data-at="17"] { --at: 17%; }
[data-at="18"] { --at: 18%; }
[data-at="19"] { --at: 19%; }
[data-at="20"] { --at: 20%; }
[data-at="21"] { --at: 21%; }
[data-at="22"] { --at: 22%; }
[data-at="23"] { --at: 23%; }
[data-at="24"] { --at: 24%; }
[data-at="25"] { --at: 25%; }
[data-at="26"] { --at: 26%; }
[data-at="27"] { --at: 27%; }
[data-at="28"] { --at: 28%; }
[data-at="29"] { --at: 29%; }
[data-at="30"] { --at: 30%; }
[data-at="31"] { --at: 31%; }
[data-at="32"] { --at: 32%; }
[data-at="33"] { --at: 33%; }
[data-at="34"] { --at: 34%; }
[data-at="35"] { --at: 35%; }
[data-at="36"] { --at: 36%; }
[data-at="37"] { --at: 37%; }
[data-at="38"] { --at: 38%; }
[data-at="39"] { --at: 39%; }
[data-at="40"] { --at: 40%; }
[data-at="41"] { --at: 41%; }
[data-at="42"] { --at: 42%; }
[data-at="43"] { --at: 43%; }
[data-at="44"] { --at: 44%; }
[data-at="45"] { --at: 45%; }
[data-at="46"] { --at: 46%; }
[data-at="47"] { --at: 47%; }
[data-at="48"] { --at: 48%; }
[data-at="49"] { --at: 49%; }
[data-at="50"] { --at: 50%; }
[data-at="51"] { --at: 51%; }
[data-at="52"] { --at: 52%; }
[data-at="53"] { --at: 53%; }
[data-at="54"] { --at: 54%; }
[data-at="55"] { --at: 55%; }
[data-at="56"] { --at: 56%; }
[data-at="57"] { --at: 57%; }
[data-at="58"] { --at: 58%; }
[data-at="59"] { --at: 59%; }
[data-at="60"] { --at: 60%; }
[data-at="61"] { --at: 61%; }
[data-at="62"] { --at: 62%; }
[data-at="63"] { --at: 63%; }
[data-at="64"] { --at: 64%; }
[data-at="65"] { --at: 65%; }
[data-at="66"] { --at: 66%; }
[data-at="67"] { --at: 67%; }
[data-at="68"] { --at: 68%; }
[data-at="69"] { --at: 69%; }
[data-at="70"] { --at: 70%; }
[data-at="71"] { --at: 71%; }
[data-at="72"] { --at: 72%; }
[data-at="73"] { --at: 73%; }
[data-at="74"] { --at: 74%; }
[data-at="75"] { --at: 75%; }
[data-at="76"] { --at: 76%; }
[data-at="77"] { --at: 77%; }
[data-at="78"] { --at: 78%; }
[data-at="79"] { --at: 79%; }
[data-at="80"] { --at: 80%; }
[data-at="81"] { --at: 81%; }
[data-at="82"] { --at: 82%; }
[data-at="83"] { --at: 83%; }
[data-at="84"] { --at: 84%; }
[data-at="85"] { --at: 85%; }
[data-at="86"] { --at: 86%; }
[data-at="87"] { --at: 87%; }
[data-at="88"] { --at: 88%; }
[data-at="89"] { --at: 89%; }
[data-at="90"] { --at: 90%; }
[data-at="91"] { --at: 91%; }
[data-at="92"] { --at: 92%; }
[data-at="93"] { --at: 93%; }
[data-at="94"] { --at: 94%; }
[data-at="95"] { --at: 95%; }
[data-at="96"] { --at: 96%; }
[data-at="97"] { --at: 97%; }
[data-at="98"] { --at: 98%; }
[data-at="99"] { --at: 99%; }
[data-at="100"] { --at: 100%; }

.tl {
    list-style: none;
    margin: var(--s-4) 0 0;
    padding: 0;
    display: grid;
    grid-auto-flow: row;
}

.tl-step {
    position: relative;
    min-width: 0;
    padding: 0 0 var(--s-4) 2rem;
}

.tl-step::before {
    content: "";
    position: absolute;
    top: 0.5rem;
    bottom: -0.5rem;
    left: 0.4375rem;
    border-left: 2px solid;
    border-color: var(--rule-strong);
}

.tl-step:last-child::before {
    display: none;
}

.tl-step::after {
    content: "";
    position: absolute;
    top: 0.375rem;
    left: 0.125rem;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    border: 2px solid var(--ink);
    background: var(--sheet);
}

/* The deadline's mark is the one filled navy mark; once passed, it greys as every past one does. */
.tl-step--deadline::after {
    border-color: var(--navy);
    background: var(--navy);
}

.tl-step--past::after {
    border-color: var(--muted);
    background: var(--muted);
}

.tl-step--today::after {
    background: var(--ink);
}

/* Today's marker: a navy ring, because it is where you are. */
.tl-step--now::after {
    top: 0;
    left: 0;
    width: 1rem;
    height: 1rem;
    border: 3px solid var(--navy);
    background: var(--navy-tint);
}

.tl-label,
.tl-date,
.tl-away,
.tl-by {
    display: block;
}

.tl-label {
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--ink-2);
}

/* Each date said once, large; the window's opening and the narrow band's two dates a step smaller. */
.tl-date {
    font-family: var(--font-serif);
    font-size: 1.375rem;
    font-weight: 600;
    line-height: 1.25;
    font-variant-numeric: lining-nums tabular-nums;
}

.tl-step:is(.tl-step--window, .tl-step--band) .tl-date {
    font-size: 1.125rem;
}

.tl-step--deadline .tl-date {
    color: var(--navy);
    font-size: 1.75rem;
}

.tl-away,
.tl-by {
    color: var(--muted);
    font-size: var(--fs-meta);
}

.tl-step--past .tl-label,
.tl-step--past .tl-date {
    color: var(--muted);
}

.tl-step--now .tl-label {
    color: var(--navy-strong);
}

.tl-ticks {
    list-style: none;
    margin: 0.75rem 0 0;
    padding: 0;
    font-size: var(--fs-meta);
    line-height: 1.4;
}

.tl-tick {
    position: relative;
    padding-left: 1rem;
    margin-bottom: 0.5rem;
}

/* The tick is a small dot, filled once the alert went out to Nick, hollow until then (the text says
 * which). Smaller than a step marker, so a reminder never reads as a stop or as a checkbox. */
.tl-tick::before {
    content: "";
    position: absolute;
    top: 0.3rem;
    left: 0;
    width: 0.4375rem;
    height: 0.4375rem;
    border: 1.5px solid var(--ink);
    border-radius: 50%;
    background: var(--sheet);
}

.tl-tick--sent::before {
    background: var(--ink);
}

.tl-tick-label,
.tl-tick-when {
    display: block;
}

.tl-tick-label {
    color: var(--ink-2);
}

/* A reminder's date is set as the summary's dates are, in the serif's regular figures. */
.tl-tick-when {
    color: var(--muted);
    font-family: var(--font-serif);
    font-variant-numeric: lining-nums tabular-nums;
}

/* The scale as a strip (decorative, aria-hidden: the list says every date): the line, the navy run
 * from today to the deadline, light month ticks and a firmer one where the year turns and its label has room, and a bar at
 * each step, navy for today and the deadline, short for the window and Deliver by, grey once past.
 * All of it is borders, so it prints and keeps its shapes in forced colours. */
.tl-scale {
    position: relative;
    height: 2.25rem;
    margin: var(--s-4) 0 0;
}

.tl-scale::before {
    content: "";
    position: absolute;
    top: calc(1.5rem - 0.5px);
    left: 0;
    right: 0;
    border-top: 1px solid var(--rule-strong);
}

.tl-run,
.tl-before {
    position: absolute;
    top: calc(1.5rem - 1px);
    left: 0;
    width: var(--at);
    border-top: 2px solid var(--navy);
}

/* With steps already past, the run starts at today: the stretch before it is drawn over in the
 * sheet's colour and given back its hairline. */
.tl-before {
    border-top-color: var(--sheet);
}

.tl-before::after {
    content: "";
    position: absolute;
    top: -1.5px;
    left: 0;
    right: 0;
    border-top: 1px solid var(--rule-strong);
}

/* The strip's line sits in its lower part; where the year turns and its label has room, its tick is
 * taller and names the
 * year above the line ("Jan 2027"), so the strip reads as time on its own. */
.tl-month {
    position: absolute;
    top: 1.125rem;
    left: var(--at);
    height: 0.75rem;
    border-left: 1px solid var(--rule);
}

.tl-month--year {
    top: 0;
    height: 1.875rem;
    padding-left: 0.25rem;
    border-left-color: var(--control);
    color: var(--muted);
    font-size: var(--fs-meta);
    line-height: 1;
    white-space: nowrap;
}

.tl-mark {
    position: absolute;
    top: 0.875rem;
    left: var(--at);
    height: 1.25rem;
    margin-left: -1.5px;
    border-left: 3px solid var(--ink);
}

.tl-mark:is(.tl-mark--now, .tl-mark--deadline, .tl-mark--band) {
    border-left-color: var(--navy);
}

.tl-mark:is(.tl-mark--window, .tl-mark--deliver) {
    top: 1.125rem;
    height: 0.75rem;
}

.tl-mark.tl-mark--past {
    border-left-color: var(--control);
}

/* Where a zoomed line skips a long empty stretch at one end, it breaks into dots for the view model's
 * share of the line (_BREAK in src/alerts/timeline.py, 12%), clear of the mark at that end: a box in
 * the sheet's colour hides the line and the run there, so either side of the break reads to scale. */
.tl-break {
    position: absolute;
    top: calc(1.5rem - 0.625rem);
    width: calc(12% - 1rem);
    height: 1.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--sheet);
}

.tl-break--start {
    left: 0.75rem;
}

.tl-break--end {
    right: 0.75rem;
}

.tl-break::before {
    content: "";
    position: absolute;
    top: calc(50% - 1px);
    left: 0;
    right: 0;
    border-top: 2px dotted var(--control);
}

/* The time a break skips ("3 yr 8 mo") is written on the wide line only; the small strip keeps its
 * dots. */
.tl-break > span {
    display: none;
}

@container (min-width: 44rem) {
    .tl {
        grid-auto-flow: column;
        grid-auto-columns: minmax(0, 1fr);
        column-gap: var(--s-4);
    }

    .tl-step {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        padding: 1.9rem 0 0;
    }

    .tl-step::before {
        top: 0.5rem;
        bottom: auto;
        left: 0;
        right: calc(-1 * var(--s-4));
        border-left: 0;
        border-top: 2px solid;
        border-color: var(--rule-strong);
    }

    .tl-step::after {
        top: 0.1875rem;
        left: 0;
    }

    .tl-step--now::after {
        top: 0.0625rem;
        left: 0;
    }
}

/* The line to scale. The rows: what hangs above the line (only as tall as it needs, when something
 * does), the line, what hangs below. Each step sits in its row at its place, its words starting at
 * its mark or ending there (data-align), with a stem up or down to the line; the marks are the
 * scale's, so a step's own dot steps aside. */
@container (min-width: 58rem) {
    .tl-chart--hung {
        --tl-above: 0px;
        position: relative;
    }

    .tl-chart--above {
        --tl-above: 5.5rem;
    }

    /* With nothing hanging above the line, the year labels are the chart's top: the line before it
     * (the head, or a status line such as "No option notice clause is recorded") keeps the strip's
     * own gap over them (UI review polish U8: on leases 2 and 9 the sentence sat on the labels). */
    .tl-chart--hung:not(.tl-chart--above) {
        margin-top: var(--s-4);
    }

    .tl-chart--hung .tl-scale {
        position: absolute;
        top: var(--tl-above);
        left: 0;
        right: 0;
        height: 2.5rem;
        margin: 0;
    }

    .tl-chart--hung .tl-scale::before {
        top: calc(50% - 0.5px);
    }

    .tl-chart--hung :is(.tl-run, .tl-before) {
        top: calc(50% - 1px);
    }

    .tl-chart--hung .tl-month {
        top: calc(50% - 0.25rem);
        height: 0.5rem;
    }

    .tl-chart--hung .tl-month--year {
        top: 0;
        height: calc(50% + 0.375rem);
    }

    .tl-chart--hung .tl-mark {
        top: 50%;
        width: 0.875rem;
        height: 0.875rem;
        margin: -0.4375rem 0 0 -0.4375rem;
        border: 2px solid var(--ink);
        border-radius: 50%;
        background: var(--sheet);
    }

    .tl-chart--hung .tl-mark--now {
        width: 1.125rem;
        height: 1.125rem;
        margin: -0.5625rem 0 0 -0.5625rem;
        border: 3px solid var(--navy);
        background: var(--navy-tint);
    }

    .tl-chart--hung .tl-mark:is(.tl-mark--deadline, .tl-mark--band) {
        border-color: var(--navy);
        background: var(--navy);
    }

    .tl-chart--hung .tl-mark--today {
        background: var(--ink);
    }

    .tl-chart--hung .tl-mark.tl-mark--past {
        border-color: var(--muted);
        background: var(--muted);
    }

    .tl-chart--hung .tl-break {
        top: calc(50% - 0.625rem);
    }

    .tl-chart--hung .tl-break > span {
        display: block;
        position: relative;
        padding: 0 0.375rem;
        background: var(--sheet);
        color: var(--muted);
        font-size: var(--fs-meta);
        line-height: 1;
        white-space: nowrap;
    }

    .tl-chart--hung .tl {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: var(--tl-above) 2.5rem auto;
        grid-auto-flow: row;
        column-gap: 0;
        margin: 0;
    }

    .tl-chart--hung .tl-step {
        grid-area: 3 / 1;
        justify-self: start;
        display: block;
        width: max-content;
        max-width: 24%;
        margin-left: var(--at);
        padding: 0.75rem 0 0;
    }

    .tl-chart--hung .tl-step[data-hang="above"] {
        grid-row: 1;
        align-self: end;
        padding: 0 0 0.75rem;
    }

    .tl-chart--hung .tl-step[data-align="end"] {
        justify-self: end;
        margin-left: 0;
        margin-right: calc(100% - var(--at));
        text-align: right;
    }

    /* A step ending at its mark ends every line there, its reminders too: their dots move to the right. */
    .tl-chart--hung .tl-step[data-align="end"] .tl-tick {
        padding: 0 1rem 0 0;
    }

    .tl-chart--hung .tl-step[data-align="end"] .tl-tick::before {
        left: auto;
        right: 0;
    }

    .tl-chart--hung .tl-step::before,
    .tl-chart--hung .tl-step:last-child::before {
        display: block;
        top: -1.25rem;
        bottom: auto;
        left: 0;
        right: auto;
        height: 1.75rem;
        border: 0;
        border-left: 1px solid var(--rule-strong);
    }

    .tl-chart--hung .tl-step[data-hang="above"]::before {
        top: auto;
        bottom: -1.25rem;
    }

    .tl-chart--hung .tl-step[data-align="end"]::before {
        left: auto;
        right: 0;
    }

    .tl-chart--hung .tl-step::after {
        display: none;
    }
}

/* ------------------------------------------------------------------ 13b. first run and import */

/* The getting-started list in Today's first-run panel: three plain steps, each led by a numbered
 * navy square (the V2 primary, as on the import steps below), then a title, a line and its link. It is
 * static: the panel goes with the empty book, so no step is ever ticked off. The numbers come from
 * the list's own counter, and the list stays an ordered list for assistive technology. */
.checklist {
    max-width: var(--measure);
    margin: var(--s-6) 0 0;
    padding: 0;
    list-style: none;
    counter-reset: checklist;
}

.checklist-step {
    display: grid;
    grid-template-columns: 2.5rem minmax(0, 1fr);
    gap: var(--s-4);
    padding: var(--s-4) 0;
    border-top: 1px solid var(--rule);
    counter-increment: checklist;
}

.checklist-step::before {
    content: counter(checklist);
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 2px solid var(--navy);
    border-radius: var(--radius);
    background: var(--navy);
    color: #ffffff;
    font-family: var(--font-display);
    font-size: 1.125rem;
    font-weight: 700;
}

.checklist-body h3 {
    margin: var(--s-1) 0 var(--s-2);
}

.checklist-body p {
    margin: 0 0 var(--s-3);
}

.checklist-body > :last-child {
    margin-bottom: 0;
}

/* The step indicator on the four import screens, over the page's head on the paper. Four numbered round
 * marks joined by a hairline (design elevation U24: dotted boxes and rules read as placeholders, and
 * the timeline's dotted rule means time skipped): a finished step's mark holds a check, the current
 * one is the navy disc, the rest are outlined; the rule after a finished step is the stronger one.
 * Narrow, only the marks show and the Step line above them names the current step; from 560px every
 * step shows its name. The names
 * are cut out of sight rather than hidden, so a screen reader always hears all four and which one is
 * current. */
.import-steps {
    margin: 0 0 var(--s-4);
}

.import-steps-count {
    margin: 0 0 var(--s-2);
    color: var(--ink-2);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 650;
}

.import-steps-list {
    display: flex;
    align-items: center;
    max-width: 40rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.import-step {
    display: flex;
    flex: 1 1 0;
    align-items: center;
    gap: var(--s-2);
    min-width: 0;
}

.import-step:last-child {
    flex: none;
}

/* the rule that joins a step to the next one */
.import-step:not(:last-child)::after {
    content: "";
    flex: 1 1 auto;
    min-width: var(--s-2);
    margin-right: var(--s-2);
    border-top: 1px solid var(--rule);
}

.import-step-n {
    display: grid;
    flex: none;
    place-items: center;
    width: 1.75rem;
    height: 1.75rem;
    border: 1px solid var(--rule-strong);
    border-radius: 50%;
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 700;
}

.import-step-name,
.import-step-state {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.import-step-name {
    color: var(--muted);
    font-family: var(--font-text);
    font-size: var(--fs-small);
    font-weight: 600;
}

.import-step.is-current .import-step-n {
    border-color: var(--navy);
    background: var(--navy);
    color: #ffffff;
}

.import-step.is-current .import-step-name {
    color: var(--ink);
    font-weight: 700;
}

.import-step.is-done .import-step-n {
    border-color: var(--ok);
    color: var(--ok);
}

.import-step.is-done .import-step-n::before {
    content: "";
    width: 1rem;
    height: 1rem;
    background: currentcolor;
    -webkit-mask: var(--icon-check) center / contain no-repeat;
    mask: var(--icon-check) center / contain no-repeat;
}

.import-step.is-done:not(:last-child)::after {
    border-top-color: var(--rule-strong);
}

.import-step.is-done .import-step-name {
    color: var(--ink-2);
}

@media (min-width: 560px) {
    .import-steps-now {
        display: none;
    }

    .import-step-name {
        position: static;
        width: auto;
        height: auto;
        margin: 0;
        overflow: visible;
        clip-path: none;
        white-space: normal;
    }
}

/* What next, on the import result page: the two places to go from here, as buttons. */
.next-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The import screens' tables (Recent imports, the columns to match, the preview's rows, a batch's
 * rows) restack on a phone (9b). A row's choice box fits the small card it sits in there, and goes
 * back to section 9's table width once the rows are a table again; the "already in negotiation"
 * tick's label is a 44px target, as a tick's label is in a stack form (section 8). */
.import-rows select {
    max-width: 100%;
}

.import-rows label:has(> input[type="checkbox"]) {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--target);
}

.import-rows label > input[type="checkbox"] {
    margin: 0;
}

@media (min-width: 720px) {
    .import-rows select {
        max-width: 22rem;
    }
}

/* The mailbox page's status header, on the paper as every page's head is (design elevation U24): the
 * chip sits in the page head's action row, and the sentence under it says what sending means in that
 * state, at reading width in the lead line's ink, a section gap above the first card. */
.mailbox-head {
    margin-bottom: var(--section-gap);
}

.mailbox-says {
    max-width: 40rem;
    margin: var(--s-2) 0 0;
    color: var(--ink-2);
    font-size: var(--fs-lead);
    line-height: 1.4;
}

/* ------------------------------------------------------------------ 14. motion */

/* Motion marks a change of state and nothing else (docs/design/direction.md, "Motion"; design elevation
 * U31 and U12): the crossfade between pages, the line a change lands with, a pressed button, a fold
 * opening and the enlarged page's popover opening (UI research gaps plan U3). All of it waits for
 * no-preference, and nothing a poll swaps in moves. */
@media (prefers-reduced-motion: no-preference) {
    /* A page-to-page navigation inside the app cross-fades from the old page to the new one instead of
     * leaving a blank gap (browsers that do not know the rule just load the next page). Pages have no
     * entrance of their own: motion is for a change of state, not for arriving (design elevation U31). */
    @view-transition {
        navigation: auto;
    }

    /* A short dissolve between two pages: the default 0.25s lets both layouts show through each other. */
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation-duration: 0.14s;
    }

    /* The line a change lands with after its redirect (Checked it on Today, a decision on the fact list,
     * an answer on the lease page, a document, a landlord contact, a new reading) rises 4px into place
     * once the crossfade has played: the delay is the crossfade's 0.14s, and the backwards fill keeps
     * the line hidden until then. A region htmx polls carries hx-trigger and nothing inside one is
     * matched, so a poll's swap (the reading banner, the documents table, a waiting assignment) never
     * replays it. A line with its Undo (corrections plan U8) rises as a whole, the tint and the button
     * with the words. */
    p.todo-line:not([hx-trigger] *),
    #queue-flash p[role="status"]:not([hx-trigger] *),
    section.lease-head p.alert[role="status"]:not([hx-trigger] *),
    section#documents p[role="status"]:not([hx-trigger] *),
    #landlord-contacts p[role="status"]:not([hx-trigger] *),
    #ai-reading p[role="status"]:not([hx-trigger] *),
    .undo-line:not([hx-trigger] *) {
        animation: status-arrive var(--dur-2) var(--ease-out) 140ms backwards;
    }

    @keyframes status-arrive {
        from {
            opacity: 0;
            translate: 0 4px;
        }
    }

    /* A button gives a little while it is held down: the press eases in, and letting go returns it at
     * once. A blocked or busy button doesn't give, since pressing it does nothing. */
    .btn:active:not(:disabled, [aria-disabled="true"], .is-busy) {
        transform: scale(0.98);
        transition: transform var(--dur-1) var(--ease-out);
    }

    /* The enlarged page and the dim behind it fade in as the popover opens, and go at once when it
     * closes (UI research gaps plan U3, Matt 2026-10-26): the one sheet that still snapped open while
     * the folds and the menu eased. A closed popover isn't drawn, so its starting style plays at each
     * opening; with no exit transition, closing has nothing to play. The page popover only, never the
     * definition tip (a popover too). A browser whose ::backdrop does not inherit the tokens drops the
     * dim's fade and shows it at once. */
    .page-pop,
    .page-pop::backdrop {
        transition: opacity var(--dur-2) var(--ease-out);
    }

    @starting-style {
        .page-pop,
        .page-pop::backdrop {
            opacity: 0;
        }
    }

    /* A fold opens to its height instead of appearing whole, and closes at once. The clip shows only
     * what has opened so far and lifts the moment the fold is open (the overflow waits out the
     * opening, then flips), so an open fold clips nothing: a clip at rest would cut the bottom off the
     * focus ring of a fold's last button. Only where the browser can animate to a keyword height:
     * elsewhere the block is skipped and a fold opens as it always has. */
    @supports (interpolate-size: allow-keywords) {
        details::details-content {
            block-size: 0;
            overflow-y: clip;
            interpolate-size: allow-keywords;
            transition: block-size var(--dur-2) var(--ease-out), overflow-y 0s var(--dur-2) allow-discrete;
        }

        details[open]::details-content {
            block-size: auto;
            overflow-y: visible;
        }
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation: none !important;
        transition: none !important;
        scroll-behavior: auto !important;
    }
}

/* ------------------------------------------------------------------ 15. forced colours */

/* Windows high contrast replaces colours with system ones: keep shapes, borders and icons visible. */
@media (forced-colors: active) {
    .chip::before,
    .icon,
    .alert::before,
    .paused-banner::before,
    details > summary::before {
        background-color: CanvasText;
    }

    /* A tone or a settled state recolours some chips' icons at a higher weight than .chip::before, so
     * that rule would win here too and leave a gap where the icon was (UI review 2026-10-29 V-05).
     * Each is repeated at its own weight, later, so the text colour wins; tests/test_design_guards.py
     * holds every such rule to a repeat in this block. */
    .chip:is(.tone-urgent, .tone-action)::before,
    .fact-item:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .chip::before,
    .fact-pane:is([data-status="accepted"], [data-status="rejected"], [data-status="not-found"], [data-status="not-recorded"]) .pane-status .chip::before,
    .used-fact[data-state="ok"] .chip::before,
    .sent-when .chip::before {
        background-color: CanvasText;
    }

    .btn,
    .chip,
    .alert,
    input,
    select,
    textarea {
        border-color: ButtonText;
    }

    /* Every chip's border shows here, so an unfilled one takes back a filled pill's padding: flush,
     * its round ends cut through the icon and the last letter. */
    .chip:not(.tone-urgent, .tone-action) {
        padding-left: 0.5rem;
        padding-right: 0.625rem;
    }

    .tl-step::after,
    .tl-tick::before,
    .tl-mark {
        border-color: CanvasText;
    }

    /* The page text panel's edge shadows are backgrounds, which this mode blanks, so its border is
     * what marks where the scrolling panel is; a failed picture's box keeps its dashed edge too. */
    .page-text,
    .page-image::after {
        border-color: CanvasText;
    }

    /* The filled marks stay filled: a milestone due today, a sent reminder, and on the line to scale
     * the deadline's mark. */
    .tl-step--today::after,
    .tl-tick--sent::before,
    .tl-chart--hung .tl-mark:is(.tl-mark--today, .tl-mark--deadline, .tl-mark--band) {
        background-color: CanvasText;
    }

    /* The deadline's mark stays filled, as on screen, and a passed mark grey (UI review V-11). */
    .tl-step--deadline::after {
        background-color: CanvasText;
    }

    .tl-step--past::after {
        border-color: GrayText;
        background-color: GrayText;
    }

    .env-banner:not(.paused-banner) {
        border-top: 6px double CanvasText;
        border-bottom: 6px double CanvasText;
    }

    /* The header's tabs: this mode shows a clear border, so every tab drew the current one's rule (UI
     * review V-04). The rest take the ground's colour and the current tab the highlight. */
    .site-nav a {
        border-bottom-color: Canvas;
    }

    .site-nav a[aria-current="page"] {
        border-bottom-color: Highlight;
    }

    /* The runway's filled shapes are backgrounds, which this mode would blank: the diamond, the arrow
     * and the past-due triangle (a mask) are painted in text colour, and the ring and the hold bar keep
     * their borders (a hold bar's dashed edge is its mark here: its hatching is a gradient, which this
     * mode drops). The window's end line is a gradient too, so a border draws it (UI review V-12). */
    .rw-next .rw-shape,
    .rw-past .rw-shape,
    .rw-arrow {
        background-color: CanvasText;
    }

    .runway,
    .rw-row,
    .rw-foot,
    .rw-expiry .rw-shape,
    .rw-hold .rw-shape {
        border-color: CanvasText;
    }

    .rw-row::before {
        border-right: 1px solid CanvasText;
    }

    .rw-row::after,
    .rw-key.rw-now .rw-shape {
        border-left-color: CanvasText;
    }

    /* The busy button keeps its own colours: a forced text colour would paint the real, transparent
     * text over the label. Its border, its label and its spinner use system colours, so the cue is a
     * border and words. */
    .btn.is-busy,
    .btn.is-busy:hover {
        forced-color-adjust: none;
        background: Canvas;
        border-color: ButtonText;
    }

    .btn.is-busy::after {
        color: ButtonText;
    }

    .btn.is-busy::before {
        border-color: ButtonText;
        border-top-color: Canvas;
    }

    /* The queue strip's bar is a filled track with no border on screen; here its ground is blanked, so a
     * border draws its extent and its filled part is the highlight colour (a plain background would be
     * blanked too); the check is painted in text colour. */
    .queue-bar {
        border: 1px solid CanvasText;
    }

    .queue-bar::-webkit-progress-value {
        background-color: Highlight;
    }

    .queue-bar::-moz-progress-bar {
        background-color: Highlight;
    }

    /* A finished queue's calm fill out-ranks the two rules above: each is repeated (UI review V-07). */
    .queue-strip.is-done .queue-bar::-webkit-progress-value {
        background-color: Highlight;
    }

    .queue-strip.is-done .queue-bar::-moz-progress-bar {
        background-color: Highlight;
    }

    .queue-strip.is-done .queue-count::before,
    .fact-done h2::before,
    .pane-back::before {
        background-color: CanvasText;
    }

    /* The bulk accept's check, the pane's Back and Next chevrons and an all-clear list's check are
     * masks too (UI review V-10). */
    .bulk-form .btn:not(.is-busy)::before,
    .pane-step[rel="prev"]::before,
    .pane-step[rel="next"]::after,
    .empty--calm::before {
        background-color: CanvasText;
    }

    /* The fact list's chosen row and filter are a tint and an inset shadow, both blanked here: a
     * highlight outline marks them instead, and the draft's line a date in the letter opened. */
    .fact-link[aria-current="page"],
    .fact-filters a[aria-current="true"],
    .used-fact:target {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }

    /* An open Edit or Reject's navy tint is blanked, so it looked closed (UI review V-13): a highlight
     * edge a pixel wider says it is open, its padding giving the pixel back, so the button keeps its
     * size and its place in the row. */
    .review-actions > details[open] > summary {
        padding: 0 calc(var(--s-4) - 1px);
        border: 2px solid Highlight;
    }

    /* The current import step's navy disc is blanked, leaving four alike circles: a highlight ring
     * marks it (design elevation U24 drew the steps as circles on a hairline). */
    .import-step.is-current .import-step-n {
        outline: 2px solid Highlight;
        outline-offset: 1px;
    }

    /* The open definition tip and the enlarged page lose their fills: a border draws their edge. */
    .fact-tip:popover-open,
    .page-pop {
        border: 1px solid CanvasText;
    }

    .def-mark::before {
        background-color: CanvasText;
    }

    /* Leases: the filter in use is underlined with an inset shadow, blanked here, so an outline marks
     * it; the active heading's arrow and the other sortable headings' resting glyph are masks. */
    .lease-filters a[aria-current="true"] {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }

    /* Focus on the chosen fact or filter: the outline that marks it chosen is the focus ring's own
     * property at a higher weight, so tabbing onto it showed nothing (UI review V-08). A double ring
     * marks focus there: inside the fact list and its filters, whose boxes clip what crosses their
     * edge, and outside a Leases filter, whose icon and words reach its own edge. */
    .fact-link[aria-current="page"]:focus-visible,
    .fact-filters a[aria-current="true"]:focus-visible {
        outline: 6px double Highlight;
        outline-offset: -6px;
    }

    .lease-filters a[aria-current="true"]:focus-visible {
        outline: 6px double Highlight;
        outline-offset: 2px;
    }

    .sort-link::after,
    .sort-arrow {
        background-color: CanvasText;
    }

    /* The import steps' check is a mask over a background, which this mode would blank. */
    .import-step.is-done .import-step-n::before {
        background-color: CanvasText;
    }

    /* Add lease documents and the filing card: the drop box's plus and "Needs you"'s question mark
     * are masks too, and the chosen destination's rule (a shadow) is blanked, so a highlight outline
     * marks it, as on the fact list. */
    .drop-form .drop-zone-label::before,
    .card-needs h3::before {
        background-color: CanvasText;
    }

    .dest-option:has(.dest-radio:checked) {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }

    /* Deciding an assignment: the chosen buyer's rule is blanked too, so it is outlined. */
    .buyer-option:has(input[name="buyer"]:checked) {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }

    /* Today's warning icon and its groups' chevrons are masks too. */
    .todo-warn::before,
    .today-group-head .group-head::after {
        background-color: CanvasText;
    }

    /* The urgent and action groups colour their chevron at a higher weight, so it is repeated (UI
     * review V-06). */
    .today-group:is(.tone-urgent, .tone-action) .group-head::after {
        background-color: CanvasText;
    }

    /* The quoted words' highlighter is a gradient over no background colour, which this mode drops,
     * while the words take the mark's own dark text colour: they vanished on the dark ground (UI V2
     * plan U15). The mark's own pair keeps them highlighted and readable. */
    .quote-context mark {
        background: Mark;
        color: MarkText;
    }

    /* The draft's locked footer lost its paper ground and read as one more box to type in: a dashed
     * edge sets it apart from the Message box above it (UI review V-14). */
    .email-preview--footer {
        border-style: dashed;
    }

    /* A date or time field's picker glyph follows the page's colour scheme, light only, so it drew dark
     * on a dark theme's ground (UI review V-09, seen in emulation only): either scheme, as the theme. */
    :root {
        color-scheme: light dark;
    }

    :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible,
    input:is([type="date"], [type="time"]):focus-within {
        outline-color: Highlight;
    }
}

/* ------------------------------------------------------------------ 16. print */

/* Words and copies that exist for paper only: the "as of" line on the lease, alert, draft and outcomes
 * pages, and an unsent draft's subject and text (the composer's own fields would print clipped). They
 * are hidden on screen and shown by the print block below. */
.print-only {
    display: none;
}

/* Letter paper. The side margins are small on purpose: a letter page less its margins is about 739 CSS
 * pixels, just over the 720px breakpoint, so tables print as tables and not as stacked cards. */
@page {
    size: letter;
    margin: 0.6in 0.4in;
}

@media print {
    /* The brand line the screen leaves to the header comes back on paper, where the header doesn't go. */
    .footer-brand {
        display: block;
    }

    /* Paper has no use for the chrome: the header, tabs and account menu, the skip link, the development
     * and paused banners, the screen reader busy line, the back links, buttons and every form. The forms
     * carry display rules of their own (flex, inline), so theirs is the one rule that must be forced.
     * Folded sheets that hold only a form (act on this alert, close this deal) go with their section,
     * and the landlord contacts' Edit, Remove and "Add a contact", a fact's Review or Decide and each
     * value's Change go from the lease summary, as Change goes from Amendments. A link that only
     * leads to another screen ("See every fact", "All contacts", "Opportunity and outcomes", the
     * alert's "Client contacts") and a line that speaks to the screen ("Nothing is sent until you press
     * Send") carry .screen-only, and the Technical details folds go by their class (.tech: no details
     * selector here sets display) (UI review 2026-10-29 V-42). */
    .skip-link,
    .site-header,
    .site-nav,
    .account,
    .env-banner,
    .paused-banner,
    .busy-status,
    .crumbs,
    .btn,
    .act-on,
    .close-sheet,
    #upload,
    .todo-main,
    .todo-quiet,
    .landlord-actions,
    .landlord-add,
    .summary-act,
    .change-link,
    .assignment-actions,
    .email-actions,
    .screen-only,
    .tech {
        display: none;
    }

    form {
        display: none !important;
    }

    .card:has(> .act-on, > .close-sheet, > form:only-child) {
        display: none;
    }

    /* A card that is a heading over a form goes with its form, so no empty card prints: the drop box
     * on Add lease documents, Nick's filing card (the drop's name and its first line stay) and Enter
     * by hand's typing card (the facts and the preview stay). */
    .card:has(> .drop-form, > #filing-card, > #enter-form) {
        display: none;
    }

    /* So does any card that is only a heading and a form: "Add a contact" on a client's contacts,
     * "Save a new version" on split settings, "Change password" on the account page; and one whose
     * form is followed only by its links to other screens (the alert's "Email the client" before a
     * draft exists). */
    .card:has(> h2:first-child + form:last-child),
    .card:has(> h2:first-child + form + .screen-only:last-child) {
        display: none;
    }

    /* A sheet whose first card goes (Search's form card) starts at the next card, with the sheet's top
     * corners (design elevation U24). */
    .one-sheet > .card:first-child:is(:has(> .act-on, > .close-sheet, > form:only-child),
            :has(> .drop-form, > #filing-card, > #enter-form), :has(> h2:first-child + form:last-child)) + .card {
        margin-top: 0;
        border-top-left-radius: var(--radius-sheet);
        border-top-right-radius: var(--radius-sheet);
    }

    /* Decorative pictures stay off paper: the banner's and every art() picture (the ART table's, web/art.py).
     * The words beside them already read on their own, as on screen when a picture fails to load. */
    .art,
    .banner-img {
        display: none;
    }

    .print-only {
        display: block;
    }

    p.print-only {
        color: var(--muted);
        font-size: var(--fs-small);
    }

    /* The page is plain ink on paper: no ground colour, no flex column that stretches to the screen,
     * and no side padding (the page margins already give that). The root size is a plain 16px, not the
     * wide screen's larger step: the timeline's card is then wide enough for its strip layout. */
    html {
        font-size: 100%;
    }

    body {
        display: block;
        min-height: 0;
        background: none;
        color: var(--ink);
    }

    .container {
        max-width: none;
        padding: 0;
    }

    .site-main {
        padding: 0;
    }

    /* The sign-in and error pages' own ground and top gap (12d) out-rank the two rules above. */
    .page-auth {
        background: none;
    }

    .page-auth .site-main {
        padding-top: 0;
    }

    .site-main > .card:last-child,
    .one-sheet:last-child > .card:last-child {
        padding-bottom: 0;
    }

    .card,
    .card.narrow,
    .dtile,
    .email-preview,
    .page-image {
        box-shadow: none;
    }

    /* The locked footer lives inside the composer, which is hidden on paper (the printed copy of the
     * whole email stands in for it); if it is ever printed, its words are plain ink on white. */
    .email-preview--footer {
        background: none;
        color: var(--ink);
    }

    /* A date in the letter can't be followed on paper: it prints as the letter's plain text. */
    a.letter-date {
        text-decoration: none;
    }

    /* The footer keeps with the last card, so no page holds the footer alone (UI review 2026-10-29
     * V-42). */
    .site-footer {
        margin-top: var(--s-3);
        break-before: avoid;
        break-inside: avoid;
    }

    .footer-row {
        padding-top: var(--s-2);
        padding-bottom: 0;
    }

    /* Cards, rows, pictures and timeline steps stay whole, a heading stays with what follows it, and a
     * table repeats its column names on each page. */
    .card,
    tr,
    img,
    .page-image,
    .activity-row,
    .carried-row,
    .rw-row,
    .tl-step,
    .tl-tick,
    .alert,
    .dtile-wrap,
    .lease-callout,
    .amendment,
    .assignment,
    .facts-entry,
    .todo-row,
    .email-head {
        break-inside: avoid;
    }

    /* Today on paper is a plain list: the rows' actions are hidden above, and each row keeps its
     * tile (when it has a date), its client and its words on one line of the sheet. A folded group
     * prints its header only, as on screen; Recent activity is left off. Paper is wider than 720px,
     * so these two selectors stay as specific as the desk grid's (12g) and win by coming later. The
     * body keeps its own box here too: a print scale over 100% draws the page under 720px, where the
     * phone's body steps aside (display: contents) and its pieces would leave the body's area. */
    .todo-row {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "body";
    }

    .todo-row:has(> .todo-tile) {
        grid-template-columns: 3.5rem minmax(0, 1fr);
        grid-template-areas: "tile body";
    }

    .todo-tile {
        grid-area: tile;
    }

    .todo-body {
        display: block;
        grid-area: body;
        min-width: 0;
    }

    .today-side {
        display: none;
    }

    /* A card made of rows that stay whole may break between them, so a long list does not leave a
     * half-empty page behind it: the Amendments section is one, breaking between its amendments, and
     * Recent backups another, between its backups. The timeline is one too, breaking between its steps
     * (each kept whole, above) once its head, its strip and its first step are on the page: kept whole,
     * it moved to page 2 and left page 1 three-quarters empty (UI review 2026-10-29 V-41). The email is
     * a letter: it may break between its lines, its memo head (to, from, subject) kept whole (above);
     * kept whole, it moved to the next page and left the head alone on page 1 (design elevation U24). */
    .card:has(.activity, .carried, .issue-list, .data-table, .amendment, .assignment, .facts-entry),
    .card.timeline,
    .card.email-sheet {
        break-inside: auto;
    }

    /* The timeline's head, its status line and its strip stay with the first step, so the strip's
     * marks never stand apart from the dates they draw. Not a head or a status line that ends its card
     * (a hold, a missing date): an avoid there would pass to the card and tie it to the summary, so
     * both moved to the next page with Next up and left the head alone on page 1 (as Next up's line
     * did, below). */
    .tl-head:not(:last-child),
    .tl-status:not(:last-child),
    .tl-scale {
        break-after: avoid;
    }

    /* Next up is the header line of the timeline's sheet, with no bottom edge of its own (12b). On
     * paper the two may part at a page's end, so Next up keeps its edge; joined, it lies under the
     * timeline's top edge, one hairline as on screen (UI review 2026-10-29 V-41). The selector is
     * the screen rule's, so it out-ranks it by coming later. */
    .next-up:has(+ .timeline:not(.timeline--hold, .timeline--needs_entry)) {
        margin-bottom: -1px;
        border-bottom-width: 1px;
    }

    h1,
    h2,
    h3,
    h2 + p {
        break-after: avoid;
    }

    /* A heading's line that ends its card (Next up's) is the card's content, not a lead into more: an
     * avoid there would pass to the card and tie it to the next one, so both moved to the next page
     * together and left the head alone on page 1 (design elevation U24). */
    h2 + p:last-child {
        break-after: auto;
    }

    /* The paragraphs here are a line or three: keep each whole, so a heading and its line travel together. */
    p {
        break-inside: avoid;
    }

    thead {
        display: table-header-group;
    }

    /* Today's marker hangs a little above and left of the list, where the screen has page padding and paper
     * has none, so the list is padded to hold it (paper clips what leaves the page margins). */
    .tl {
        padding: 0.25rem 0 0 0.25rem;
    }

    /* A Letter page less its margins leaves the timeline's card, at the screen's 1.25rem sides, a few
     * pixels short of the 44rem its steps need to stand in columns (13), so its rail ran down most of
     * a page. At 1rem sides (Next up's too, so the sheet's words keep one edge) the steps stand in
     * columns, and a lease with a short Next up prints its timeline whole on page 1 and the lease on
     * three pages, not four (UI review 2026-10-29 V-41). */
    .card.next-up,
    .card.timeline {
        padding-left: var(--s-4);
        padding-right: var(--s-4);
    }

    /* Wide tables and the scrolling page text print in full instead of being cut at the wrapper. */
    .table-wrap,
    .page-text {
        overflow: visible;
        max-height: none;
    }

    /* A ledger's screen width is wider than the paper: on paper it fits the page and wraps its cells. */
    .data-table.ledger {
        min-width: 0;
    }

    /* Long ids and hashes wrap at any letter instead of running off the paper. Not the table cells: a cell
     * that may break anywhere has no minimum width, and the table squeezes a chip to a letter a line. */
    code {
        overflow-wrap: anywhere;
    }

    .chip {
        white-space: nowrap;
    }

    /* The documents list keeps its facts; its download link is no use on paper. */
    .doc-file-link {
        display: none;
    }

    /* The waiting list's Next step column holds only buttons, which paper leaves out: the column
     * goes too (Matt's list has no such column, so it has no fourth). */
    .drops-table th:nth-child(4),
    .drops-table td[data-label="Next step"] {
        display: none;
    }

    /* Backgrounds do not print unless asked, so these keep a border (where they have one) and, where the
     * colour is the message, ask to be printed: the tiles' coloured month, the timeline dots,
     * marks and ticks, a break's box that hides the line, the chips and alerts with their icons, and
     * the fold chevrons. */
    .dtile,
    .tl-mark,
    .tl-break,
    .tl-step::after,
    .tl-tick::before,
    .chip,
    .icon,
    .tinted,
    .alert,
    .empty--calm,
    .card-needs h3::before,
    details > summary {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    /* A link names its address on paper, but only on the lease, alert, draft and outcomes pages: the
     * mailbox page carries a long sign-in address that must never print. */
    .site-main:has(.lease-grid, .alert-dates, .composer, .email-preview, .opp-head) a[href^="http"]::after {
        content: " (" attr(href) ")";
        color: var(--muted);
        font-size: var(--fs-small);
        overflow-wrap: anywhere;
    }
}
