/*
  bigbear.css - the served stylesheet.

  This is NOT docs/mockups/css/bigbear.css. That file is the design pass's
  deliverable and it is written against the MOCKUPS' markup. MEASURED on this
  tree, comments stripped first: it carries 189 distinct class selectors and
  ZERO of them begin with `.bb-`, which is the prefix every class the
  application emits uses. So not one of those selectors can match anything this
  site renders.

  The comment-stripping is not a detail. The same count taken over the raw file
  returns 191, because two tokens inside comment prose look like selectors. A
  first draft of this header carried the 191.

  What moves across is the DESIGN, not the markup: the token set, the measured
  colour pairs, the type scale, the spacing scale, the control heights, the
  focus treatment. What stays put is the class vocabulary - the `bb-` hooks are
  per-slice contracts recorded in docs/style-hooks/, ten slices' worth, and
  renaming them to match the mockups would be a large diff across files other
  slices own.

  Every colour pair below is one the design pass measured with the WCAG 2
  relative-luminance formula; the table is docs/mockups/DESIGN.md section 3.1.
  The ratios are NOT re-measured here - the values are copied unchanged, so
  copying is what had to be got right, and the source table is cited rather
  than restated so there is one place to correct.

  Layers, lowest priority first:
    tokens      custom properties only
    base        reset, typography, focus, links, form element defaults
    components  the hooks the application emits
    pages       the gate, which re-points tokens rather than restating rules

  TWO LAYERS FROM THE MOCKUP STYLESHEET ARE ABSENT AND THAT IS DELIBERATE.
  `layout` holds the app shell, top bar, sidebar, scrim and page grids; none of
  those exist in this application - there is one `<main>` and no navigation,
  because there is nowhere to navigate to yet. `utilities` holds single-purpose
  helper classes; MEASURED on this tree, the application emits no class that is
  not a `bb-` hook, so every utility would be a rule with no possible subject.

  SCOPE, stated because the gap is the interesting part, and RE-MEASURED when
  `/rsvp` was mounted. Routed today: the gate at `/`, the six identity endpoints
  under `/who`, `/profile/contact`, the RSVP form at `/rsvp`, and the organiser
  board at `/o/{hash}/gate`. MEASURED over the four files
  `crates/web/src/layout/assets.rs` names as reachable: THIRTY-FIVE distinct
  `bb-` hooks, and this file has a rule for each - a test asserts it and fails
  naming the gap.

  The previous figure was twenty-nine. RSVP contributed exactly six new hooks -
  bb-rsvp, bb-rsvp-response, bb-rsvp-nights, bb-rsvp-housing, bb-party-member
  and bb-party-member-legend - and the rules for them are in the components
  layer below. Its other five were shared form hooks that already had rules.

  FIVE PAGES ARE STILL BUILT AND UNMOUNTED: the dashboard, money, parking,
  sleeping and meals. MEASURED across those five files: 57 distinct hooks, of
  which 7 already have a rule here because they are the shared form vocabulary,
  and 50 do not. Mounting one of them is the same three steps this change took -
  add the file to the reachable list, watch the test name what is missing, write
  those rules.

  Browser baseline, unchanged from the design: cascade layers, native nesting
  written with the leading `&`, `:has()`, logical properties. Roughly mid-2023
  browsers onward. Nothing a page needs depends on hover, pointer precision or
  scripting. Two rule sets on /bring read more than the markup, and both match
  only classes the browser bundle puts on the page: the swipe itself (D-64,
  D-67), and D-69's, which hides the bring buttons a swipe replaces, and only
  where the main pointer is coarse. Without the bundle neither matches
  anything. An older browser gets an unstyled but readable document, which is
  why the markup is semantic in the first place.
*/

@layer tokens, base, components, pages;

/* ------------------------------------------------------------------ */
/* Tokens                                                              */
/* ------------------------------------------------------------------ */

@layer tokens {
  :root {
    color-scheme: light;

    /* Colour. Values copied from DESIGN.md 3.1, where each pair's contrast
       ratio was measured with the WCAG 2 formula. Body text is 14.62:1 on the
       page background; muted text 7.53:1; white on the accent 7.49:1. */
    --color-bg: #FAF6EF;
    --color-surface: #FFFFFF;
    --color-surface-2: #F3ECE0;
    --color-text: #2B2118;
    --color-text-muted: #5C4D3F;
    --color-line: #D9CDBB;
    --color-line-strong: #8C7A66;

    --color-accent: #8F3B12;        /* button fill */
    --color-accent-deep: #853610;   /* pressed */
    --color-pumpkin: #A8461A;       /* warning notice edge, non-text */
    --color-cranberry: #7D1F33;     /* links, errors */
    --color-sage: #3F5330;          /* confirmed, saved */
    --color-on-accent: #FFFFFF;
    --color-link: var(--color-cranberry);
    --color-focus: #2B2118;

    --tint-accent: #FBE8DB;
    --tint-cranberry: #F6E1E5;
    --tint-sage: #E7EDDD;
    --tint-sand: #EFE7D8;

    /* D-102: the dashboard's cabin and harvest. Each ratio below was computed
       with the WCAG 2 formula from these values on 2026-10-07, and the dark
       values carry their own beside them. The pine edge and the marks are not
       text: 7.76:1 against the page and 8.36:1 against a card. Text on the
       harvest wash: body 14.12:1, muted 7.27:1. */
    --color-pine: #35553A;          /* a dashboard card's edge and marks */
    --color-wheat: #9A6A12;         /* one harvest leaf, non-text, 4.24:1 */
    --tint-harvest: #FCF1E3;        /* the meal and need cards */

    /* The scene under the dashboard's title: a cabin in the pines. Pictures,
       not text, so no ratio applies to them EXCEPT the sky, which is what the
       title sits on: the title is 12.97:1 on it. */
    --scene-sky: #F6E7D4;
    --scene-fleck: #FFFFFF;
    --scene-moon: #F4C98E;
    --scene-far: #B9A8A0;
    --scene-snow: #FFFFFF;
    --scene-near: #8E9C82;
    --scene-pine-back: #4E6B4F;
    --scene-pine: #2F4A33;
    --scene-wood: #8A5A35;
    --scene-roof: #5A3424;
    --scene-window: #F2B55C;
    --scene-smoke: #D7C8BA;
    /* D-106: the ground under the treeline in the footer and the drawer.
       Snow by day; at night a dim grey, because the scene's snow (#D3DAE3)
       drawn as a thin band across a dark drawer was the brightest thing on
       the screen (LOOKED AT in Chrome). Not text; no ratio applies. */
    --scene-ground: #FFFFFF;
    /* D-121: what the dashboard's cabin gains as the trip comes closer, and
       its people. Named by paint, not by object, so one paint serves a coat,
       a car and a pumpkin. Pictures, not text, so no ratio applies. */
    --scene-paint-red: #B23A48;     /* a coat, a hat, a car, the sled, the wreath's bow */
    --scene-paint-orange: #CF6F2B;  /* a coat, a hat, the pumpkins */
    --scene-paint-gold: #D3A13A;    /* a coat, a hat */
    --scene-paint-blue: #3E6F95;    /* a coat, a hat, a car */
    --scene-paint-green: #4B8156;   /* a coat, a hat, the wreath, a pumpkin's stem */
    --scene-iron: #3A3330;          /* legs, the smoker, wheels, the lantern, the sled's runner */
    --scene-logend: #DDB88E;        /* the woodpile's cut ends, the wheels' hubs */
    --scene-bulb-red: #D2353F;      /* a roof light */
    --scene-bulb-green: #2F9A5A;    /* a roof light */
    --scene-bulb-blue: #3576CC;     /* a roof light */
    --scene-track: #E4D9CC;         /* tyre tracks in the snow, after the trip */

    /* The ink the drawer's scrim is painted in, at the opacity that rule sets.
       A TOKEN RATHER THAN `--color-text` READ DIRECTLY, and it is the one veil
       on this site that cannot follow the text colour: under the dark theme at
       the end of this file `--color-text` is an off-white, so a scrim drawn in
       it would LIGHTEN the page behind an open drawer instead of dimming it.
       The light value is the text colour and nothing has changed here; the
       dark theme re-points this one separately. */
    --scrim-ink: var(--color-text);

    /* The gate's dark palette, DESIGN.md 3.1's second table. It lives in the
       same file rather than in a second stylesheet because the application
       serves ONE stylesheet: crates/web/src/app.rs's `Shell` is shared by every
       page and emits exactly one `<link rel="stylesheet">`. Splitting the gate
       out again would mean editing that shared markup, which this slice does
       not do. The cost is that a visitor who never gets past the door downloads
       the whole file; the mockup's gate.css avoided that and this cannot. */
    --gate-bg: #1A110B;
    --gate-bg-deep: #0D0805;
    --gate-text: #FAF6EF;
    --gate-text-muted: #D7C4AA;
    --gate-input: #2A1C13;
    --gate-line: #8C6B4F;
    --gate-amber: #F2A65A;
    --gate-amber-pressed: #F0A14F;
    --gate-glow-warm: rgb(168 70 26 / 0.65);
    --gate-glow-cool: rgb(125 31 51 / 0.5);

    /* Type. System stacks only: nothing is downloaded, so text renders the
       moment the HTML arrives, and D-5b bans an externally hosted resource on
       any page anyway - a font from another origin would put the whole URL,
       admin hash included, into that origin's logs via `Referer`.

       --font-mono is NOT from the design. DESIGN.md has no monospace token
       because no mockup contains a `<code>` element. The organiser board does:
       it renders the recovered gate password inside one. A stack, not a
       downloaded face, for the same reason as the other two. */
    --font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-display: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", "Noto Serif", "Times New Roman", serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    --text-sm: 0.889rem;            /* the floor: 15.1px at the 17px phone root (D-130) */
    --text-base: 1rem;              /* 17px on phones, 19px on wide screens (D-130) */
    --text-lg: 1.125rem;
    --text-xl: 1.333rem;
    --text-3xl: clamp(1.889rem, 1.5rem + 1.5vw, 2.5rem);
    --leading-body: 1.5;
    --leading-tight: 1.2;

    /* Space */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-8: 4rem;

    /* Sizes. --tap-min is the design's floor for every control: 3rem, which
       is 51px at the phone root and 57px on wide screens since D-130 (the
       owner's one size smaller, 2026-10-23; it was 54px and 60px), and
       DESIGN.md measured every mocked control at or above it.
       --column-max is `.onboard`'s literal 30rem promoted to a token;
       the mockup used that width for the screens before sign-in, and every page
       this application routes today is one question or one short form. */
    --tap-min: 3rem;
    --tap-large: 3.5rem;
    --control-height: 3.25rem;
    --measure: 65ch;
    --column-max: 30rem;

    /* Shape */
    --radius-sm: 0.5rem;
    --radius: 0.75rem;
    --border: 1px solid var(--color-line);
  }
}

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

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

  * {
    margin: 0;
  }

  /* D-130, the owner, 2026-10-23: one size smaller everywhere, 17px on a
     phone and 19px on a wide screen, where it was 18px and 20px. Every size
     on the site is in rem, so this one rule moves them all: the text floor
     (`--text-sm`) to 15.1px and the tap floor (`--tap-min`) to 51px on a
     phone. Below the floors DESIGN.md set for relatives in their seventies,
     and the owner's call; DESIGN.md section 2 states the floors he chose. */
  html {
    font-size: 106.25%;
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
  }

  /* The breakpoint is in `em`, which resolves against the browser's default
     size rather than this page's. A reader who has turned their browser text up
     keeps the phone layout for longer, which is the right outcome for an
     audience whose several primary users are in their seventies. */
  @media (min-width: 64em) {
    html {
      font-size: 118.75%;
    }
  }

  body {
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: var(--leading-body);
    color: var(--color-text);
    background: var(--color-bg);
    min-block-size: 100vh;
    overflow-wrap: break-word;
  }

  h1 {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: var(--text-3xl);
    line-height: var(--leading-tight);
    letter-spacing: -0.01em;
    text-wrap: balance;
  }

  a {
    color: var(--color-link);
    text-decoration: underline;
    text-underline-offset: 0.18em;
    text-decoration-thickness: 0.09em;

    &:hover,
    &:focus-visible {
      text-decoration-thickness: 0.18em;
    }
  }

  /* 3px solid, offset 3px, in the text colour. DESIGN.md measured it at better
     than 9:1 against its ground on both themes. */
  :focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 3px;
  }

  input,
  button,
  select,
  textarea {
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
  }

  button {
    cursor: pointer;
  }

  ul[class] {
    list-style: none;
    padding: 0;
  }

  code {
    font-family: var(--font-mono);
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      transition-duration: 0.01ms !important;
      animation-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

/* ------------------------------------------------------------------ */
/* Components - one section per group of hooks                         */
/* ------------------------------------------------------------------ */

@layer components {

  /* The shell: every page ---------------------------------------- */

  .bb-main {
    inline-size: 100%;
    max-inline-size: var(--column-max);
    margin-inline: auto;
    padding: var(--space-6) var(--space-4) var(--space-8);
    display: grid;
    gap: var(--space-5);
    align-content: start;
  }

  .bb-title {
    margin-block-end: var(--space-1);
  }

  /* Ordinary prose. NOT muted: DESIGN.md puts muted text at 7.53:1 and body
     text at 14.62:1, and on these pages the note is usually the only
     instruction on the screen - "Type the name your family calls you" is the
     content, not a caption. The measure cap is the design's 65ch. */
  .bb-note {
    max-inline-size: var(--measure);
  }

  /* The one list of destinations that exists. Each row is its own tap target
     rather than a bare bullet, because it is a link a grandparent aims at. */
  .bb-links {
    display: grid;
    gap: var(--space-2);
    margin: 0;

    & a {
      display: flex;
      align-items: center;
      min-block-size: var(--tap-min);
      padding: var(--space-2) var(--space-4);
      border: var(--border);
      border-radius: var(--radius-sm);
      background: var(--color-surface);
      font-weight: 600;
      text-decoration: none;

      /* D-106: a pine edge where the page you are on has an accent one, so
         a hovered or focused row reads as the cabin's and the current one as
         the one lit. */
      &:hover,
      &:focus-visible {
        border-inline-start: 3px solid var(--color-pine);
        background: var(--color-surface-2);
        text-decoration: underline;
      }
    }
  }

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

  /* Every form on the site, and they are all the same shape: a stack of
     label-then-input pairs ending in one button.

     THE SPACING MECHANISM, because the obvious one is wrong here. The mockup
     wraps each label and its input in a `.field` div and puts a 1.5rem gap
     between fields. This markup does NOT wrap - the label and the input are
     siblings - so one uniform gap would separate a label from its own input by
     as much as it separates two questions. Instead the base gap is tight and a
     new group is declared by ADJACENCY: a label that directly follows an input,
     a checkbox row, or the rotate form's warning starts a new question and gets
     the larger space. A label that follows a hidden input - which is how the
     PIN, claim and new-person forms begin, because they carry sealed state -
     matches nothing and correctly gets no leading gap.

     THIS USED TO BE A LIST OF NINE CLASS NAMES AND THE LIST WAS THE DEFECT.
     `.bb-change-pin` was not on it and had no rule anywhere, so `/who/change`
     shipped with its labels and inputs overlapping at every width - MEASURED
     2026-09-28 at 320, 390, 768, 1024 and 1440. `.bb-reset` was not on it
     either and `/who/reset` had the same defect. `/who/resolve` renders the
     same markup correctly for the single reason that `.bb-who` IS on the list,
     which is what makes the omission a mechanism rather than an oversight: a
     page written in a new file could render unstyled and nothing said so.

     So the enumeration is gone and the selector is structural. Every form this
     application renders sits inside `<main class="bb-main">`, because every
     view goes through `Shell`, so `form` reaches all of them including ones
     nobody has written yet.

     IT SAID `form[class]` FOR ONE COMMIT AND THE REASON GIVEN WAS FALSE. The
     claim was that all twenty-five forms carry a `bb-` class; MEASURED by
     grepping `<form` without one, exactly ONE does not - the ballot form in
     `crates/web/src/routes/meals.rs`, which is a bare `<form method="post">`.
     So the attribute was excluding the single form the enumeration it replaced
     had also missed, which is the same failure in a new costume.

     `:where()` WRAPS THE WHOLE SELECTOR ON PURPOSE: it contributes zero
     specificity, so this is a DEFAULT rather than a rule to fight. Every form
     below with an opinion of its own - `.bb-rsvp`'s larger gap, `.bb-suggest`'s
     leading rule, `.bb-claim-act`'s button row, `.bb-bring-act` inside a card -
     overrides it with an ordinary class selector, with no `!important` and no
     reordering. MEASURED by rendering all nine member pages against both
     stylesheets at 390 and 1024 and comparing every element rectangle: home,
     rsvp, meals, bring and contact did not move by one pixel, and the only
     pages that changed were the two that had no rule.

     `.bb-claim` left the list and did not join this rule as itself. It is two
     things: a form on the identity screens, which this reaches, and the claim
     prompt `<aside>` in `crates/web/src/app.rs`, which is not a form and is
     laid out by its own rule at the end of this file. */
  :where(.bb-main form) {
    display: grid;
    gap: var(--space-2);
    max-inline-size: var(--column-max);
  }

  /* The RSVP form is the one page that is not a flat stack: it is four groups
     and a repeating one, so its parts need the space BETWEEN groups that the
     adjacency rule above gives between questions. */
  .bb-rsvp {
    gap: var(--space-5);
  }

  .bb-input + .bb-field,
  .bb-check + .bb-field,
  .bb-warn + .bb-field,
  .bb-gate-help + .bb-field,
  .bb-gate-wrong + .bb-field {
    margin-block-start: var(--space-4);
  }

  /* A visible word above the box, never a placeholder that vanishes. */
  .bb-field {
    font-weight: 700;
  }

  /* A sentence under a field's word saying where its answer goes: the
     organiser's "Write back" says the household reads it (D-92). A block, so
     it sits on its own line between the word and the box. */
  .bb-field-hint {
    display: block;
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--color-text-muted);
  }

  .bb-input {
    inline-size: 100%;
    min-block-size: var(--control-height);
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);

    &:focus-visible {
      outline: 3px solid var(--color-focus);
      outline-offset: 2px;
      border-color: var(--color-text);
    }
  }

  /* The PIN boxes, selected by the markup that already distinguishes them
     rather than by a class this slice cannot add. MEASURED by reading
     crates/web/src/routes/who/view.rs: the three PIN inputs are the only
     inputs on the site carrying maxlength="4".

     docs/style-hooks/s20-identity.md asks for this in terms: "The boxes are
     four characters; a full-width text input reads as 'type a lot here' and is
     the wrong shape." The width is the mockup's `.input-short`, 12rem, rather
     than a number invented here. */
  .bb-input[maxlength="4"] {
    max-inline-size: 12rem;
    min-block-size: var(--tap-large);
    font-size: var(--text-xl);
    letter-spacing: 0.15em;
    text-align: center;
  }

  /* A checkbox with a big tappable label around it. The native control stays
     VISIBLE, so the state is carried by the tick as well as by the tint and
     never by colour alone.

     THE JUSTIFICATION THIS CARRIED IS GONE AND THE RULE IS NOT. It said "these
     two boxes" - the contact form's two remove boxes - were load-bearing
     because they were how somebody said "I have none" without a blank box
     deleting a grandparent's phone number. That form now prefills and has no
     checkboxes at all. MEASURED 2026-09-28 by grepping `class="bb-check"` over
     crates/web/src: nine occurrences remain, across five other files, so the
     rule stays and only its reason changes.

     A full-width bordered row rather than a hairline tick in the margin,
     because every one of those nine is a real choice on a phone. */
  .bb-check {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
    gap: var(--space-3);
    min-block-size: var(--tap-min);
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    cursor: pointer;

    & input {
      inline-size: 1.5rem;
      block-size: 1.5rem;
      margin: 0.15rem 0 0;
      accent-color: var(--color-accent);
    }

    &:has(input:checked) {
      background: var(--tint-accent);
      border-color: var(--color-accent);
    }

    &:has(input:focus-visible) {
      outline: 3px solid var(--color-focus);
      outline-offset: 3px;
    }
  }

  /* RSVP: four groups and one that repeats ----------------------- */

  /* Every group on this page is a `fieldset`, which is correct markup and has
     a browser default border, padding and margin that would draw five boxes
     inside one form. The border is dropped and the grouping is carried by the
     legend and the spacing instead - the same way the rest of this site groups
     things, and the reason .bb-rsvp above sets a larger gap.

     min-inline-size: 0 is not decoration. A fieldset's default is auto, which
     makes it refuse to shrink below its content and pushes a wide row out of
     the column on a narrow phone. It is the one browser default here that is
     specific to this element rather than to boxes in general. */
  .bb-rsvp-response,
  .bb-rsvp-nights,
  .bb-rsvp-housing,
  .bb-rsvp-parking {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    margin: 0;
    padding: 0;
    border: 0;

    & > legend {
      padding: 0;
      font-weight: 700;
      margin-block-end: var(--space-2);
    }
  }

  /* D-117: the car section under the parking question, there while "Yes"
     is checked. "No" hides it with this stylesheet alone, no JavaScript; a
     browser without :has() shows it always, and a car typed under a "No" is
     refused in words (HC-02), never dropped. The section itself is Your
     Info's (D-118, with `.bb-car-line`). */
  .bb-rsvp-cars {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
  }

  /* `.bb-rsvp-cars-typed` (HC-02): the page drew these boxes holding a car
     somebody typed, which is the refused "No". They stay shown, so the
     sentence above the form and the car it is about are both in front of
     the reader. */
  .bb-rsvp-parking:has(input[name="parking"][value="no"]:checked)
    .bb-rsvp-cars:not(.bb-rsvp-cars-typed) {
    display: none;
  }

  /* One body, and the only group that repeats. It keeps a box where the three
     above drop theirs: two adjacent people with no boundary read as one long
     list of name-age-adult-related rows, and the party is where somebody has
     to see at a glance which fields belong to which person. */
  .bb-party-member {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    margin: 0;
    padding: var(--space-4);
    border: var(--border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  /* "Person 1". A row number and nothing else - docs/style-hooks/s6-rsvp.md
     and HOTEL-LEVER-INVISIBLE both say so, and the test that enforces it
     computes the allowed numbers from the component's own inputs. */
  .bb-party-member-legend {
    padding: 0;
    font-weight: 700;
    margin-block-end: var(--space-2);
  }

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

  /* One action per page, so it is full width and it is the primary fill.
     min-block-size is the design's 3rem floor; DESIGN.md measured the mocked
     primary button at 54px, which was 3rem at the phone root until D-130
     made the root 17px (51px). */
  .bb-submit,
  .bb-offer-yes,
  .bb-decline {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    line-height: var(--leading-tight);
    text-align: center;

    &:hover {
      background: var(--color-surface-2);
    }
  }

  .bb-submit {
    margin-block-start: var(--space-3);
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-on-accent);

    &:hover,
    &:active {
      background: var(--color-accent-deep);
      border-color: var(--color-accent-deep);
    }
  }

  /* Identity: is this you? --------------------------------------- */

  /* At most two names, and it never grows - docs/style-hooks/s20-identity.md
     is explicit that two is the whole range, so this is a stack rather than a
     grid that would only look right at some other count. */
  .bb-offers {
    display: grid;
    gap: var(--space-3);
  }

  .bb-offer {
    display: grid;
  }

  /* The name itself is the button label and it is what a phone user aims at,
     so it is the largest control on the site. */
  .bb-offer-yes {
    min-block-size: var(--tap-large);
    padding-inline: var(--space-5);
    font-size: var(--text-lg);
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-on-accent);

    &:hover,
    &:active {
      background: var(--color-accent-deep);
      border-color: var(--color-accent-deep);
    }
  }

  /* "None of these" is a normal path with its own full-width button, not a
     small link under the form. Secondary to the names above it: outlined
     rather than filled, same height, same width. */
  .bb-offer-no {
    display: grid;
    margin-block-start: var(--space-1);
  }

  /* Echoed-back typing on the unrecognised page. Styled as a quotation of
     what the visitor typed rather than as a heading, which is what
     docs/style-hooks/s20-identity.md asks for: it is TEXT and not an input,
     because an editable name there would be a way past the matcher. */
  .bb-typed {
    padding: var(--space-3) var(--space-4);
    border-inline-start: 6px solid var(--color-line-strong);
    background: var(--color-surface-2);
    font-size: var(--text-lg);
    /* An email address has nowhere to break (HC-10 shows the one typed on
       the PIN step). MEASURED at 360 px before this line: a 58-character
       address with no hyphen made the page 640 px wide. */
    overflow-wrap: anywhere;
  }

  /* Answers back to the reader ----------------------------------- */

  /* A refusal, and it must be legible without colour: it carries role="alert"
     and it is the only thing telling somebody their PIN was wrong. So it is
     bold text in a bordered box, and the cranberry is the third signal rather
     than the only one. */
  .bb-form-error {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-cranberry);
    border-radius: var(--radius-sm);
    background: var(--tint-cranberry);
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  /* A write that worked is a toast since D-138: `.bb-toast`, at the end of
     this file. */

  /* The organiser board ------------------------------------------ */

  /* "Minting a new password signs everybody out, including you." A consequence
     stated before the buttons, which is the pattern DESIGN.md 5.8 describes for
     every decision that has one. */
  .bb-warn {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-accent);
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  .bb-facts {
    display: grid;
    gap: var(--space-1) var(--space-4);
    padding: var(--space-4);
    border: var(--border);
    border-radius: var(--radius);
    background: var(--color-surface);

    & dt {
      font-weight: 700;
      color: var(--color-text-muted);
    }

    & dd {
      margin: 0;
    }

    & dd + dt {
      margin-block-start: var(--space-3);
    }
  }

  @media (min-width: 48em) {
    .bb-facts {
      grid-template-columns: max-content 1fr;

      & dd + dt {
        margin-block-start: 0;
      }
    }
  }

  /* Meals -------------------------------------------------------- */

  /* ELEVEN MEALS ON ONE PAGE, which is the whole shape this has to solve. The
     member page and the organiser board draw the same thing and therefore share
     these rules: a stack of cards, one per meal, each holding a heading and
     whatever there is to do.

     The gap between cards is the LARGEST on the site. On a phone, eleven
     bordered boxes in a column need the boundary between two meals to be more
     obvious than the boundary between two dishes inside one, or the page reads
     as one very long list of food. */
  .bb-meals,
  .bb-board {
    display: grid;
    gap: var(--space-6);
    margin: 0;
  }

  /* The closing day's panel at the top of the organiser's board (D-77) is
     drawn as one more card, above the meals it closes, and "Add a meal"
     (D-86) as one more below them. */
  .bb-meal,
  .bb-board-meal,
  .bb-board-closing,
  .bb-board-add {
    display: grid;
    gap: var(--space-3);
    min-inline-size: 0;
    padding: var(--space-4);
    border: var(--border);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  /* The trip dates board (D-86): the days as they stand, and what a move
     touches, each a short list of sentences. */
  .bb-trip-days,
  .bb-trip-changes {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding-inline-start: var(--space-5);
  }

  /* "Thursday dinner". The weekday is how anybody at the cabin talks about a
     meal, and it is the only thing a reader scans for on a page of eleven, so it
     is the one thing here at heading size. */
  .bb-meal-heading,
  .bb-board-heading {
    margin: 0;
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  /* "Thanksgiving dinner". D-8: a caption, never a rule - so it is muted and it
     is not the heading, because a label that looked authoritative is exactly the
     tradition lock the owner said does not exist. */
  .bb-meal-label {
    margin: 0;
    color: var(--color-text-muted);
  }

  /* A meal that is not happening. Pumpkin rather than cranberry: it is a fact
     about the trip and not a refusal, and the note beside it is the only place
     anybody can read why. */
  .bb-meal-cancelled {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-accent);
    max-inline-size: var(--measure);
  }

  /* What was decided. Sage, which is this palette's settled colour, and the
     same treatment on both pages - the organiser's "Settled: turkey, beans" is
     the same fact as the member page's list of winners. */
  .bb-menu,
  .bb-board-settled {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-sage);
    border-radius: var(--radius-sm);
    background: var(--tint-sage);
    font-weight: 700;
  }

  .bb-menu-dish {
    margin: 0;
  }

  /* One round of voting. A meal can carry two - which dish, then how to cook it
     - so each one is its own group with the same internal spacing as a form. */
  .bb-ballot-round,
  .bb-board-round {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
  }

  /* "Tick everything you would be happy with." A word and not only a class,
     which is the rule the rest of this site follows, so it has to READ like an
     instruction: bold, above the boxes it governs. */
  .bb-ballot-heading,
  .bb-board-round-what {
    margin: 0;
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  .bb-ballot,
  .bb-board-dishes {
    display: grid;
    gap: var(--space-2);
    margin: 0;
  }

  /* One dish and its count. The count sits at the END of the row rather than
     beside the name, so eleven meals' worth of numbers line up in a column that
     can be read down.

     `align-items: center` and not `start`: the ballot row's own label is a
     `.bb-check` with its own padding, and a count aligned to the top of that box
     reads as belonging to the row above. */
  .bb-dish,
  .bb-board-dish {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-3);
    min-inline-size: 0;
  }

  /* Since D-106 the member page's `.bb-dish` is laid out by its own rule in
     the voting-page block near the end of this file, as a card with named
     areas; the organiser board's `.bb-board-dish` keeps the two columns
     above. */

  /* A THIRD column on the ballot row only, for the button to the recipe. The
     organiser board's `.bb-board-dish` keeps two, because it has no such link.

     `auto` rather than a width: the word is "Recipe" and it should take what
     it needs.

     THE TRADE, stated rather than discovered. A third column takes width from
     the dish name, so a long name wraps onto two lines where it used to fit on
     one. The alternative was a second ROW carrying the link, which costs
     `--tap-min` of height on every dish instead. A wrapped name is still read;
     a taller page is more scrolling for a reader who is already scrolling
     eleven meals. NOT MEASURED in a browser - no browser exists in the
     environment this was written in - so this is a judgement about two costs
     rather than a measurement of either. D-105 made the link an outlined
     button, which is wider than the word was; what that costs the name is
     measured in D-105's entry in docs/design/CORRECTIONS.md.

     THE COUNT IS THE LAST COLUMN, SINCE D-105. The markup draws the button
     BEFORE the count, so DOM order matches column order and a row WITHOUT a
     recipe, which keeps two columns, still ends in its count. That was the fix
     this comment named as the better one while the wart below stood, and D-105
     took it because the button made the wart wider. `minmax(2ch, auto)` on
     the count, as `.bb-feast .bb-dish` has for the same reason: a count of 12
     beside a count of 7 would otherwise push its button one figure left of the
     rest.

     WAS A KNOWN WART, MEASURED 2026-09-29 AT 390px, LEFT AS IT IS THEN. Because
     this rule is conditional, a row WITH a recipe has three columns and a row
     WITHOUT keeps two, so with the count drawn before the link the count sat in
     a different place on the two kinds of row. On a six-dish round where five
     had a recipe, the five counts aligned and the sixth was pushed right by the
     width of the absent "Recipe" plus its gap.

     AN ATTEMPTED FIX WAS REVERTED AND THE REASON IS STILL WORTH KEEPING.
     Pinning the cells with `grid-column: 2` on the link and `3` on the count
     ALIGNED the counts and BROKE the row: the count came BEFORE the link in
     the DOM, so under the default sparse `grid-auto-flow` the link could not
     back-fill an earlier column and was pushed onto a NEW ROW. Rendered, every
     recipe link floated on its own line under its dish and the page grew 674px
     taller. Reordering the markup is what avoids that: nothing here is pinned.
     RENDER ANY CHANGE TO THIS ROW - the wart and its failed fix were both
     invisible to the whole test suite.

     SUPERSEDED 2026-10-07 BY D-106, which lays the member page's row out as
     a card with named areas (the voting-page block near the end of this
     file): the button has an area of its own under the name on a phone and
     beside it on a wide screen, so there is no conditional column and no
     wart. The rule that stood here, `.bb-dish:has(.bb-dish-recipe)
     { grid-template-columns: 1fr auto minmax(2ch, auto); }`, is deleted
     rather than left to be overridden. The history above is kept because the
     pinned-column failure it records is still the failure a sparse auto-flow
     produces. */

  /* "Recipe", a button to press (D-105, the owner's call: "that needs to be a
     tappable button"). It was a word at the tap floor, muted and small, and on
     a phone it read as text rather than as something to tap.

     The house's outlined control - `.bb-people-act`'s shape: the border, the
     surface behind it, bold, `--tap-min` tall, its own width - and not
     `.bb-panel-action`'s row the width of the column, which inside a dish row
     would be a second row under every dish. Not the accent fill, which is the
     page's one primary action, "Cast votes". No chevron: by
     `.bb-action-chev`'s rule it would take 1.4rem and a gap more from the dish
     name on every row, and the word says where it goes. Smaller words than the dish name and a narrower inset than
     `.bb-people-act`, because the name is the first thing on the row.

     `align-self: center` and not `stretch`, a judgement and not a
     measurement: a bordered box as tall as a dish name that wraps onto four
     lines would read as a panel rather than a button. */
  .bb-dish-recipe {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: var(--leading-tight);
    text-decoration: none;
    white-space: nowrap;
    /* D-106: the row is a card with named areas (the voting-page block near
       the end of this file); the button has the `recipe` area, under the
       name on a phone and beside it from 48em, and starts at the area's
       start. Here and not in a rule of its own, because
       `the_recipe_on_a_dish_is_a_button_to_press_and_not_a_bare_link` reads
       THE one rule for this class. SINCE D-112 (2026-10-08) the button sits
       in the card's strip, `.bb-dish-acts`, a flex row at the card's foot,
       where these two lines do nothing; they are kept so this stays the
       one rule. */
    grid-area: recipe;
    justify-self: start;

    &:hover {
      background: var(--color-surface-2);
      text-decoration: none;
    }
  }

  /* "Take this off the list", on a dish the reader put up themselves.

     A WHOLE SECOND ROW rather than another column, and that is the lesson of the
     wart recorded at `.bb-dish:has(.bb-dish-recipe)` above rather than a
     preference. This row already has three columns on a dish with a recipe, and
     the comment there measures what a third one costs a long dish name at 390px.
     A fourth would take the name down to a third of the width on the rows that
     have one.

     `grid-column: 1 / -1` spans whichever of the two or three tracks the row
     turns out to have, so this one rule is right for a dish with a recipe and a
     dish without. It comes AFTER the count and the link in the markup, so sparse
     auto-flow puts it on a new row on its own - which is the failure mode that
     broke the recipe link when somebody tried to pin the columns, used here on
     purpose instead of fought.

     It keeps `.bb-decline`'s full-width shape, which is what makes it a thumb
     target on a phone rather than a word to hit precisely. The cost is
     `--tap-min` of height, and it is paid only on a row the reader owns - at most
     a dish or two per round.

     THE TIGHTER ROW GAP IS NOT COSMETIC AND LOOKING IS WHAT FOUND IT. The row's
     own `gap` is `--space-3` and the gap BETWEEN rows in `.bb-ballot` is
     `--space-2`, so with the row gap left alone the button sat FARTHER from the
     dish it belongs to than from the next dish down - and a reader would
     reasonably take it as belonging to the dish below. `--space-1` puts it
     against its own dish and leaves the column gap at `--space-3`, which is what
     keeps the count and the recipe link apart on the first line.

     MEASURED 2026-09-29 in Chrome at 390px, six dishes with one of them the
     reader's own: the button is 316x54, its row is 119px tall where a row with a
     name of the same length and no button is 61px, and the five rows without one
     are unchanged. The gap above the button is 5px and the gap below it, to the
     next dish, is 9px - which is the right way round. Before the row gap was
     tightened the same row measured 128px, and the two gaps were the raw
     `--space-3` and `--space-2`, which is 12px above and 8px below. */
  /* SUPERSEDED 2026-10-07 BY D-106: the button has the row's `mine` area
     (the voting-page block near the end of this file), which is the same
     full-width row under the dish that `grid-column: 1 / -1` gave it, and the
     card's own gap is what sits between the dish and its button now. The two
     rules that stood here are deleted rather than overridden. */

  /* The real number, because D-5 puts every ballot in front of every member.
     Tabular figures so a column of counts is a column, and muted because it is
     the second thing on the row - the dish is the first. */
  .bb-dish-count {
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    text-align: end;
  }

  /* "Nobody has suggested a dish yet." Muted, because it is the absence of
     content rather than content - but not smaller, because on the first day of
     voting it is what the whole page says. */
  .bb-dish-empty {
    color: var(--color-text-muted);
  }

  /* The buttons that open a vote: a form of the same shape as every other form
     on this site, a stack of label-then-input pairs ending in one button. Set
     off from the ballot above it, because it is a different action and a phone
     user scrolling past should not mistake it for part of the list.

     A dish row's own button is NOT full width - see the override below.

     `.bb-suggest` LEFT THIS RULE ON 2026-09-29 and has its own below. It used
     to be the typed dish box, which owner decision D-41(e) removed; it is now
     an anchor to the recipe search, so a grid of label-and-input pairs is the
     wrong shape for it. */
  .bb-open-vote {
    display: grid;
    gap: var(--space-2);
    max-inline-size: var(--column-max);
    min-inline-size: 0;
    margin-block-start: var(--space-3);
    padding-block-start: var(--space-3);
    border-block-start: var(--border);
  }

  /* "Nominate a dish", which is now a LINK to the recipe search rather
     than a box to type into. Shaped like the secondary buttons on this site -
     bordered, full width, `--tap-min` tall - because it is the one action under
     a ballot and a bare inline link there reads as a footnote.

     It keeps the separating rule the typed box had, for the same reason: a
     phone user scrolling past the list of dishes should see where the list ends
     and the action begins. */
  .bb-suggest {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    max-inline-size: var(--column-max);
    min-block-size: var(--tap-min);
    margin-block-start: var(--space-3);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    text-align: center;
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
    }
  }

  /* "Browse the recipes", at the top of the meals page and above the eleven
     meals rather than inside any one of them.

     It is the same secondary-button shape as `.bb-suggest` and it is NOT that
     rule, because the two are in different places and want different spacing:
     `.bb-suggest` closes a ballot and carries a leading margin that separates it
     from the list above; this one opens the page and needs a trailing one that
     separates it from the list below.

     ITS UNCONDITIONAL PLACEMENT IS THE POINT AND IT IS NOT A CSS DECISION - see
     `crate::routes::meals::MealsView`. Between votes there is no ballot for a
     `.bb-suggest` to sit under, which is exactly when a reader has no other way
     to the recipes. */
  .bb-all-recipes {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    max-inline-size: var(--column-max);
    min-block-size: var(--tap-min);
    margin-block-end: var(--space-4);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    text-align: center;
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
    }
  }

  /* "How to cook it", which is the one button on this site that sits INSIDE a
     row rather than under a form. Full width and a leading margin would push the
     dish name onto its own line and turn a list of dishes into a list of
     buttons, so this is the narrow case and it is scoped to it. */
  .bb-board-dish .bb-open-vote {
    max-inline-size: none;
    margin: 0;
    padding: 0;
    border: 0;

    & .bb-submit {
      inline-size: auto;
      min-block-size: var(--tap-min);
      margin: 0;
      padding-inline: var(--space-4);
    }
  }

  /* A refusal on the meals page. The same treatment as `.bb-form-error`, which
     it is the meals-page name for: it carries role="alert" and it is the only
     thing telling somebody their ballot was not taken.

     They are one rule rather than two written to match. The hooks differ because
     the pages were written by different slices, and renaming either would be a
     diff across a file the other owns. */
  .bb-problem {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-cranberry);
    border-radius: var(--radius-sm);
    background: var(--tint-cranberry);
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  /* The recovered gate password. Monospace and loosely tracked because it is
     read off a screen and typed into a phone by somebody else, and an l/1 or an
     O/0 mistake there is a relative who cannot get in. Selectable and on a
     tinted ground so the boundaries of the value are visible - a trailing space
     copied by accident is the same failure. */
  .bb-secret {
    font-weight: 700;

    & code {
      display: inline-block;
      padding: var(--space-1) var(--space-2);
      border-radius: var(--radius-sm);
      background: var(--color-surface-2);
      font-size: var(--text-lg);
      letter-spacing: 0.06em;
      overflow-wrap: anywhere;
    }
  }
}

/* ------------------------------------------------------------------ */
/* Pages                                                               */
/* ------------------------------------------------------------------ */

@layer pages {

  /* THE GATE.

     The whole dark treatment is done by RE-POINTING the tokens rather than by
     restating the component rules against a second palette. Every rule above
     already reads its colours from custom properties, so changing the
     properties changes the screen and there is one copy of each rule. That is
     the mechanism docs/mockups/DESIGN.md 3.4 records the separately-written
     dark theme using, applied here to one page instead of the whole site.

     SCOPED WITH `:has()` BECAUSE THIS SLICE ADDS NO MARKUP. There is no body
     class to hang the theme on and adding one would mean editing
     crates/web/src/app.rs, which belongs to another slice. MEASURED by reading
     crates/web/src: `bb-gate` appears in exactly one component, `GateView`, so
     `body:has(.bb-gate)` selects the door and nothing else.

     WHAT HAPPENS IF `:has()` IS MISSING, since the whole page rests on it: the
     selector never matches, the tokens keep their light values, and the gate
     renders as a cream page with a brown-orange button - the same 14.62:1 body
     text and 7.49:1 button as every other page. It loses the drama and keeps
     every measured contrast ratio, which is the correct direction to fail. NOT
     MEASURED in a browser; no browser was run in this slice. */
  body:has(.bb-gate) {
    color-scheme: dark;

    --color-bg: var(--gate-bg);
    --color-surface: var(--gate-input);
    --color-surface-2: var(--gate-input);
    --color-text: var(--gate-text);
    --color-text-muted: var(--gate-text-muted);
    --color-line: var(--gate-line);
    --color-line-strong: var(--gate-line);
    --color-accent: var(--gate-amber);
    --color-accent-deep: var(--gate-amber-pressed);
    --color-on-accent: var(--gate-bg);
    --color-focus: var(--gate-amber);
    --color-link: var(--gate-text);

    min-block-size: 100vh;
    min-block-size: 100dvh;
    display: grid;
    align-content: center;

    /* Two radial glows, warm low-left and cool high-right, over a vertical
       fall from #1A110B to #0D0805. DESIGN.md is explicit that their strength
       is a judgement made from a 360px render and not a measurement. */
    background:
      radial-gradient(70rem 45rem at 15% 115%, var(--gate-glow-warm), transparent 60%),
      radial-gradient(45rem 35rem at 105% -10%, var(--gate-glow-cool), transparent 60%),
      linear-gradient(180deg, var(--gate-bg), var(--gate-bg-deep));
    background-attachment: fixed;
  }

  /* Drama from scale and darkness and one warm light, not from imagery: a
     large serif name with a short amber rule under it. The clamp is the
     mockup's own. */
  body:has(.bb-gate) .bb-title {
    font-size: clamp(3.5rem, 2rem + 7vw, 5.5rem);
    line-height: 0.95;
    letter-spacing: -0.02em;

    &::after {
      content: "";
      display: block;
      inline-size: 3.5rem;
      block-size: 3px;
      margin-block-start: var(--space-4);
      background: var(--gate-amber);
    }
  }

  body:has(.bb-gate) .bb-gate-help {
    color: var(--color-text-muted);
    max-inline-size: 22rem;
  }

  /* The wrong-password state: one calm sentence in an outlined box above the
     field. The screen does not shake, does not flash and does not clear what
     was typed. Amber rather than cranberry because cranberry is a light-theme
     value and was never measured against this ground; amber is, at 9.19:1. */
  body:has(.bb-gate) .bb-gate-wrong {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--gate-amber);
    border-radius: var(--radius);
    background: rgb(242 166 90 / 0.12);
    font-weight: 700;
  }

  /* The field and the button are the two things on the screen, and DESIGN.md
     measured them at 67px and 68px in the mockup. 3.75rem is 67.5px at the
     phone root, which is where those two numbers came from. */
  body:has(.bb-gate) .bb-input,
  body:has(.bb-gate) .bb-submit {
    min-block-size: 3.75rem;
    border-radius: var(--radius);
    font-size: var(--text-xl);
  }

  body:has(.bb-gate) .bb-input {
    letter-spacing: 0.12em;
  }
}

  /* ------------------------------------------------------------------------
     The drawer.

     NO JAVASCRIPT ON THIS SITE, so the open/closed state is a checkbox and the
     controls are labels pointed at it. That is not a workaround for a missing
     bundle: it keeps working with the page half-loaded, on a dead connection,
     and with the browser's own focus and keyboard handling rather than a
     reimplementation of them.

     Below 64em the drawer is off-canvas and a thumb opens it. At 64em and up it
     is simply always there and the button, the scrim and the close label are
     not rendered visible - the same markup, no duplicate nav.
     ------------------------------------------------------------------------ */

  /* Off-screen but still focusable, so a keyboard reaches the control. Not
     `display: none`, which would take it out of the tab order entirely. */
  .bb-nav-state {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The button, NOT the bar. It used to be both - sticky, full-width surface,
     its own bottom border - because there was no header for it to sit in. Now
     `.bb-head` is the bar, and leaving the bar-ness here painted a stub
     underline beneath the word "Menu" that stopped where the label stopped.
     Measured in a browser at 390px before removing it. */
  .bb-nav-open {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-block-size: var(--tap-large);
    color: var(--color-text);
    font-weight: 600;
    cursor: pointer;
  }

  /* Three bars drawn rather than an icon font or an inline SVG: one element,
     no extra request, and it cannot fail to load. */
  .bb-nav-bars {
    inline-size: 1.25rem;
    block-size: 2px;
    background: currentColor;
    box-shadow: 0 6px 0 currentColor, 0 -6px 0 currentColor;
  }

  .bb-nav {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 5;
    inline-size: min(17rem, 82vw);
    /* No padding along the top since D-106: the banner below reaches the
       drawer's edges, and it carries the space the links need under it. */
    padding: 0 var(--space-4) var(--space-5);
    background: var(--color-surface);
    border-inline-end: 1px solid var(--color-line);
    overflow-y: auto;
    /* See the scroll lock above: this is the half that stops a swipe inside
       the drawer chaining onto the page once the drawer reaches its end. */
    overscroll-behavior: contain;
    translate: -100% 0;
    transition: translate 180ms ease;
  }

  .bb-nav-scrim {
    position: fixed;
    inset: 0;
    z-index: 4;
    background: var(--scrim-ink);
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease;
  }

  .bb-nav-state:checked ~ .bb-nav {
    translate: 0 0;
  }

  .bb-nav-state:checked ~ .bb-nav-scrim {
    opacity: 0.45;
    pointer-events: auto;
  }

  /* SWIPING THE MENU ALSO SWIPED THE PAGE UNDERNEATH IT. The owner's report,
     on a phone, and there was nothing here to prevent it: MEASURED 2026-09-28
     by reading this file, `overscroll` appeared zero times and the only rules
     keyed on `.bb-nav-state:checked` were the two above.

     Two separate movements, so two declarations. Both MEASURED 2026-09-28 in
     headless Chromium at 390x844 with the drawer open, driving real wheel
     events through the input pipeline rather than calling `window.scrollBy` -
     which is the wrong probe, because `overflow: hidden` deliberately still
     permits a PROGRAMMATIC scroll and reported 500px of movement on a page a
     finger cannot move at all:

     1. `overflow: hidden` on the body stops the page scrolling at all while the
        drawer is open. That covers a drag that starts on the scrim, which is
        `position: fixed` and scrolls nothing itself, so the gesture reaches the
        document behind it. MEASURED with the pointer on the scrim: the page
        moved 800px before this rule and 0px after it.
     2. `overscroll-behavior: contain` on the drawer stops SCROLL CHAINING. The
        drawer is `overflow-y: auto` and taller than a small phone, so a swipe
        that runs it to its end hands the rest of the gesture to the page
        behind. MEASURED with three wheels over the drawer itself: before, the
        first filled the drawer and the next two took the page to 600 and then
        1200; after, the drawer reached its end and the page stayed at 0.

     The control that makes those numbers mean anything: with the drawer CLOSED
     the page still moves 800px, before and after. A lock that worked by making
     the page unscrollable everywhere would read identically without it.

     `body:has(.bb-nav-state:checked)` rather than a class, which is the same
     mechanism `body:has(.bb-gate)` has used in production since the door screen
     got its own palette. No JavaScript, and no state anywhere except the
     checkbox that already exists.

     This is UNDONE at 64em, where the drawer is a permanent sidebar and the
     checkbox means nothing - see the media query at the end of this block.
     Without that reset a reader who opened the drawer on a phone and then
     rotated onto a wide layout would be left unable to scroll the page. */
  body:has(.bb-nav-state:checked) {
    overflow: hidden;
  }

  /* The keyboard focus ring has to follow the LABEL, because the input it
     drives is visually hidden. */
  .bb-nav-state:focus-visible ~ .bb-nav-open {
    outline: 3px solid var(--color-focus);
    outline-offset: -3px;
  }

  .bb-nav-close {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
    margin-block-end: var(--space-3);
    color: var(--color-link);
    font-weight: 600;
    cursor: pointer;
  }

  /* D-106: the drawer opens on the sky, the header's own, with a row of pines
     along the banner's foot and the pine rule under them. "Close" sits on the
     plain sky above the trees, never on them. The negative inline margins
     take the banner to the drawer's edges through the padding above. */
  .bb-nav-head {
    display: grid;
    margin: 0 calc(var(--space-4) * -1) var(--space-4);
    background: var(--scene-sky);
    border-block-end: 0.3rem solid var(--color-pine);

    & > .bb-nav-close {
      justify-self: start;
      margin: var(--space-2) var(--space-4) 0;
    }

    & > .bb-treeline {
      block-size: 2.25rem;
    }
  }

  /* The entry for the page you are already on. A span, not a link, so it is not
     a control that does nothing when tapped. */
  .bb-here {
    display: flex;
    align-items: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: var(--border);
    border-inline-start: 3px solid var(--color-accent);
    border-radius: var(--radius-sm);
    background: var(--tint-harvest);
    color: var(--color-text);
    font-weight: 600;
  }

  /* The light-or-dark switch, at the bottom of the drawer.
     ------------------------------------------------------------------------

     A FORM, NOT A SCRIPT. It posts, the server writes a cookie and sends the
     page back, and the palette is right on the first paint of the next page
     because the cookie arrives with the request. Nothing here depends on the
     browser bundle, which is on one of the nine pages the drawer lists.

     The rule above it is the only separation: the drawer is a short list and a
     second panel inside it would read as another site. */
  .bb-theme {
    margin-block-start: var(--space-5);
    padding-block-start: var(--space-4);
    border-block-start: 1px solid var(--color-line);
  }

  /* A fieldset's browser default is an inset border and its own padding, and
     one drawn inside the drawer's edge reads as a box within a box. The
     grouping is carried by the legend and the rule above instead.
     `min-inline-size: 0` because the default is `min-content`, which stops a
     fieldset shrinking to a narrow drawer. */
  .bb-theme-set {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
  }

  /* Same treatment as the wordmark's second line, which is the other small
     caption in this chrome. `--text-sm` and not a smaller literal: that token
     is this file's declared floor. */
  .bb-theme-legend {
    padding: 0;
    margin-block-end: var(--space-2);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* The two you can press and the one in force share a box, so choosing does
     not make the group jump. `--tap-min` is the site's own thumb floor. */
  .bb-theme-pick,
  .bb-theme-here {
    display: flex;
    align-items: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: var(--border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-link);
    font-weight: 600;
    text-align: start;
  }

  /* The choice in force, marked exactly like the page you are on: an accent
     edge and the plain text colour, because it is not a control. */
  .bb-theme-here {
    border-inline-start: 3px solid var(--color-accent);
    background: var(--color-bg);
    color: var(--color-text);
    cursor: auto;
  }

  /* Sign out, and "this is not me" on a shared device (D-90). Under the
     pages and above the light-or-dark switch, set off by the same rule, so it
     is on the first screen of an open drawer and, on a wide screen where the
     drawer is a sidebar, on every page without opening anything. The reader's name is on it, which is what makes it the
     "not me" path: the next relative on the family iPad reads whose site it
     is. The button is the outlined secondary shape, at the thumb floor. */
  .bb-signout {
    display: grid;
    gap: var(--space-2);
    margin-block-start: var(--space-5);
    padding-block-start: var(--space-4);
    border-block-start: 1px solid var(--color-line);
  }

  .bb-signout-who {
    margin: 0;
    color: var(--color-text);
  }

  .bb-signout-go {
    display: flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
  }

  .bb-signout-hint {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  @media (prefers-reduced-motion: reduce) {
    .bb-nav,
    .bb-nav-scrim {
      transition: none;
    }
  }

  /* Wide enough for the drawer to just live on the page. */
  @media (min-width: 64em) {
    .bb-nav-open,
    .bb-nav-close,
    .bb-nav-scrim {
      display: none;
    }

    /* The checkbox still holds whatever it held on the phone, and here it means
       nothing: the drawer is a permanent sidebar and there is no overlay to
       lock the page behind. Leaving the lock on would strand a reader who
       opened the menu at 390px and then rotated, with a page that will not
       scroll and no visible control to release it. */
    body:has(.bb-nav-state:checked) {
      overflow: visible;
    }

    .bb-nav {
      position: fixed;
      translate: 0 0;
      border-inline-end: 1px solid var(--color-line);
    }

    /* Only the pages that HAVE a drawer get moved over. A page with no nav -
       the gate screen - is untouched by this rule because it renders no
       `.bb-nav` sibling for `:has()` to find. */
    body:has(.bb-nav) .bb-main {
      margin-inline-start: min(17rem, 82vw);
    }
  }

/* ------------------------------------------------------------------ *
 * The header, the footer, and the page furniture that was missing.
 *
 * WHY THIS EXISTS. Looked at every member page in a browser at 390px on
 * 2026-09-27, which is the loop for this project. The verdict, and it was the
 * owner's before it was mine: it read as a wireframe rather than a site. The
 * specific faults, each fixed below:
 *
 *   1. No header. A bare "Menu" floated on the background and nothing but
 *      `/home` said what the site was or which week it was about.
 *   2. Broken hierarchy. On `/sleeping` the `h2` rendered LARGER and BOLDER
 *      than the `h1` above it, because nothing set a size for either and the
 *      browser default for h1-inside-section is smaller than a bare h2.
 *   3. Everything was bare text on a flat field. No surface, no grouping.
 *      Five of six pages are an empty state right now, and an empty state with
 *      no container reads as a page that failed to load rather than one that
 *      is waiting for content.
 *   4. The palette was defined and almost entirely UNUSED. Accent, pumpkin,
 *      cranberry, sage and four tints were sitting in `:root` doing nothing.
 * ------------------------------------------------------------------ */

@layer components {

  /* Sticky so the way out is always reachable with a thumb, on a page that
     may be a long list of meals. */
  .bb-head {
    position: sticky;
    inset-block-start: 0;
    z-index: 3;
    /* D-106e: on a phone the header is two rows: the menu button and the
       words, then a band of pines the whole width (`.bb-head-band`) standing
       on the rule. The pines beside the words (`.bb-head-art`) are not drawn
       here, because at 390 the words left them 2px (MEASURED, D-106a). The
       row has no padding under it: the band is what sits there, and its
       tallest tree starts 6 units into its 28, which is the sky between the
       button's foot and the treetops. */
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--space-3);
    align-items: center;
    padding: var(--space-2) var(--space-4) 0;
    /* D-106: the sky the dashboard's banner is drawn on, with a pine rule
       under it, so the top of every page is the cabin's and not a plain bar.
       The wordmark on the sky is the title's pair: `--color-text` on
       `--scene-sky` is the ratio D-102 recorded, 12.97:1 light and 12.54:1
       dark. The rule is the card edge's width, `.bb-cabin`. */
    background: var(--scene-sky);
    border-block-end: 0.3rem solid var(--color-pine);
  }

  /* From 40em, which is 640px (a media query's em is the browser's default
     16px, not this page's 18px root), the header is the one row D-106a
     drew, the pines beside the words, which have the room there: 252px of
     them at 640 and 332px at 720; at 639 the band is still the one drawn
     (MEASURED in Chrome). */
  @media (min-width: 40em) {
    .bb-head {
      display: flex;
      gap: var(--space-4);
      padding: var(--space-3) var(--space-4);
    }
  }

  /* The name, with the cabin beside it (D-106). The drawing is the dashboard's
     "Where the cabin is" mark in a larger box, in pine; the words stack as
     before, the year under the name. */
  .bb-wordmark {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    line-height: 1.1;
    color: var(--color-text);
  }

  .bb-wordmark-mark {
    flex: none;
    display: grid;
    inline-size: 1.85rem;
    block-size: 1.85rem;
    color: var(--color-pine);

    & > svg {
      inline-size: 100%;
      block-size: 100%;
      fill: currentColor;
    }
  }

  .bb-wordmark-words {
    display: flex;
    flex-direction: column;
  }

  /* The pines beside the wordmark (D-106): the drawing takes whatever width
     the header has left after the button and the words, down to none at all
     on a screen too narrow for any, and stands as tall as the row. The words
     are never on it: it starts where they end. */
  .bb-head-art {
    /* Not on a phone (D-106e): the band below is the phone's pines. */
    display: none;
    flex: 1 1 0;
    min-inline-size: 0;
    align-self: center;
    /* A set height, the menu button's: with `auto` the drawing took its
       height from its own proportions once flex had given it the width, and
       the header stood 165px tall at 1280 (MEASURED in Chrome; 95px at 390,
       where the button is the tallest thing in the row). */
    block-size: var(--tap-large);
  }

  /* D-106e: the phone's pines, a band the whole width of the header on a row
     of its own under the button and the words, past the header's padding so
     the forest runs edge to edge, standing on the pine rule. 1.5rem tall,
     which with the row's top padding puts the header at 104px at 390 against
     95 without it (MEASURED in Chrome). From 40em the pines beside the
     words take over and this is not drawn. */
  .bb-head-band {
    display: block;
    grid-column: 1 / -1;
    inline-size: calc(100% + 2 * var(--space-4));
    block-size: 1.5rem;
    margin-inline: calc(var(--space-4) * -1);
  }

  @media (min-width: 40em) {
    .bb-head-art {
      display: block;
    }

    .bb-head-band {
      display: none;
    }
  }

  /* The dates every relative is actually trying to remember. */
  /* The year, under the name. `--text-sm` and not a smaller literal: that
     token is this file's own declared floor, 16px at the phone root then and
     15.1px since D-130, and this line was 13.5px - MEASURED 2026-09-28 at
     390, and 15px at 1024. It is the
     line most likely to be read by somebody holding the phone at arm's length,
     which is the population this site is for. */
  .bb-wordmark-when {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* The dates, which is the fact every relative is trying to remember, so this
     is not a place to be under the floor either: it was 14.625px at 390. */
  .bb-foot {
    /* `inline-size: 100%` for the same reason `.bb-main` above carries it.
       Without it the footer is a flex item with auto inline margins, which
       disables the stretch and shrinks the box to the width of the sentence
       inside it - MEASURED at 1024: 430px against the column's 600, so it read
       as aligned on the left and centred on nothing. */
    inline-size: 100%;
    max-inline-size: var(--column-max);
    margin-inline: auto;
    padding: var(--space-5) var(--space-4) var(--space-8);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    text-align: center;
  }

  /* D-106: a row of pines along the foot of the page, drawn in the scene's
     own colours so the dark theme draws it at night, and the dates under the
     snow. The rule the footer had along its top is now the snow's edge. */
  .bb-treeline {
    display: block;
    inline-size: 100%;
    block-size: 2.75rem;
  }

  .bb-foot-when {
    margin: 0;
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--color-line);
  }
}

/* ------------------------------------------------------------------ *
 * The wide layout, and three things it got wrong.
 *
 * All three MEASURED 2026-09-28 in a browser at 1024 and 1440, against pages
 * with data in them. They share one cause: the drawer became a permanent
 * 340px sidebar at 64em and only `<main>` was told about it.
 * ------------------------------------------------------------------ */

@layer pages {

  @media (min-width: 64em) {

    /* THE WORDMARK WAS INVISIBLE, on every page. `.bb-nav` is fixed, opaque
       and `z-index: 5`; `.bb-head` is `z-index: 3`; and at this width the menu
       button is not rendered, so the wordmark is the first thing in the header
       and sits entirely inside the nav's rectangle. MEASURED with
       `elementFromPoint` at the centre of `.bb-wordmark`: it returned a nav
       LINK, not the wordmark. What a reader saw was a 72px empty white bar
       above every page.

       The padding clears the sidebar rather than raising the header above it.
       Raising it would put the header's surface over the top of the nav and
       cut the first entry in half; this leaves the sidebar whole and starts the
       wordmark on the same vertical line as the page heading below it. */
    body:has(.bb-nav) .bb-head {
      padding-inline-start: var(--space-4);
      /* CHANGED BY D-106: the header starts at the sidebar's edge rather
         than running under it with padding to clear it, so the sidebar's
         own banner and the header's sky are two things side by side and
         nothing of the header is under the sidebar. The wordmark's
         visibility is measured again in D-106's record. */
      margin-inline-start: min(17rem, 82vw);
    }

    /* THE FOOTER CENTRED ON THE VIEWPORT AND IGNORED THE SIDEBAR. MEASURED:
       it missed the content column by -128px at 1024 and +80px at 1440, which
       on a page whose only footer content is the trip dates reads as a caption
       belonging to nothing. Same offset as `.bb-main` gets above, for the same
       reason, so the two stay aligned at every width rather than at the one
       that was checked. */
    body:has(.bb-nav) .bb-foot {
      margin-inline-start: min(17rem, 82vw);
    }
  }

  /* THE FOOTER FLOATED IN THE MIDDLE OF A SHORT PAGE. MEASURED on an empty
     driveway at 390: 363px of background below the footer, because `<body>` is
     `min-block-size: 100vh` and nothing made the content fill it. A page that
     is an empty state already looks like it failed to load; a footer halfway up
     it says so twice.

     Scoped to pages that HAVE the drawer, which is the same guard `.bb-main`'s
     offset uses: the gate screen is `display: grid; align-content: center` and
     centring its one question is the whole design of that screen. */
  body:has(.bb-nav) {
    display: flex;
    flex-direction: column;
  }

  body:has(.bb-nav) .bb-main {
    flex: 1 0 auto;
  }
}

@layer pages {

  /* HIERARCHY, and this is the one that was actually broken rather than
     merely plain. Both are sized explicitly so the relationship cannot depend
     on a browser's default for a heading inside a sectioning element. */
  .bb-title {
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 1.35rem + 2vw, 2.25rem);
    line-height: 1.15;
    font-weight: 400;

    /* D-106: the short rule the gate draws under its name, under every
       page's title, in the harvest's colour. Not text, so no ratio applies.
       The gate's own rule (amber, in `body:has(.bb-gate)`) is more specific
       in the same layer and still wins there. */
    &::after {
      content: "";
      display: block;
      inline-size: 3rem;
      block-size: 3px;
      margin-block-start: var(--space-2);
      border-radius: 2px;
      background: var(--color-pumpkin);
    }
  }

  .bb-main h2 {
    font-size: 1.125rem;
    line-height: 1.25;
    font-weight: 600;
    color: var(--color-text);
  }

  .bb-main h3 {
    font-size: 1rem;
    font-weight: 600;
  }

  /* A page-level note gets a surface. Scoped to a DIRECT child of `<main>` on
     purpose: the same class is used for instructions INSIDE forms, where a
     card around one sentence would be noise. This selector catches exactly the
     "nothing here yet" case and leaves the rest alone. */
  .bb-main > .bb-note {
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-inline-start: 3px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text-muted);
  }

  /* The pot. It was four unstyled lines of text - a label, a number, a label,
     a number - which is the one place on this site where a number is the whole
     point and it read as a paragraph. */
  .bb-pot-totals {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-3);
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  .bb-pot-label {
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  .bb-pot-amount {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.75rem;
    line-height: 1.1;
    color: var(--color-accent);
  }

  .bb-pot-from {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  /* The feed and the lists sit on surfaces too, so a page with content and a
     page with none look like the same site. */
  .bb-feed,
  .bb-meals {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* `.bb-post` and not `li`. Since D-54 the feed has TWO kinds of child, a
     post (`PostItem` in `crates/web/src/routes/home.rs`) and a line saying
     what a relative did (`DidItem`, `.bb-did` below), and only a post is a
     card. Naming the class says which child this styles. It is also what stops
     `bb-post` reading as an uncovered hook: the card is a property of being IN
     the feed rather than of the post itself, which is the case the driveway
     block at the end of this file cites. */
  .bb-feed > .bb-post,
  .bb-meal {
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-meal-heading {
    margin-block-end: var(--space-1);
  }

  .bb-meal-label {
    color: var(--color-text-muted);
  }
}

/* ------------------------------------------------------------------ *
 * The RSVP page, restyled against docs/mockups/preview-light/rsvp.html.
 *
 * The mockups are a STYLE reference and nothing else - the owner said so in
 * terms. So this takes the visual treatment and leaves the wording and the
 * behaviour exactly as the application already has them. Nothing here changes
 * what the page asks, what it accepts, or what it does with an answer.
 *
 * WHAT THE MOCKUP HAS THAT THE SHIPPED PAGE LOST, measured by rendering both
 * at 390px on 2026-09-27: the questions are not grouped, the legends read as
 * bold body text rather than headings, the notices have no treatment at all,
 * and the primary action is an ordinary button at the bottom of a long scroll
 * instead of something always reachable with a thumb. Same content, every
 * affordance stripped off - which is what "clunky" was.
 * ------------------------------------------------------------------ */

@layer pages {

  /* Each question is its own group. A fieldset with no treatment is why four
     separate questions read as one undifferentiated column of controls. */
  .bb-rsvp > fieldset {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  /* A legend IS the question. It was rendering as bold body text, which put it
     at the same visual weight as the option labels underneath it. */
  /* A legend sits IN the border by default, so it struck through the card edge.
     Giving it the card's own background and a little horizontal padding is what
     makes it read as a heading in a gap rather than text over a line.
     SUPERSEDED for the padding, margin and background by D-114 at the end of
     this file, which takes the legend off the border and inside the card. */
  .bb-rsvp legend {
    padding-inline: var(--space-2);
    margin-inline-start: calc(var(--space-2) * -1);
    background: var(--color-surface);
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--color-text);
  }

  .bb-rsvp > fieldset > .bb-check,
  .bb-rsvp > fieldset > label {
    margin-block-start: var(--space-3);
  }

  /* The party rows are a different shape from the questions - they repeat, and
     they need to read as a stack of people rather than a fourth question. */
  .bb-party-member {
    border-style: dashed;
  }

  /* "Person 1", and it has to stay the quietest thing in the card.
     `legend.` IS THE FIX AND IT IS NOT DECORATION. `.bb-rsvp legend` above is
     (0,1,1) and this was (0,1,0), so the question legend won three of this
     rule's declarations by specificity - MEASURED 2026-09-28 in a browser:
     `.bb-party-member-legend` computed to 19.125px, weight 600, in body
     colour, which is the question legend exactly. The comment on this rule in
     the components layer says it exists so a row number does not read as a
     question, and it was rendering as one. */
  .bb-rsvp legend.bb-party-member-legend {
    font-size: var(--text-sm);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* THE PRIMARY ACTION, always reachable. This form is a long scroll on a
     phone - four questions plus a row per person - and the button sat at the
     bottom of it. Sticky, full width, accent filled, with a safe-area inset so
     it clears the home indicator on a phone that has one.

     The Thanksgiving ballot's "Cast votes" is the same case, and owner
     decision D-65 gave it the same rule rather than a copy of it (the
     sleeping form's "Send" was the third until D-116 took the form off
     /sleeping). Sticky binds to the form, so each button sticks only while
     its own form is on the screen. Both are children of their form, which the
     child combinator relies on and `the_long_forms_keep_their_button_in_reach`
     checks against the rendered pages. */
  .bb-rsvp .bb-submit,
  .bb-feast > .bb-submit {
    position: sticky;
    inset-block-end: 0;
    inline-size: 100%;
    margin-block-start: var(--space-2);
    padding-block: var(--space-4);
    padding-block-end: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    border-radius: var(--radius);
    font-size: 1.0625rem;
    box-shadow: 0 -0.5rem 1rem var(--color-bg);
  }
}

@layer components {

  /* Notices. Both classes existed and neither had a rule, so a refusal and a
     confirmation rendered as ordinary paragraphs - the two moments on this
     page where a person most needs to be told something. The confirmation is
     a toast since D-138 (`.bb-toast`, at the end of this file); the refusal
     stays here, in place. */
  .bb-form-error {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid;
    border-inline-start-width: 4px;
    border-radius: var(--radius-sm);
    font-weight: 500;
  }

  .bb-form-error {
    border-color: var(--color-cranberry);
    background: var(--tint-cranberry);
    color: var(--color-text);
  }

  /* D-82. A write lands on the row or card it was about - "Add it to the list"
     on the row it added, a ballot on its meal's card, a car on the cars panel -
     and the header is sticky, so a browser scrolling an anchor to the top of
     the screen puts it UNDER the header, and the line saying it was saved is
     the first thing hidden. MEASURED in Chrome on /bring: the header is 91px
     tall at 390 and 75px at 1280. 6rem was 108px and 120px there, because the
     root was 112.5% and then 125%, so it cleared the header at both with
     room for the line to read as under it rather than against it. Since
     D-130 the root is 106.25% and 118.75% (above): 6rem is 102px and 114px,
     against a header MEASURED in Chrome on Home after D-130 at 98.5px tall
     at 360 and 412 and 100px at 1280 (104px and 106px before it), so it
     still clears, by 3.5px on a phone.
     A post on Home is a landing too (D-91, D-129: `#bb-post-<id>`) and had
     no margin: MEASURED in Chrome at 360 and 412 before D-132, each post
     landed on sat 98 to 99px under the header, and on the shortest one drawn
     (138px, a line of words and no heart) its heart button was under it too.
     D-129's sentence on that post made it 231px at 412 and its button clear;
     D-132 took the sentence away. */
  [id="bb-added"],
  [id="cars"],
  .bb-meal,
  .bb-feed > .bb-post {
    scroll-margin-block-start: 6rem;
  }

  /* D-124. "Cancel this meal" lands on the confirm it draws, by its anchor,
     so the sentence and the box are not under the sticky header. */
  .bb-cancel-ask {
    scroll-margin-block-start: 6rem;
  }

  /* The menu is a control, so it should look like one. It was bare text.
     Since D-106 it sits on the header's sky, so it carries the surface too. */
  .bb-nav-open {
    /* D-106e: a little tighter on a phone, the same height. With the pines
       moved under the row, "THANKSGIVING 2026" beside the cabin is one line
       at 360 with this and wraps without it (MEASURED in Chrome at 360: one
       line, 162px, with this; two lines in a 156px box with the band and the
       old padding; one line either way at 390). */
    gap: var(--space-2);
    padding-inline: var(--space-2);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  @media (min-width: 40em) {
    .bb-nav-open {
      gap: var(--space-3);
      padding-inline: var(--space-3);
    }
  }

  /* The one sentence under a page heading that says what the page is for. */
  .bb-intro {
    max-inline-size: var(--measure);
    margin-block-start: calc(var(--space-2) * -1);
    color: var(--color-text-muted);
  }
}
/* ------------------------------------------------------------------ *
 * The bring list at /bring.
 *
 * Taken from docs/mockups/preview-light/bring.html, which is a STYLE reference
 * and nothing else - the owner said so in terms. What moves across is the
 * treatment: two sections with a heading each and a title line above a muted
 * meta line. What does NOT move across is the mockup's content or its
 * behaviour - no chip filters and no badges.
 *
 * D-63 (2026-10-01) made each item a ROW with its button beside the words, put
 * the add form at the top, and gave an item that needs several of something a
 * count, shares and disclosures. The owner chose the rows from a design
 * review's mocks; that review measured the old full-width button under every
 * item as most of the page's length.
 *
 * ONE CARD SHAPE, SHARED WITH THE FEED AND THE MEALS LIST, and that is on
 * purpose rather than for brevity: a member arriving from /home has just read
 * two lists drawn that way, and a third shape would read as a different site.
 * ------------------------------------------------------------------ */

@layer components {
  /* A section of the list. The gap between sections is larger than the gap
     between cards inside one, for the reason the meals block states about
     eleven bordered boxes: the boundary between "needs somebody" and "sorted"
     has to be more obvious than the boundary between two things. */
  .bb-bring {
    display: grid;
    gap: var(--space-3);
    margin-block-start: var(--space-6);
  }

  .bb-bring-heading {
    margin: 0;
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  .bb-bring-list {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* D-63a. ONE ROW PER ITEM. The padding is smaller than a feed card's, because
     a row on this page is a line of a list rather than a thing to read, and
     the review measured the old full-width button under every item as most of
     the page's length. */
  .bb-bring-item {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  /* D-73. A row with a card: the words on the left and a chevron at the
     end, and nothing to press but the row. Every control is in the card a
     tap on the row opens. `--tap-min` tall at least, because the whole row
     is the control. No gap between the rows of the grid: the `<details>`
     after the words takes no room while it is closed, and a gap would still
     be drawn above it. Positioned, so the summary lies over it. */
  .bb-bring-opens {
    position: relative;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--space-2);
    row-gap: 0;
    min-block-size: var(--tap-min);

    & > .bb-bring-what {
      grid-column: 1;
    }
  }

  /* "This opens", at the end of the row, in the muted text colour. Drawn by
     the SVG's stroke in the current colour. It turns down while the card is
     open in the row, which is how the page without the bundle shows it. */
  .bb-bring-chev {
    grid-column: 2;
    grid-row: 1;
    display: grid;
    place-items: center;
    inline-size: 1.4rem;
    block-size: 1.4rem;
    color: var(--color-text-muted);
    transition: rotate 180ms ease;

    & > svg {
      inline-size: 100%;
      block-size: 100%;
      fill: none;
      stroke: currentColor;
      stroke-width: 2.2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
  }

  .bb-bring-opens:has(> .bb-bring-open[open]) > .bb-bring-chev {
    rotate: 90deg;
  }

  /* The card's `<details>`: under the words, across the row. */
  .bb-bring-open {
    grid-column: 1 / -1;
    min-inline-size: 0;
  }

  /* D-73h. The summary lies over the whole row, border included, so a tap
     anywhere on the row opens the card and the target is the row's full
     height; it holds no words of its own and is named by the title. The
     focus ring the base layer gives every control is drawn round the row.
     MEASURED in Chrome at 390: with `inset: 0` it stopped inside the row's
     1px border and was 52px tall on a row 54px tall. */
  .bb-bring-tap {
    position: absolute;
    inset: -1px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }
  }

  /* D-73h. THE CARD IN THE ROW, which is how a page without the bundle draws
     it: a `<dialog>` sent open, so drawn in place, under the row's words,
     with none of a dialog's own box. Positioned above the summary that lies
     over the row, or the summary would take every tap meant for its buttons.
     Its heading and words repeat the row's, which are just above it here, so
     they are not drawn. */
  .bb-bring-card {
    position: relative;
    z-index: 1;
    inset: auto;
    display: grid;
    gap: var(--space-2);
    inline-size: auto;
    max-inline-size: none;
    block-size: auto;
    margin: var(--space-2) 0 0;
    padding: 0 0 var(--space-1);
    border: 0;
    background: none;
    color: inherit;

    &:not([open]) {
      display: none;
    }

    & > .bb-bring-card-head {
      display: none;
    }
  }

  /* D-73h. THE CARD AS A MODAL, which is how the bundle shows it, on a phone
     and on a desktop alike: a card in the middle of the screen over a dimmed
     page, the veil the drawer uses. Keyed on a class the browser side puts on
     rather than on `:modal`, so a browser too old for `:modal` still draws
     the card in the row above. `margin: auto` is what centres a modal
     dialog, and the base layer's `* { margin: 0 }` takes it away: MEASURED in
     Chrome on 2026-10-01 for D-64d, without it the card sat in the top left
     corner of the screen. */
  .bb-bring-card.bb-bring-card-modal {
    position: fixed;
    inset: 0;
    gap: var(--space-3);
    inline-size: min(22rem, calc(100vw - 2rem));
    block-size: fit-content;
    max-block-size: calc(100dvh - 2rem);
    margin: auto;
    padding: var(--space-5) var(--space-4) var(--space-4);
    overflow: auto;
    border: var(--border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);

    &::backdrop {
      background: var(--scrim-ink);
      opacity: 0.45;
    }

    & > .bb-bring-card-head {
      display: grid;
      gap: var(--space-1);
    }
  }

  .bb-bring-card-title {
    margin: 0;
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
    overflow-wrap: anywhere;
  }

  .bb-bring-card-words {
    margin: 0;
    color: var(--color-text-muted);
  }

  /* D-125. On the asker's own thing that somebody is bringing, why it can
     only grow, just above its one control. The card's words, kept to the
     measure, and shown in the row as well as in the modal: it is not part of
     the card's head, which the row hides. */
  .bb-bring-held {
    max-inline-size: var(--measure);
  }

  /* Every button in the card across the card, the way the add form's is.
     The asker's "We do not need this" and the disclosure are text-style and
     keep their own width. */
  .bb-bring-card .bb-bring-act > .bb-submit,
  .bb-bring-card .bb-bring-act > .bb-decline {
    inline-size: 100%;
    margin: 0;
  }

  /* "Cancel", sent hidden: shown only by the browser side, because without
     it nothing could close the card from inside. An author `display` would
     beat the user agent's `[hidden]`, so the rule says it. */
  .bb-bring-card-close {
    margin: 0;

    &[hidden] {
      display: none;
    }

    /* At least as wide as it is tall: MEASURED in Chrome at 390, the word
       alone was 50px wide. */
    & > .bb-bring-text {
      min-inline-size: var(--tap-min);
    }
  }

  /* D-68. The reader's own row: a star in the top left corner says "you
     asked for this", in place of an "Asked for by you" line. Positioned, so
     the star is placed against it. Its title starts a little lower, under the
     star, as in the design E demo the owner chose from; D-68's title line was
     a control's height and kept clear of the star that way, and the row has
     no control on it now. */
  .bb-bring-yours {
    position: relative;
  }

  .bb-bring-yours .bb-bring-title {
    padding-block-start: var(--space-2);
  }

  /* The star, drawn by the SVG in the markup and filled with this colour.
     The accent, the button colour, because it marks the reader's own
     things: 7.49:1 against the card in the light theme and 7.48:1 in the
     dark, computed with the WCAG 2 formula from the tokens, where a mark
     like this needs 3:1. */
  .bb-bring-star {
    position: absolute;
    inset-block-start: var(--space-1);
    inset-inline-start: var(--space-1);
    display: flex;
    inline-size: 0.75rem;
    block-size: 0.75rem;
    color: var(--color-accent);

    & > svg {
      inline-size: 100%;
      block-size: 100%;
      fill: currentColor;
    }
  }

  /* D-73j. The practice row: dashed, on the sand tint, so it does not read as
     a thing somebody asked for, with "Hide this" at its end. */
  .bb-bring-practice {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--space-3);
    border: 2px dashed var(--color-line-strong);
    background: var(--tint-sand);
  }

  .bb-bring-practice .bb-bring-asker {
    color: var(--color-text);
  }

  /* Sent `hidden`: an author `display` beats the user agent's `[hidden]`, so
     this says it, and only the phone block below draws the row. */
  .bb-bring-practice[hidden] {
    display: none;
  }

  .bb-bring-practice-hide {
    grid-column: 2;
    grid-row: 1;
    padding-inline: var(--space-2);
  }

  /* The words: title, pill, who asked. No gap of their own, so the three read
     as one block. */
  .bb-bring-what {
    display: grid;
    min-inline-size: 0;
  }

  /* What is wanted. The one thing a reader scans a column of rows for, so it
     is the only thing here at more than body weight - and it is a <p> rather
     than a heading because the section above it is the heading. */
  .bb-bring-title {
    margin: 0;
    font-weight: 700;
  }

  /* Who asked for it. D-5 puts that in front of every member; it is muted and
     small because it is the second fact on the row and not the reason to read
     it. */
  .bb-bring-asker {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  /* "Sue Hayes is bringing this." Sage, which is this palette's settled colour
     and is what the meals page already uses for a decision - the two pages
     agree about what green means. */
  .bb-bring-claimed {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-sage);
    border-radius: var(--radius-sm);
    background: var(--tint-sage);
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  /* D-63a. In a row the claimed state is a small pill rather than a box: it
     sits under the title at its own width. */
  .bb-bring-pill {
    justify-self: start;
    margin-block: var(--space-1);
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  /* Your own share. The sentence already says "You are bringing", so this adds
     no colour of its own - it exists so the row can be found by a reader
     scanning for their own commitments without reading every row. */
  .bb-bring-mine {
    border-color: var(--color-line-strong);
  }

  /* "4 of 10 gallons covered. Darius Holmgren is bringing 4." D-63g: the
     words carry it, and there is no bar. */
  .bb-bring-count {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  /* A one-button form inside a row. It carries no top border and no margin of
     its own, unlike `.bb-suggest`, because the row is already the boundary. */
  .bb-bring-act {
    margin: 0;

    & .bb-submit,
    & .bb-decline {
      margin: 0;
    }
  }

  /* D-63a. The reader's own "We do not need this", and the disclosures'
     summaries: a text-style control, link-coloured and underlined, still
     `--tap-min` tall. It is deliberately NOT `.bb-decline`'s outlined box,
     which "I cannot after all" keeps, so the control that takes an item away
     from everybody no longer looks like the one that hands back your own
     share. */
  .bb-bring-text {
    display: inline-flex;
    align-items: center;
    justify-self: start;
    min-block-size: var(--tap-min);
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-link);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: 1.3;
    text-align: start;
    text-decoration: underline;
    text-underline-offset: 0.18em;
    text-decoration-thickness: 0.09em;
  }

  /* "Bring some of it", "Change how many you are bringing", and "It needs
     more than one" on the add form. A `<details>`, so each opens with no
     script. The summary keeps its disclosure triangle, which needs
     `list-item`, and reaches `--tap-min` by padding rather than by the flex
     height `.bb-bring-text` gives a button. */
  .bb-bring-more {
    min-inline-size: 0;

    & > .bb-bring-text {
      display: list-item;
      min-block-size: 0;
      padding-block: calc((var(--tap-min) - 1.3em) / 2);
      cursor: pointer;
    }
  }

  /* What a disclosure opens onto: a box and its button, stacked. */
  .bb-bring-fields {
    display: grid;
    gap: var(--space-2);
    padding-block: var(--space-1) var(--space-3);
  }

  /* "How many gallons?" with its box beside it. Wrapping, so a long unit word
     drops the box under the question rather than pushing the page sideways.
     The design review's own first mock put a box and its button on one line
     and measured the page 416px wide at a 390 viewport. */
  .bb-bring-howmany {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    font-weight: 700;
  }

  /* A count is three digits at most; a full-width box reads as "type a lot". */
  .bb-bring-number {
    inline-size: 6rem;
  }

  /* D-63b. The box that adds something, at the TOP of the page now. Set off
     from the lists below it with a rule under it, the way `.bb-suggest` is set
     off from a ballot: it is a different action, and a phone user scrolling
     should not mistake the text box for part of the list. */
  .bb-bring-ask {
    display: grid;
    gap: var(--space-2);
    max-inline-size: var(--column-max);
    min-inline-size: 0;
    padding-block-end: var(--space-4);
    border-block-end: var(--border);
  }

  /* D-64. Swiping a row off on a phone. Every rule below that changes how a
     row behaves is under `.bb-swipe-live`, which only the browser side puts
     on <html>, so a page without the bundle keeps every gesture and every
     pixel it had (D-64c).

     `pan-y pinch-zoom` and not `pan-y` alone: `pan-y` by itself also takes
     pinch-zoom away from a finger that starts on a row, and a relative who
     zooms in to read must still be able to. The browser side takes
     `.bb-swipe-live` off while the page is zoomed in, so a zoomed-in reader
     can move sideways around the page from a row as well. */
  .bb-swipe-live [data-bb-swipe] {
    position: relative;
    transform: translateX(var(--bb-dx));
    transition: transform 180ms ease;
  }

  /* EVERY row on the list, not only the rows that swipe (D-67c: on anybody
     else's row, swiping right does nothing). MEASURED in Chrome 153 with
     touch emulation on 2026-10-01: a sideways drag to the right on a row
     with no `touch-action` of its own went back a page, Chrome's own
     history gesture, while the same drag on a row carrying this did
     nothing. A reader taught to slide rows sideways will try it on any row.
     Under `.bb-swipe-live`, so a page without the bundle, or zoomed in,
     keeps every gesture it had. */
  .bb-swipe-live .bb-bring-item {
    touch-action: pan-y pinch-zoom;
  }

  /* D-70, D-73j and D-73k. ON A PHONE, THE LINE THAT TEACHES THE TAP AND THE
     SWIPE, AND THE PRACTICE ROW. Both are sent `hidden`, so a desktop and a
     page without the bundle never see them, and both are drawn here only
     where the main pointer is a finger, `(pointer: coarse)`, the question
     D-69 asked before the overlay opened.

     THEIR PLACE IS HELD FROM THE FIRST PAINT on a coarse pointer, unseen and
     out of the accessibility tree, so the list does not move down when the
     bundle starts (D-73j; D-71 held the line's place only on a device that
     had dismissed the overlay, which nothing reads now). They are seen once
     the swipe is live and the browser side has wired a row of their own
     section to go left (`.bb-swipe-brings`, which it puts on the practice
     row too). If the bundle never runs their place stays empty for the
     visit: the cost D-73j states.

     While the practice row is on the page the line is not shown, because
     the two teach the same gesture; once "Hide this" takes the row away
     (`.bb-practice-off`) the line is shown. A practice row slid away is gone
     without that class, and the line stays out of the way, because the
     device has now swiped. */
  @media (pointer: coarse) {
    .bb-bring > .bb-swipe-hint {
      display: block;
      visibility: hidden;
    }

    .bb-swipe-live .bb-bring:has(> .bb-bring-list > .bb-swipe-brings) > .bb-swipe-hint {
      visibility: visible;
    }

    .bb-bring-list > .bb-bring-practice[hidden] {
      display: grid;
      visibility: hidden;
    }

    .bb-swipe-live .bb-bring-list > .bb-bring-practice.bb-swipe-brings[hidden] {
      visibility: visible;
    }

    .bb-bring-list > .bb-bring-practice.bb-swipe-gone[hidden] {
      display: none;
    }

    .bb-bring:has(> .bb-bring-list > .bb-bring-practice:not(.bb-practice-off)) > .bb-swipe-hint {
      display: none;
    }
  }

  /* D-70. No `display` here: the line is sent `hidden`, and the user agent's
     `[hidden]` is what keeps it off a desktop and off a page without the
     bundle. The rules that draw it are in the phone block above. */
  .bb-swipe-hint {
    margin: 0;
    color: var(--color-text-muted);
  }

  /* D-67. The words a moving row uncovers, so which way does what is on the
     screen while the finger is still down. The browser side writes only how
     far the row has moved, `--bb-dx` in pixels and negative to the left; with
     it absent the `transform` above is invalid at computed-value time and is
     `none`, so a row at rest is not moved at all.

     Each cue is as wide as the row has moved its way and no wider, so it only
     ever fills the place the row has left, never the row beside it in the
     two-column layout, and a way the row does not go uncovers nothing. Its
     words are held against the far edge of that place and stay still while
     the row slides off them. `inset-block: -1px` and the `1px` are the row's
     border, which a positioned child is measured inside. */
  .bb-swipe-live [data-bb-swipe] > .bb-swipe-cue {
    position: absolute;
    inset-block: -1px;
    display: flex;
    align-items: center;
    overflow: hidden;
    white-space: nowrap;
    border-radius: var(--radius-sm);
    box-shadow: inset 0 0 0 2px currentColor;
    font-weight: 700;
    transition: inline-size 180ms ease;

    &::before,
    &::after {
      flex: none;
      inline-size: var(--space-3);
    }
  }

  .bb-swipe-live .bb-swipe-drag > .bb-swipe-cue {
    transition: none;
  }

  /* Moving LEFT: "Bring it", uncovered on the right. */
  .bb-swipe-live [data-bb-swipe] > .bb-swipe-cue-bring {
    left: calc(100% + 1px);
    justify-content: flex-end;
    inline-size: max(0px, -1 * var(--bb-dx, 0px));

    &::after {
      content: "";
    }
  }

  /* Moving RIGHT: "Take it off", uncovered on the left. */
  .bb-swipe-live [data-bb-swipe] > .bb-swipe-cue-withdraw {
    right: calc(100% + 1px);
    justify-content: flex-start;
    inline-size: max(0px, var(--bb-dx, 0px));

    &::before {
      content: "";
    }
  }

  /* The two cues' colours, shared with the overlay's picture of them:
     confirmed green for bringing, the error red for taking it away. */
  .bb-swipe-cue-bring {
    background: var(--tint-sage);
    color: var(--color-sage);
  }

  .bb-swipe-cue-withdraw {
    background: var(--tint-cranberry);
    color: var(--color-cranberry);
  }

  /* A row slid most of the way off must not make the page wider. `clip`
     rather than `hidden`, because `hidden` makes the list a scroll container
     and `clip` does not. */
  .bb-swipe-live .bb-bring-list {
    overflow-x: clip;
  }

  /* While the row follows the finger it must not lag behind it, and a long
     press must not start selecting its words. Under `.bb-swipe-live` too, so
     it outranks the transition the rule above gives every swiping row. */
  .bb-swipe-live .bb-swipe-drag {
    transition: none;
    user-select: none;
    -webkit-user-select: none;
  }

  /* Sliding off: nothing on it can be pressed on the way out. */
  .bb-swipe-out {
    pointer-events: none;
  }

  /* The Undo window: the row's own words and controls are hidden and the
     strip is shown in their place, across the whole row. */
  .bb-swipe-wait > :not(.bb-bring-undo) {
    display: none;
  }

  /* Sent (D-81), because the window ran out or the page was left while it
     was open, and brought (D-67), because the server took it: either way the
     strip stays in the row's place saying what it said, and its Undo goes,
     because it is too late to undo. */
  .bb-swipe-sent .bb-bring-undo > .bb-decline,
  .bb-swipe-done .bb-bring-undo > .bb-decline {
    display: none;
  }

  /* Sent. Two classes, so it outranks `.bb-bring-item`'s `display: grid`. */
  .bb-bring-item.bb-swipe-gone {
    display: none;
  }

  /* Put back by Undo, or the practice row by leaving the page: it fades
     back rather than snapping, so the reader sees which row came back. */
  .bb-swipe-back {
    animation: bb-swipe-back 180ms ease-out;
  }

  @keyframes bb-swipe-back {
    from {
      opacity: 0;
    }
  }

  /* A section whose every row has been swiped off and sent says nothing over
     an empty list. The practice row is not a row of the list for this. */
  .bb-swipe-live .bb-bring:not(:has(.bb-bring-item:not(.bb-swipe-gone, .bb-bring-practice))) {
    display: none;
  }

  /* "Taken off the list." with its Undo beside it, in the swiped row's
     place. The server sends it `hidden`, and the `[hidden]` rule is what
     keeps it hidden against the `display` here: an author rule beats the
     user agent's `[hidden]`, the reason `.bb-recipe` carries the same one.
     MEASURED in Chrome on 2026-10-01: with the rule taken out, all 11 strips
     on the preview page were drawn. */
  .bb-bring-undo {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    grid-column: 1 / -1;
    min-block-size: var(--tap-min);

    &[hidden] {
      display: none;
    }

    & > .bb-decline {
      inline-size: auto;
      margin: 0;
      padding-inline: var(--space-4);
    }
  }

  .bb-bring-undo-words {
    display: grid;
    min-inline-size: 0;
  }

  .bb-bring-undo-title {
    margin: 0;
    font-weight: 700;
  }

  .bb-bring-undo-said {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.3;
  }
}

/* ------------------------------------------------------------------ *
 * The dashboard at /home.
 *
 * Three panels: who is coming, what we still need, what is happening. Each is
 * a heading, a few sentences and a link to the page that owns the detail, so
 * they share ONE shape and the shape is the same card the feed and the meals
 * list already use. A member arriving here has seen it before.
 *
 * THREE OF THE RULES BELOW ARE NOT NEW WORK AND ARE NOT DECORATION.
 * MEASURED 2026-09-27 by adding `src/routes/home.rs` to the reachable list in
 * `crates/web/src/layout/assets.rs`: the coverage test named TWELVE hooks with
 * no rule, and three of them - bb-post-author, bb-post-body and bb-post-kind -
 * are the FEED's, which has been mounted since the five-page change. The page
 * was never on that list, so its own text had no rule at all and the only
 * reason it looked like anything is that the narrow-screen block above styles
 * the list around it.
 * ------------------------------------------------------------------ */

@layer components {
  /* One section of the dashboard. The same card the feed and the meals list
     use, and the gap between panels is the large one for the reason the meals
     block states: on a phone, the boundary between two subjects has to be more
     obvious than the boundary between two lines inside one. */
  .bb-panel {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    margin-block-end: var(--space-6);
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-panel-heading {
    margin: 0;
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  /* The sentence somebody is here to read: the headcount, or what still has
     nobody. Body size and bold rather than heading size - the heading above it
     already says which subject this is, and two heading-sized lines in a row
     read as two sections. */
  .bb-panel-lead {
    margin: 0;
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  /* The second fact. Muted, because a dashboard that shouts every line does not
     tell anybody which line matters. */
  .bb-panel-note {
    margin: 0;
    color: var(--color-text-muted);
    max-inline-size: var(--measure);
  }

  /* Who is here on which night. A two-column list rather than sentences: four
     or five nights read as a shape, and the numbers line up in a column that can
     be read down without reading the labels. */
  .bb-nights {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    margin-block-start: var(--space-2);
    padding: 0;
    list-style: none;
  }

  /* THE ROW IS DRAWN, NOT LEFT TO PROXIMITY. MEASURED 2026-09-28 at 320:
     "Wednesday 25 November" does not fit the 207px the label column gets, so
     it wraps, and the count stays on the first baseline - which puts "22"
     beside "Wednesday 25" with "November" hanging underneath it, reading as a
     count for half a date. A dotted rule closes each row, so a wrapped label is
     still visibly one row. The last row does not need one; the panel ends. */
  .bb-night {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: var(--space-3);
    min-inline-size: 0;
    padding-block-end: var(--space-1);
    border-block-end: 1px dotted var(--color-line);
  }

  .bb-night:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  /* The days of the trip on the dashboard (D-86): each day, and under it the
     name an organiser gave it. The same rows as the nights list above, with the
     name under the day rather than beside it, because a name may be sixty
     letters long. */
  .bb-days {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    margin-block-start: var(--space-2);
    padding: 0;
    list-style: none;
  }

  .bb-day {
    display: grid;
    min-inline-size: 0;
    padding-block-end: var(--space-1);
    border-block-end: 1px dotted var(--color-line);
  }

  .bb-day:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  .bb-day-when {
    min-inline-size: 0;
  }

  .bb-day-name {
    color: var(--color-text-muted);
  }

  .bb-night-when {
    min-inline-size: 0;
  }

  /* Tabular figures, so a column of counts is a column. The same treatment
     `.bb-dish-count` gets, and for the same reason. */
  .bb-night-count {
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    text-align: end;
  }

  /* "Announcement". A word and not only a class, which is the rule the feed's
     own markup follows - a class is invisible to a screen reader and to a phone
     that has not loaded this file - so it has to READ as a label: small, bold
     and above the name it qualifies. */
  .bb-post-kind {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* Who wrote it. The first thing on the row, because on a family feed the
     answer to "should I read this" is usually the name. */
  .bb-post-author {
    margin: 0;
    font-weight: 700;
  }

  /* What they wrote. One paragraph per typed line, which is why this has a gap
     above it rather than the list supplying one. */
  .bb-post-body {
    margin: 0;
    margin-block-start: var(--space-1);
    max-inline-size: var(--measure);
  }

  /* D-54. A line saying what a relative did: lighter than a post, because a
     post is somebody talking to the family and a line is the site saying what
     happened. No card, the same inset as a post's words so the names line up
     down the feed, and a rule under it like the nights list's rows. */
  .bb-feed > .bb-did {
    padding-block: var(--space-1) var(--space-2);
    padding-inline: var(--space-4);
    border-block-end: 1px dotted var(--color-line);
  }

  .bb-did-line {
    margin: 0;
    max-inline-size: var(--measure);
    color: var(--color-text-muted);
  }

  /* The name reads first, in the text colour; what they did follows in the
     muted one. */
  .bb-did-who {
    font-weight: 700;
    color: var(--color-text);
  }

  /* The dashboard's controls (D-97): each panel's way to the page that owns its
     detail, and the cabin address, is one of these, the last thing in its panel
     so a thumb finds them in the same place every time. A relative on a phone
     sees a row to press and not a word to aim at; before D-97 they were inline
     links in a `.bb-panel-more` paragraph, which the dashboard no longer draws.
     `crate::routes::home`'s walk holds that every control on the page carries
     a class with a rule at the tap floor. The
     outlined shape `.bb-people-act` and the secondary buttons use, the whole
     width of the panel, at the tap floor, with `/bring`'s chevron at the end
     saying it opens something. Not the accent fill: `.bb-submit` is the one
     primary action of a page, and the dashboard has one of these in nearly
     every panel. */
  .bb-panel-action {
    display: flex;
    margin-block-start: var(--space-1);
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    inline-size: 100%;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    line-height: var(--leading-tight);
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
      text-decoration: none;
    }
  }

  /* The cabin address is the one control whose words are the thing a relative
     came for, so it reads at the lead's size and wraps under itself. */
  .bb-panel-action.bb-cabin-link {
    min-block-size: var(--tap-large);
    font-size: var(--text-lg);
  }

  .bb-action-chev {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 1.4rem;
    block-size: 1.4rem;
    color: var(--color-text-muted);

    & > svg {
      inline-size: 100%;
      block-size: 100%;
      fill: none;
      stroke: currentColor;
      stroke-width: 2.2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
  }
}
@layer components {

  /* The add-a-person button, which is the one control on this site a BROWSER
     creates rather than the server.
     *
     * It shipped with no class and therefore no rule, and MEASURED at 390 CSS
     * pixels that left it 195x33 in the platform's grey default - against this
     * file's own `--tap-min` of 3rem, 54px, on a page grandparents use. "Send
     * it" beside it is 354x63. A control that fails the floor the rest of the
     * site holds is worse than one that looks plain.
     *
     * SECONDARY, not primary: outlined rather than filled, because the page
     * already has one filled accent button and the thing a person came here to
     * do is send the form, not add a row. Two filled buttons would compete. */
  .bb-add-person {
    display: block;
    inline-size: 100%;
    min-block-size: var(--tap-min);
    margin-block-start: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-accent);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }

  .bb-add-person:hover {
    background: var(--tint-accent);
  }
}

/* ------------------------------------------------------------------ *
 * The claim prompt, and the read-only answer a covered person gets.
 *
 * The prompt is the ONE thing on a page that is addressed to the reader rather
 * than to everybody, so it is the one thing allowed to interrupt: a sage-tinted
 * card at the top of `<main>`, ahead of the page's own content. Sage rather
 * than cranberry because this is a question, not a problem - cranberry is
 * spoken for by `.bb-form-error` and reusing it would make an ordinary
 * invitation read as something having gone wrong.
 *
 * BOTH BUTTONS ARE FULL CONTROLS AT `--tap-min`. The add-a-person button
 * shipped at 195 x 33 against this file's own 54px floor and that was recorded
 * as a real shortcoming rather than a cosmetic one; these are the first
 * controls a grandparent meets on this site, and no is exactly as big as yes.
 * Making the no smaller would be a thumb-sized opinion about what she should
 * answer.
 * ------------------------------------------------------------------ */

@layer components {
  .bb-claim {
    margin-block-end: var(--space-5);
    padding: var(--space-4);
    border: 2px solid var(--color-sage);
    border-radius: var(--radius);
    background: var(--tint-sage);
    max-inline-size: var(--measure);
  }

  .bb-claim-ask {
    margin-block: 0 var(--space-4);
    font-size: var(--text-lg);
    line-height: var(--leading-body);
  }

  .bb-claim-act {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
  }

  .bb-claim-yes,
  .bb-claim-no {
    flex: 1 1 12rem;
    min-block-size: var(--tap-min);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-sm);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }

  /* `--color-on-accent` and not `--color-surface`. MEASURED: the two are the
     same #FFFFFF in the light theme, so this changes nothing there - but they
     are different values under the dark theme, and this button wants the one
     that names what it is for. */
  .bb-claim-yes {
    border: 2px solid var(--color-accent);
    background: var(--color-accent);
    color: var(--color-on-accent);
  }

  .bb-claim-yes:hover {
    background: var(--color-accent-deep);
  }

  /* A real control and visibly the second one. An outline rather than a fill,
     which is the difference between the two answers being told apart and the
     no being discouraged. */
  .bb-claim-no {
    border: 2px solid var(--color-line-strong);
    background: var(--color-surface);
    color: var(--color-text);
  }

  .bb-claim-no:hover {
    background: var(--color-surface-2);
  }

  /* What was answered on a covered person's behalf. A plain list of sentences:
     they cannot change any of it, so nothing here is shaped like a control. */
  .bb-covered {
    margin-block: var(--space-4);
    padding: var(--space-4);
    border: var(--border);
    border-radius: var(--radius);
    background: var(--color-surface-2);
    max-inline-size: var(--measure);
  }

  .bb-covered-line {
    margin-block: 0 var(--space-2);
  }

  .bb-covered-line:last-child {
    margin-block-end: 0;
  }

  /* Where each claim a household raised has got to, under their own form. */
  .bb-claims {
    margin-block-start: var(--space-6);
    max-inline-size: var(--measure);
  }

  .bb-claims-heading {
    font-size: var(--text-lg);
    margin-block-end: var(--space-2);
  }

  .bb-claim-line {
    margin-block-end: var(--space-2);
    color: var(--color-text-muted);
  }
}

/* ------------------------------------------------------------------ *
 * What the pages look like once they have data in them: the driveway, the
 * ledger, the sleeping board, the feed, and the organiser's reset board.
 *
 * WHY THIS EXISTS, and it is one measurement rather than a taste. MEASURED
 * 2026-09-28 against the served stylesheet on a LAN box populated with eight
 * households, twenty-seven relatives and a ten-row ledger: 35 of the 92 class
 * hooks those pages emit had NO RULE - not as a selector, not as a descendant,
 * not in a comment. `bb-driveway`, `bb-ledger`, `bb-sleep-list`,
 * `bb-sleep-spot` and `bb-space` appeared ZERO times in the 56,559 bytes the
 * site was serving.
 *
 * WHAT THAT LOOKS LIKE TO A PERSON, which is why this is not cosmetic. The
 * three spans in a driveway row are bare inline boxes with no separator, so
 * they concatenate: "Third in line, blocked by bothSilver Honda OdysseyCobb
 * Hayes" was one continuous run at every width. On the pot it reached a money
 * column - "Delfina Ruiz$95.002026-09-26Bank transfer" - where `$95.002026-09-26`
 * reads as a corrupt figure rather than as two fields. The sleeping board was
 * thirty lines of flat text, ten spots at three lines each with no gap.
 *
 * ONE CARD SHAPE, SHARED WITH THE FEED, THE MEALS LIST AND THE BRING LIST.
 * Same reason the bring block gives: a member arriving here has already read
 * two lists drawn that way and a third shape would read as a different site.
 *
 * NOT EVERY HOOK GETS A RULE OF ITS OWN, and the feed is the model. `.bb-post`
 * is styled by the selector that names its parent's child, because the card is
 * a property of being IN the feed rather than of the post itself. Where that is
 * true below it is said at the rule.
 *
 * EVERY COLOUR HERE IS A TOKEN, and not one of them is a literal. A theme on
 * this site is done by RE-POINTING tokens and changing no rule - which is how
 * `body:has(.bb-gate)` above turns the door screen dark - so a colour written
 * directly into one of these rules is a value no theme can reach.
 * ------------------------------------------------------------------ */

@layer components {

  /* The driveway ------------------------------------------------- */

  /* Something needing attention: a car is blocked in, or somebody is leaving
     early. Pumpkin rather than cranberry, the same call `.bb-meal-cancelled`
     makes: it is a fact about the trip that somebody has to act on, not a
     refusal of anything the reader just did. */
  .bb-alarm {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-accent);
    font-weight: 700;
    max-inline-size: var(--measure);
  }

  .bb-driveway {
    display: grid;
    gap: var(--space-3);
  }

  /* One lane of the driveway is one card, because the lanes are the thing the
     stacking is about: D-10 says cars block each other in, so which lane a car
     is in is the fact a reader came for. */
  .bb-lane {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--color-surface-2);
  }

  /* One space, and the fix for the run-together. Three or four spans that were
     inline become rows of a grid, so each field is on its own line and the
     lanes cannot concatenate however long the label gets.

     The DOM order is label, car, owner, state and the visual order is the
     same. Reordering with `order` would read correctly and be announced
     wrongly, which on a site built for grandparents is the wrong trade. */
  /* The space is the RAISED plane and the lane is the quiet one, which is the
     order the rest of this file uses and is also the one that reads better.
     MEASURED both ways at 390 in a browser: with the space on `--color-surface-2`
     its muted label came out at 6.91:1, the lowest number on the site; on
     `--color-surface` it is 7.53:1, which is where the other muted text sits. */
  .bb-space {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  /* Where in the driveway. A caption above the car rather than beside it: some
     of these are a whole sentence ("Down by the propane tank where the plough
     leaves the berm") and a label column would wrap to three lines on a phone. */
  .bb-space-label {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* The car. The one thing a person scans this page for, so it is the only
     line here at more than body weight. */
  .bb-space-car {
    font-weight: 700;
  }

  /* Whose car it is (D-50) and how sure the board is. Both are second facts
     about the car above them, so both are muted and small. */
  .bb-space-state,
  .bb-space-owner {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* An empty space is still a space and still has a position, so it keeps its
     row. Dashed and flat rather than hidden: the count at the top of the page
     says how many spaces there are and a reader has to be able to find them.

     D-72: EMPTY IS READ FROM THE CAR'S LINE BEING EMPTY, not from a class on
     the space, because an open page writes who is in a space as text and never
     changes a class. The car's line is always drawn and is empty when the
     space is; "Empty" is its own word, shown only then; and a line with
     nothing in it takes no row of the grid. A page without the bundle draws
     the same three things and looks as it did. */
  /* An OUTLINE drawn inside the edge rather than a border, so an empty space
     is exactly as tall as an occupied one. MEASURED in Chrome at 390: with a
     1px border the empty card was 113px against 111px, and a car parking in
     it moved everything under it 2px - which D-72's guard then refused, so
     the page offered itself instead of showing the car. */
  .bb-space:has(> .bb-space-car:empty) {
    background: none;
    outline: 1px dashed var(--color-line);
    outline-offset: -1px;
  }

  .bb-space-car:empty,
  .bb-space-state:empty,
  .bb-space-car:not(:empty) + .bb-space-none {
    display: none;
  }

  /* THE OWNER'S LINE KEEPS ITS PLACE ON AN EMPTY SPACE, blank. Every car on
     the board carries its owner's name (D-50), so a space that a car parks in
     goes from label, "Empty" and this blank line to label, car and owner: the
     same three lines, and nothing under it moves when an open page writes the
     car in (D-72). Without it an arrival is two lines becoming three, which
     D-72's guard puts back and offers instead (reasoned from the guard; the 2px
     border below is the case that was measured). MEASURED: the page without
     the bundle is 26px taller at 390 and 30px at 1280 for one empty space,
     and nothing else on it moves. The builder's choice, the owner's to
     reverse. */
  .bb-space-owner:empty {
    visibility: hidden;

    &::before {
      content: "\a0";
    }
  }

  .bb-space-none {
    color: var(--color-text-muted);
  }

  /* One lane under a heading that says which end is the road, on both the
     member's board and the organiser's. The heading is the only thing on the
     page that says the first space in a lane is the one by the road, so it is
     a heading and not a caption, at body size so it does not compete with the
     page's own title. */
  .bb-lane-block {
    display: grid;
    gap: var(--space-2);
  }

  .bb-lane-heading {
    margin: 0;
    font-size: var(--text-base);
    line-height: var(--leading-tight);
  }

  /* The organiser's board: a space's own name at the top of its card, above
     the form that renames or moves it. It is the line somebody scans the board
     for, so it is the card's title. MEASURED BY LOOKING at 390px: at body size
     and bold it was the same weight and size as the "Name" label under it and
     read as one more field label, so it is a step up. */
  .bb-space-name {
    font-size: var(--text-lg);
    font-weight: 700;
    line-height: var(--leading-tight);
  }

  /* Lane and place, side by side where there is room and stacked where there
     is not; the option words run to "4th from the road" and a half-width
     picker on a phone would cut them. MEASURED BY LOOKING: stacked at 390px,
     side by side at 1280px. */
  .bb-space-where {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: var(--space-2);
  }

  /* "Turn this lane round". Outlined rather than filled, the treatment
     `.bb-recipe-remove` gives its button and for the same reason: every space
     above it has its own filled Save, and a lane-wide control that looked like
     one more of them would read as the page's main action. */
  .bb-lane-turn .bb-submit {
    margin-block-start: 0;
    border: 2px solid var(--color-accent);
    background: transparent;
    color: var(--color-accent);

    &:hover,
    &:active {
      background: var(--color-accent);
      color: var(--color-on-accent);
    }
  }

  /* A car with nowhere to go. Pumpkin for the same reason as the alarm. */
  .bb-unplaced {
    display: grid;
    gap: var(--space-2);
    margin: 0;
  }

  .bb-unplaced-car {
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-accent);
    font-weight: 700;
  }

  /* Who is parked behind whom, in order. This is an `<ol>` and it KEEPS its
     numbers - `ul[class]` in the base layer strips markers from lists and this
     is deliberately not that list. The number IS the content here. */
  .bb-arrival {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    padding-inline-start: var(--space-5);
  }

  .bb-arrival-car {
    padding-inline-start: var(--space-1);
  }

  .bb-blocked {
    display: grid;
    gap: var(--space-2);
    margin: 0;
  }

  .bb-blocked-item {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-blocked-car {
    font-weight: 700;
  }

  .bb-blocked-by {
    color: var(--color-text-muted);
  }

  /* Your cars, on the profile page ------------------------------- */

  /* D-51 moved declaring a car onto "Your Info". Each car is one card holding
     its own correction form and its removal, in the same card shape as the
     blocked-in list above, because a reader who has seen their car on the
     driveway should recognise it here. */
  .bb-cars {
    display: grid;
    gap: var(--space-3);
    margin: 0;
  }

  .bb-car {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  /* "Add a car", under the list. A step down from the panel heading above it,
     because it is part of that panel rather than a section of its own. */
  .bb-car-heading {
    margin: 0;
    font-size: var(--text-base);
    line-height: var(--leading-tight);
  }

  /* D-118: THE CAR SECTION, one design on Your Info and on the RSVP. A saved
     car is one line, its name over what it does; its boxes open from "Change
     or delete this car" under the line, and adding opens from "Add another
     car" once a car is saved. Both openers are bordered rows at the floor, so
     they read as things to press and not as more of the line. */
  .bb-car-line {
    display: grid;
    gap: var(--space-1);
    margin: 0;
  }

  .bb-car-name {
    font-weight: 700;
  }

  .bb-car-facts {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  .bb-car-edit > * + *,
  .bb-car-more > * + * {
    margin-block-start: var(--space-3);
  }

  .bb-car-edit-head,
  .bb-car-more-head {
    display: flex;
    align-items: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    cursor: pointer;
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }
  }

  /* A new car's boxes: a box of their own, so they never run on from a
     saved car's. */
  .bb-car-new {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  /* The cabin pot's ledger --------------------------------------- */

  .bb-ledger {
    display: grid;
    gap: var(--space-2);
    margin: 0;
  }

  /* Five spans that used to run together, one of them MONEY. Two columns and
     three rows: who and how much on the first, the date and the rail on the
     second, who recorded it across the third.

     The row placements are explicit rather than left to auto-flow, because
     auto-flow puts five items in five cells and the whole point is that the
     amount belongs beside the name it is attributed to. */
  .bb-ledger-row {
    display: grid;
    grid-template-columns: 1fr auto;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-ledger-who {
    grid-column: 1;
    font-weight: 700;
  }

  /* Tabular figures so a column of amounts is a column, the same treatment
     `.bb-dish-count` and `.bb-night-count` get and for the same reason. Not
     accent-coloured: `.bb-pot-amount` above is the total and is allowed to
     shout; ten shouting rows under it would drown it. */
  .bb-ledger-amount {
    grid-column: 2;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    text-align: end;
  }

  .bb-ledger-day {
    grid-column: 1;
  }

  .bb-ledger-rail {
    grid-column: 2;
    text-align: end;
  }

  .bb-ledger-recorded {
    grid-column: 1 / -1;
  }

  .bb-ledger-day,
  .bb-ledger-rail,
  .bb-ledger-recorded {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* The sleeping board ------------------------------------------- */

  /* D-92. The organisers' reply to this household's bed request, at the top
     of /sleeping. The heading says whose words these are; the edge is the
     announcement's, because it is the organisers speaking, and an edge is
     never the only thing that says so. */
  .bb-bed-reply {
    border-inline-start: 4px solid var(--color-accent);
  }

  .bb-bed-reply-words {
    margin: 0;
    margin-block-start: var(--space-1);
    max-inline-size: var(--measure);
    font-weight: 700;
  }

  .bb-sleep-board {
    display: grid;
    gap: var(--space-3);
  }

  .bb-sleep-heading {
    margin: 0;
  }

  /* Rooms are not sorted out yet. An absence rather than a problem, so it gets
     the same quiet surface a page-level note gets and none of the colour the
     alarms above carry. */
  .bb-sleep-pending {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-inline-start: 3px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text-muted);
    max-inline-size: var(--measure);
  }

  .bb-sleep-list {
    display: grid;
    gap: var(--space-3);
    margin: 0;
  }

  /* One bed. Three lines with no gap between spots is what made this page
     thirty lines of flat text. */
  .bb-sleep-spot {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-sleep-label {
    margin: 0;
  }

  .bb-sleep-description {
    margin: 0;
    color: var(--color-text-muted);
  }

  /* Who is in it, or that nobody is. The base is the empty case and it is
     muted, because "Not assigned yet" is the absence of an answer. */
  .bb-sleep-status {
    margin: 0;
    color: var(--color-text-muted);
  }

  /* A name is the answer the reader came for, so it is body colour and bold
     where the absence above is muted. */
  .bb-sleep-taken {
    color: var(--color-text);
    font-weight: 700;
  }

  /* "Held for Grandma Nkechi, who cannot manage the stairs." Sage, which is
     this palette's settled colour - the meals page uses it for a decision and
     the bring list for a claim, and a reserved bed is the same kind of fact. */
  .bb-sleep-reserved {
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-sage);
    border-radius: var(--radius-sm);
    background: var(--tint-sage);
    color: var(--color-text);
    font-weight: 700;
  }

  /* The feed ----------------------------------------------------- */

  /* An announcement. The word "Announcement" is already rendered above the
     name by `.bb-post-kind`, so this adds an edge and nothing that carries
     meaning on its own - the palette is not the only reader. */
  .bb-post-announcement {
    border-inline-start: 4px solid var(--color-accent);
  }

  /* D-91. "(edited)" after the author's name, in the plain weight and the
     muted colour, so the name still reads first. */
  .bb-post-edited {
    font-weight: 400;
    color: var(--color-text-muted);
  }

  /* D-91. The author's own Change and Remove, side by side under the post.
     Two `<details>`, so each opens with no script; one that is open takes the
     whole width under the pair, so its box is never squeezed beside the other
     control. */
  .bb-post-own {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--space-5);
    margin-block-start: var(--space-2);

    & > details[open] {
      flex-basis: 100%;
    }
  }

  .bb-post-change,
  .bb-post-remove {
    min-inline-size: 0;
  }

  /* The words in the Change box as the post shows them: the label above it is
     bold, and a post's own words are not. MEASURED in Chrome at 390 before
     this rule: the box drew them bold, inherited from `.bb-field`. */
  .bb-post-form .bb-textarea {
    font-weight: 400;
  }

  /* "Change" and "Remove": text-style controls in the link colour, keeping the
     disclosure triangle a summary carries (which needs `list-item`), and
     reaching `--tap-min` by padding, the way `.bb-bring-more` does, with the
     floor itself as a minimum: MEASURED at 390, padding alone drew them a
     fraction of a pixel under 54. */
  .bb-post-control {
    display: list-item;
    min-block-size: var(--tap-min);
    padding-block: calc((var(--tap-min) - 1.3em) / 2);
    font-weight: 700;
    line-height: 1.3;
    color: var(--color-link);
    cursor: pointer;
  }

  /* The organiser's PIN reset board ------------------------------ */

  .bb-people {
    display: grid;
    gap: var(--space-2);
    margin: 0;
  }

  .bb-person {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  /* Two relatives really can share a name - D-38 settled that as a supported
     case rather than a duplicate - so the note beside the name is what tells
     them apart and it must not be so quiet that nobody reads it. */
  .bb-person-name {
    font-weight: 700;
  }

  .bb-person-note {
    color: var(--color-text-muted);
  }

  /* The organiser's people board (D-90), which reuses the PIN board's rows. A
     name and its tags on one line; the tags are words, never colour alone. */
  .bb-person-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin: 0;
  }

  .bb-person-tag {
    padding: 0 var(--space-2);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    color: var(--color-text);
  }

  /* A row's buttons, side by side while they fit and stacked when they do not.
     Each is its own form, so the forms are the grid items. */
  .bb-people-acts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-2);
  }

  .bb-people-form {
    margin: 0;
  }

  /* The secondary button shape `.bb-submit` takes without the accent fill:
     outlined, `--tap-min` tall, its own width in a row of them. */
  .bb-people-act {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    line-height: var(--leading-tight);

    &:hover {
      background: var(--color-surface-2);
    }
  }

  /* "Two records for one person?", under the list and set off from it. */
  .bb-people-merge {
    display: grid;
    gap: var(--space-3);
    margin-block-start: var(--space-6);
    padding-block-start: var(--space-4);
    border-block-start: var(--border);
  }
}

/* ------------------------------------------------------------------ *
 * Links that were controls but were not shaped like one.
 *
 * MEASURED 2026-09-28 at 320, 390, 1024 and 1440: FOUR links on this site are
 * under `--tap-min`, the 54px floor this file declares for every control, and
 * every one of them is an inline link in a paragraph at 22 to 24px tall.
 * Three are the dashboard's calls to action - "Answer for your household",
 * "Suggest a dish, or vote", "What to bring" - which are the primary action of
 * their panel and the reason a member opens `/home` at all. The fourth is
 * "Go back" on `/who/change`.
 *
 * CHANGED BY D-97: the dashboard's links are now `.bb-panel-action` controls
 * with a rule of their own, and no longer sit in `.bb-panel-more`, which no
 * page draws, so it is gone from the list below. The paragraph above is kept
 * as the record of what was measured.
 *
 * THE BRIEF THAT ASKED FOR THIS SAID FIVE AND NAMED TWO ON `/who/change`. The
 * second one, "Cannot remember the one you use now? Ask Cobb or Bay for a
 * code", wraps to three lines at 320 and 390 and to two at 1024 and 1440, so
 * it is over the floor at every width measured. The count is four. It is
 * written down here because the number in the brief was a measurement and this
 * one contradicts it.
 *
 * The rule reaches them through their CONTAINERS rather than by naming four
 * links, for the same reason the form default above is structural: the fifth
 * one somebody adds tomorrow should not have to be remembered.
 * ------------------------------------------------------------------ */

@layer components {

  /* `inline-flex` and not `block`: these sit inside a sentence, and a block
     link would take the whole width of the card and underline empty space
     after the words. The box grows down to the floor around the text.

     `.bb-panel-lead a` joined on 2026-09-30 for the cabin address on the
     dashboard (D-49a), which is the first link to sit in a panel's lead line:
     it is the address itself, and tapping it is how a relative gets it into a
     maps app, so it is a control and gets the floor like the others. */
  .bb-panel-lead a,
  .bb-main > .bb-note a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
  }
}

/* ------------------------------------------------------------------ *
 * Dark mode.
 *
 * IT CONTAINS NO RULES. No selectors, no components, no class names, nothing
 * that can drift out of step with the light theme. Every visible change comes
 * from re-pointing a custom property that a rule above already reads, which is
 * the mechanism `body:has(.bb-gate)` has been using in production since the
 * gate screen shipped.
 *
 * WHY IT IS THIS CHEAP. RE-MEASURED 2026-09-28 with comments stripped, because
 * the figures that stood here - "29 colour literals, 28 of them on a token
 * definition ... 138 `var()` references" - were counted before this file grew
 * and none of the three still matched it. The shape of the claim survived the
 * re-count and only the numbers had moved, which is exactly why a stale number
 * is worse than none: it reads as a live measurement.
 *
 * TODAY: 48 colour literals in the whole stylesheet - 45 hex and 3 `rgb()` - of
 * which 47 are on a custom-property definition and exactly ONE is written into
 * a rule. That one is `body:has(.bb-gate) .bb-gate-wrong`'s wash, which is on a
 * screen that is already dark and is deliberately not themed. 517 `var()`
 * references, 38 of them the two alias lists at the bottom of this file, so 479
 * pick the palette up without being touched.
 *
 * TWO DOORS NOW, AND THAT REVERSES WHAT THIS COMMENT SAID THIS MORNING. It
 * said "AUTOMATIC, NOT A TOGGLE, and that is the owner's call", and it dropped
 * the `data-theme` hooks `docs/mockups/css/dark.css` carries on the ground that
 * "a door nothing can open is a list of eighteen aliases to keep in step for
 * nobody". The reasoning was sound and the premise was wrong: the owner asked
 * for the switch the same day. The doors are back and something opens them.
 *
 * The state a toggle needs is a COOKIE, which this site may now set - the
 * one-cookie rule was exempted for this site by the owner on 2026-09-28, about
 * twenty-five relatives and no banner needed at that size. It travels with the
 * request, so the server writes `data-theme` on `<html>` before it writes
 * anything else and there is no flash of the wrong palette to fix. A relative
 * who chooses on one phone and opens the site on another gets that phone's own
 * setting, which is the honest answer to the question the old comment raised.
 *
 * THE PALETTE IS WRITTEN ONCE, as `--dm-*` values, and the two doors alias
 * those onto the real tokens. That duplicates the alias LIST but never a hex,
 * which is the design pass's own argument from `dark.css`: a typo in an alias
 * fails loudly as an unresolved var, a typo in a duplicated hex is invisible.
 * Plain CSS cannot give one block both a media condition and a selector
 * condition, so one of the two has to be repeated and this is the cheaper one.
 *
 * `[data-theme="light"]` NEEDS NO BLOCK. It wins by defeating the media query,
 * and everything above is already the light theme.
 *
 * THE VALUES ARE THE DESIGN PASS'S, COPIED UNCHANGED from
 * `docs/mockups/css/dark.css`, where each pair's contrast was measured with
 * the WCAG 2 relative-luminance formula. They are NOT re-measured here, so
 * copying is what had to be got right, and the source is cited rather than
 * restated. The numbers that decide the palette: body text 13.04:1 on the
 * ground and 11.58:1 on a card; muted text 8.90:1 and 7.91:1; dark text on the
 * amber fill 8.42:1; cranberry links 8.54:1 and 7.58:1; sage 8.67:1. The floor
 * for anything read at length is 6.64:1, against the light theme's own 6.50:1.
 *
 * AND IT IS NOT THE LIGHT THEME INVERTED, for a reason about this audience.
 * Several of the primary users are in their seventies. Maximum contrast on a
 * dark ground causes halation, where light text blooms into the background,
 * and it is worse with the astigmatism that is common at that age. So the
 * ground is a warm dark brown and not black, the text is a warm off-white and
 * not white, and body text lands at 13.04:1 rather than the 17 to 21:1 that
 * white on black would give.
 *
 * THE LIGHT THEME'S ACTION COLOUR CANNOT SURVIVE THE MOVE. #8F3B12 is a burnt
 * orange carrying white text; as a fill on a dark ground it is a hole. It is
 * replaced by #F2A65A, which is the amber the front door already uses, now
 * carrying dark text - the same warm light as the gate, which is the point.
 *
 * THE GATE IS UNTOUCHED AND NEEDS TO BE. `body:has(.bb-gate)` re-points twelve
 * tokens to the `--gate-*` values, which are literals on `:root` that neither
 * door below names, and it does it on `<body>`, which beats `:root` whichever
 * way the phone is set and whatever `data-theme` says. So the door renders
 * identically for everybody - and it has no switch on it either, because the
 * switch lives in the drawer and the gate screen deliberately has none.
 * ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ *
 * The recipe corpus: the search at /recipes and one recipe at /recipes/<id>.
 *
 * Owner decision D-41: a dish is a recipe or a link, nothing else. The search
 * page renders the WHOLE corpus and hides what does not match, so everything
 * here has to look right at 230 rows as well as at three.
 *
 * NO COLOUR OF ITS OWN. Every value below is a token, so the dark palette at
 * the end of this file re-points all of it with no rule written twice.
 * ------------------------------------------------------------------ */
@layer components {
  /* The results. A plain stack of cards, the same shape the bring list and the
     feed already use, so a reader arriving from either has seen it before. */
  .bb-recipes {
    display: grid;
    gap: var(--space-3);
    margin-block-start: var(--space-4);
    padding: 0;
    list-style: none;
  }

  /* One recipe. A CARD rather than a list row, because it carries a name, a
     sentence and two things to tap, and a flat row of that puts them in a line
     a thumb cannot separate. */
  .bb-recipe {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-4);
    border: var(--border);
    border-radius: var(--radius-md);
    background: var(--color-surface);

    /* WITHOUT THIS THE WHOLE SEARCH DOES NOTHING VISIBLE, and it is not a
       browser quirk - it is the cascade working as specified. A browser hides
       `hidden` elements with `[hidden] { display: none }` in the USER AGENT
       origin, and an author declaration beats a user-agent one whatever the
       specificity. The `display: grid` three lines up is an author declaration
       on the same element, so every "hidden" result would keep its grid and
       stay on the page: the no-JavaScript search would filter nothing and the
       browser filter would filter nothing.

       Scoped rather than a global `[hidden] { display: none !important }`
       reset, because this stylesheet has no `!important` anywhere and a layer
       rule cannot do it - `base` sits below `components`, so a base-layer
       `[hidden]` would lose to `.bb-recipe` in this layer.

       NOT MEASURED in a browser: no browser exists in the environment this was
       written in. It rests on the cascade-origin rule rather than on a run, and
       `a_hidden_search_result_has_a_rule_that_can_actually_hide_it` is what
       stops the rule being deleted as redundant. */
    &[hidden] {
      display: none;
    }
  }

  /* The primary action, and the only place the dish's name appears when this
     page was opened from a meal. Full width and accent-filled, like every other
     primary button on the site.

     `text-align: start` and not centre, unlike `.bb-submit`: the label is "Add
     Sweet potato casserole" rather than one word, and a long dish name centred
     over two lines reads as a heading rather than a button. */
  .bb-recipe-pick {
    display: inline-flex;
    align-items: center;
    inline-size: 100%;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-accent);
    border-radius: var(--radius-sm);
    background: var(--color-accent);
    color: var(--color-on-accent);
    font-weight: 700;
    line-height: var(--leading-tight);
    text-align: start;

    &:hover,
    &:active {
      background: var(--color-accent-deep);
      border-color: var(--color-accent-deep);
    }
  }

  /* The name, when there is no button to carry it: somebody browsing the corpus
     rather than picking for a meal. A tap target in its own right. */
  .bb-recipe-name {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
    font-size: var(--text-lg);
    font-weight: 700;
  }

  /* "One of ours." A family recipe somebody typed in, which is not the
     internet's version of the same dish. A word rather than only a colour,
     because a class is invisible to a screen reader and to a phone that has not
     loaded this file. */
  .bb-recipe-ours {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: 700;
  }

  /* The one-line description, where the corpus has one. */
  .bb-recipe-summary {
    color: var(--color-text-muted);
  }

  /* The secondary links in this feature: "What is in it" under a search result,
     "Where this came from" on a recipe and "Back to the meals" at the foot of
     one. All of them are real tap targets and none is the first thing on its own
     surface - somebody scanning for a dish reads names. "Look at the whole list"
     on the organiser's board was one of these until D-105 made it the
     dashboard's control. */
  .bb-recipe-see,
  .bb-recipe-link,
  .bb-recipe-back {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
    font-weight: 700;
  }

  /* Nothing matched. NOT muted and NOT small: it is the only thing on the page
     when it shows. Since D-96 it holds the two ways to add a dish we do not
     hold - the paste-a-link box and "Type in a recipe" - so the bold is on its
     opening sentence and not on the box a relative types into. */
  .bb-recipe-none {
    display: grid;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);

    /* It sets `display`, which beats the user agent's `[hidden]`, and both the
       server and the browser filter show and hide it by that attribute: the
       same override `.bb-recipe` carries, for the same reason. */
    &[hidden] {
      display: none;
    }
  }

  .bb-recipe-none-lead {
    font-weight: 700;
  }

  /* "Not in the list? Add a recipe", under the search box on every search
     (D-96). D-96 drew it as a link and not a button, "it is not what the page
     is for"; REVERSED 2026-10-07 BY D-101, the owner's call: it is the
     dashboard's control, `.bb-panel-action`, and this rule only spaces it
     from the search box above it, a little more than that rule's own gap. */
  .bb-recipe-add {
    margin-block-start: var(--space-2);
  }

  /* "From allrecipes.com": where a recipe came from, with its site's name as
     the link on the recipe's page and on the preview (D-96). */
  .bb-recipe-from {
    color: var(--color-text-muted);
  }

  /* On the preview of a page that was read, what the page did not give and a
     relative has to fill in before it saves. An edge in the accent colour and
     a heavy weight, so it reads as "this needs you" and is not mistaken for a
     refusal, which is cranberry. */
  .bb-recipe-gap {
    padding-inline-start: var(--space-3);
    border-inline-start: 4px solid var(--color-accent);
    font-weight: 700;
  }

  /* "What is in it" and "How it is made" on one recipe. */
  .bb-recipe-heading {
    margin-block-start: var(--space-5);
    font-size: var(--text-xl);
  }

  /* A recipe an organiser has taken out of the box, as a relative meets it: they
     followed a "Recipe" link off a ballot they had already voted on.

     THE SAME SHAPE AS `.bb-recipe-none`, and that is the point rather than reuse
     for its own sake - both are "the thing you were looking for is not in the
     list", and somebody who has met one has met the other. Cranberry rather than
     accent because it is the token this stylesheet already spends on the case
     where something is not as expected; it is a boxed notice and not body text,
     so the colour is on a border and a heavy weight rather than on the sentence
     alone, which keeps it readable for anybody who cannot separate the hue. */
  .bb-recipe-gone {
    margin-block-end: var(--space-4);
    padding: var(--space-4);
    border: 2px solid var(--color-cranberry);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    font-weight: 700;
  }

  /* Both controls of the removal feature, in both directions: "Take this out of
     the list" and "Put it back", on the recipe page and on the organiser's board.

     A RULE OF ITS OWN SO THE BUTTON IS NOT THE PAGE'S MAIN ACTION. `.bb-submit`
     is accent-filled everywhere on this site, which is right for "Save it" and
     wrong for a control that takes something away. Here it is outlined instead:
     the same size and the same tap target, so nothing is harder to press, and
     visibly not the thing the page is for.

     MEASURED BY LOOKING, at 390px, which is what put this rule here: as plain
     `.bb-submit` buttons the board's two "Put it back" controls were full-width
     and accent-filled under the form's own "Save it", and by repetition alone
     they read as the loudest thing on a page whose job is that form.

     Separated by a rule above rather than by space alone, because on the recipe
     page it sits under "Back to the meals" and on the board it sits under a
     recipe's name, and a gap is not a boundary on a phone in either place. */
  /* The same treatment for deleting a car on the profile page, for the same
     reason: it sits under the car's own "Save changes", and two accent-filled
     buttons in a row would make deleting the car read as the page's main
     action. */
  .bb-recipe-remove,
  .bb-car-remove {
    margin-block-start: var(--space-5);
    padding-block-start: var(--space-4);
    border-block-start: var(--border);

    & .bb-submit {
      border: 2px solid var(--color-cranberry);
      background: transparent;
      color: var(--color-cranberry);

      &:hover,
      &:active {
        background: var(--color-cranberry);
        color: var(--color-on-accent);
      }
    }
  }

  /* The ingredients. One string per row, because that is what the corpus holds
     and what the hand-entry form takes - see D-41(a). */
  .bb-ingredients {
    display: grid;
    gap: var(--space-2);
    margin-block-start: var(--space-3);
    padding: 0;
    list-style: none;
  }

  .bb-ingredient {
    padding-block-end: var(--space-2);
    border-block-end: var(--border);
  }

  /* One step. A PARAGRAPH PER LINE, which is what the render does rather than
     one block with newlines in it: HTML collapses whitespace, and that is how
     `/pot` once rendered "Delfina Ruiz$95.002026-09-26Bank transfer". */
  .bb-method {
    margin-block-start: var(--space-3);
    max-inline-size: var(--column-max);
  }

  /* The organiser's hand-entry boxes. The ingredients go in one per line and
     the method is free text, so both need room to be read back while they are
     being typed; `--control-height` is one line and is the wrong shape here. */
  .bb-textarea {
    inline-size: 100%;
    min-block-size: 8rem;
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;

    &:focus-visible {
      outline: 3px solid var(--color-focus);
      outline-offset: 2px;
      border-color: var(--color-text);
    }
  }

  /* Invite family, on the dashboard (crate::share). The message a relative
     sends, read-only. The quiet surface rather than the card's, so it reads as
     something to copy and not as a box waiting to be typed in. AFTER
     `.bb-textarea` in this file on purpose: both are one class, so the later
     rule wins the background. */
  .bb-share-text {
    background: var(--color-surface-2);
    resize: vertical;
    /* The whole message, with nothing to scroll inside the box: MEASURED in
       Chrome at 390 with 12 rows, the last paragraph was cut off and the box
       scrolled. `field-sizing` grows the box to its text where the browser
       has it; the 16 `rows` the page sends are the fallback, sized for a
       narrow phone. Which phone browsers have `field-sizing` is NOT
       MEASURED. */
    field-sizing: content;
  }

  /* The share button is sent `hidden` and only the bundle shows it, because
     without a script it can do nothing. `.bb-submit` sets `display`, and an
     author `display` beats the user agent's `[hidden]`, so the rule says it,
     as `.bb-bring-card-close` does. */
  .bb-share-button[hidden] {
    display: none;
  }
}

@layer tokens {

  /* The palette, written once. Nothing reads these directly; the two doors
     below alias them onto the tokens every rule in this file already uses. */
  :root {
    /* Three planes, in the light theme's ORDER rather than inverted: a card
       is still raised above the ground and the quiet surface still sits
       below it, so a card reads as a card. */
    --dm-bg: #211B16;
    --dm-surface: #2B2520;
    --dm-surface-2: #191410;

    --dm-text: #E9E0D4;
    --dm-text-muted: #C8B9AA;

    /* The soft line is the main thing telling a card from the page here,
       where in the light theme a background difference does more of it. */
    --dm-line: #544840;
    --dm-line-strong: #948576;

    --dm-accent: #F2A65A;
    --dm-accent-deep: #F0A14F;
    --dm-on-accent: #211B16;
    --dm-pumpkin: #E06C2E;

    /* Links, refusals and settled things are all TEXT on dark here, where in
       the light theme they were dark inks on pale grounds, so all three
       lighten. `--color-link` needs no alias below: `:root` defines it as
       `var(--color-cranberry)` and custom properties resolve after the
       cascade, so re-pointing cranberry carries the links with it. */
    --dm-cranberry: #F0A3AA;
    --dm-sage: #A5C28C;

    --dm-focus: #E9E0D4;

    /* Washes that carry body and muted text, so they are dark here where
       they were pale there. Each sits 1.17 to 1.19:1 against the card it
       lands on, so a tinted notice still reads as a shape and not only as a
       hue. `--tint-sand` is aliased although MEASURED 2026-09-28 no rule
       in this file reads it: the cost is one line and the cost of leaving it
       out is a cream wash appearing on a dark page the first time somebody
       uses it. */
    --dm-tint-accent: #402E1E;
    --dm-tint-cranberry: #472A32;
    --dm-tint-sage: #2B3621;
    --dm-tint-sand: #393128;

    /* D-102, at night. Text on the harvest wash: body 10.84:1, muted 7.40:1;
       the title on the night sky 12.54:1; the pine edge 6.02:1 against the
       page and the pine mark 5.35:1 against a card. */
    --dm-pine: #7FA37A;
    --dm-wheat: #E2B355;
    --dm-tint-harvest: #352919;
    --dm-scene-sky: #18202C;
    --dm-scene-fleck: #E9E0D4;
    --dm-scene-moon: #E9E0D4;
    --dm-scene-far: #3A4352;
    --dm-scene-snow: #D3DAE3;
    --dm-scene-near: #26303A;
    --dm-scene-pine-back: #23362B;
    --dm-scene-pine: #142519;
    --dm-scene-wood: #6B4630;
    --dm-scene-roof: #3A2419;
    --dm-scene-window: #F2A65A;
    --dm-scene-smoke: #59616E;
    --dm-scene-ground: #3C4653;
    --dm-scene-paint-red: #9A3F4C;
    --dm-scene-paint-orange: #B0602C;
    --dm-scene-paint-gold: #B08C36;
    --dm-scene-paint-blue: #4F769C;
    --dm-scene-paint-green: #467852;
    --dm-scene-iron: #121418;
    --dm-scene-logend: #8F6D50;
    --dm-scene-bulb-red: #FF7B7B;
    --dm-scene-bulb-green: #8EE6A0;
    --dm-scene-bulb-blue: #93C9FF;
    --dm-scene-track: #A3AEBB;

    /* THE ONE VALUE THAT IS NOT A COLOUR SWAP. The scrim is painted in an
       ink at 45 percent, and in the light theme that ink is the text colour.
       Following the text colour here would paint an off-white veil over the
       page behind an open drawer and make it BRIGHTER. Near-black instead,
       which is the direction a scrim means. */
    --dm-scrim-ink: #090604;
  }

  /* DOOR ONE: the device already knows, and most relatives will never go
     looking for the switch. `:not([data-theme="light"])` is what lets somebody
     on a dark phone ask for the light one and be answered. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      color-scheme: dark;

      --color-bg: var(--dm-bg);
      --color-surface: var(--dm-surface);
      --color-surface-2: var(--dm-surface-2);
      --color-text: var(--dm-text);
      --color-text-muted: var(--dm-text-muted);
      --color-line: var(--dm-line);
      --color-line-strong: var(--dm-line-strong);
      --color-accent: var(--dm-accent);
      --color-accent-deep: var(--dm-accent-deep);
      --color-on-accent: var(--dm-on-accent);
      --color-pumpkin: var(--dm-pumpkin);
      --color-cranberry: var(--dm-cranberry);
      --color-sage: var(--dm-sage);
      --color-focus: var(--dm-focus);
      --tint-accent: var(--dm-tint-accent);
      --tint-cranberry: var(--dm-tint-cranberry);
      --tint-sage: var(--dm-tint-sage);
      --tint-sand: var(--dm-tint-sand);
      --scrim-ink: var(--dm-scrim-ink);
      --color-pine: var(--dm-pine);
      --color-wheat: var(--dm-wheat);
      --tint-harvest: var(--dm-tint-harvest);
      --scene-sky: var(--dm-scene-sky);
      --scene-fleck: var(--dm-scene-fleck);
      --scene-moon: var(--dm-scene-moon);
      --scene-far: var(--dm-scene-far);
      --scene-snow: var(--dm-scene-snow);
      --scene-near: var(--dm-scene-near);
      --scene-pine-back: var(--dm-scene-pine-back);
      --scene-pine: var(--dm-scene-pine);
      --scene-wood: var(--dm-scene-wood);
      --scene-roof: var(--dm-scene-roof);
      --scene-window: var(--dm-scene-window);
      --scene-smoke: var(--dm-scene-smoke);
      --scene-ground: var(--dm-scene-ground);
      --scene-paint-red: var(--dm-scene-paint-red);
      --scene-paint-orange: var(--dm-scene-paint-orange);
      --scene-paint-gold: var(--dm-scene-paint-gold);
      --scene-paint-blue: var(--dm-scene-paint-blue);
      --scene-paint-green: var(--dm-scene-paint-green);
      --scene-iron: var(--dm-scene-iron);
      --scene-logend: var(--dm-scene-logend);
      --scene-bulb-red: var(--dm-scene-bulb-red);
      --scene-bulb-green: var(--dm-scene-bulb-green);
      --scene-bulb-blue: var(--dm-scene-bulb-blue);
      --scene-track: var(--dm-scene-track);
    }
  }

  /* DOOR TWO: somebody pressed Always dark. Same list, and it is written out
     again rather than shared because plain CSS cannot give one block both a
     media condition and a selector condition. */
  :root[data-theme="dark"] {
    color-scheme: dark;

    --color-bg: var(--dm-bg);
    --color-surface: var(--dm-surface);
    --color-surface-2: var(--dm-surface-2);
    --color-text: var(--dm-text);
    --color-text-muted: var(--dm-text-muted);
    --color-line: var(--dm-line);
    --color-line-strong: var(--dm-line-strong);
    --color-accent: var(--dm-accent);
    --color-accent-deep: var(--dm-accent-deep);
    --color-on-accent: var(--dm-on-accent);
    --color-pumpkin: var(--dm-pumpkin);
    --color-cranberry: var(--dm-cranberry);
    --color-sage: var(--dm-sage);
    --color-focus: var(--dm-focus);
    --tint-accent: var(--dm-tint-accent);
    --tint-cranberry: var(--dm-tint-cranberry);
    --tint-sage: var(--dm-tint-sage);
    --tint-sand: var(--dm-tint-sand);
    --scrim-ink: var(--dm-scrim-ink);
    --color-pine: var(--dm-pine);
    --color-wheat: var(--dm-wheat);
    --tint-harvest: var(--dm-tint-harvest);
    --scene-sky: var(--dm-scene-sky);
    --scene-fleck: var(--dm-scene-fleck);
    --scene-moon: var(--dm-scene-moon);
    --scene-far: var(--dm-scene-far);
    --scene-snow: var(--dm-scene-snow);
    --scene-near: var(--dm-scene-near);
    --scene-pine-back: var(--dm-scene-pine-back);
    --scene-pine: var(--dm-scene-pine);
    --scene-wood: var(--dm-scene-wood);
    --scene-roof: var(--dm-scene-roof);
    --scene-window: var(--dm-scene-window);
    --scene-smoke: var(--dm-scene-smoke);
    --scene-ground: var(--dm-scene-ground);
    --scene-paint-red: var(--dm-scene-paint-red);
    --scene-paint-orange: var(--dm-scene-paint-orange);
    --scene-paint-gold: var(--dm-scene-paint-gold);
    --scene-paint-blue: var(--dm-scene-paint-blue);
    --scene-paint-green: var(--dm-scene-paint-green);
    --scene-iron: var(--dm-scene-iron);
    --scene-logend: var(--dm-scene-logend);
    --scene-bulb-red: var(--dm-scene-bulb-red);
    --scene-bulb-green: var(--dm-scene-bulb-green);
    --scene-bulb-blue: var(--dm-scene-bulb-blue);
    --scene-track: var(--dm-scene-track);
  }
}

/* ------------------------------------------------------------------
 * The Thanksgiving dinner ballot. Owner decision D-61.
 *
 * One premade form of five questions. Each question is a card with its
 * question as the heading, the shape the RSVP form's groups use, because five
 * groups of radio buttons and ticks with no boundary between them read as one
 * long column. The options themselves reuse the meals ballot's rows, so a
 * count sits at the end of every row the same way on both pages.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The whole form. More space between the questions than inside one. */
  .bb-feast {
    gap: var(--space-5);
  }

  .bb-feast-question {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--color-surface);

    & > legend {
      padding-inline: var(--space-2);
      margin-inline-start: calc(var(--space-2) * -1);
      background: var(--color-surface);
      font-size: 1.0625rem;
      font-weight: 600;
      line-height: 1.3;
      color: var(--color-text);
    }

    & > .bb-note {
      margin: 0;
    }
  }

  /* "Anyone is free to bring any side dish they like." D-61 asks for it to be
     said plainly, so it is set off from the instructions around it rather than
     being one more grey line. */
  .bb-feast-notice {
    margin: 0 0 var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-inline-start: 4px solid var(--color-sage);
    border-radius: var(--radius-sm);
    background: var(--tint-sage);
    color: var(--color-text);
    max-inline-size: var(--measure);
  }

  /* The way to the ballot from /meals, in the shape of the meals page's own
     secondary links. */
  .bb-feast-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    max-inline-size: var(--column-max);
    min-block-size: var(--tap-min);
    margin-block-start: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-accent);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    text-align: center;
    text-decoration: none;

    &:hover {
      background: var(--tint-accent);
    }
  }

  /* Putting your name in to cook: one button, on its own. */
  .bb-feast-stand {
    margin-block-start: var(--space-2);
  }
}

/* ------------------------------------------------------------------
 * The meals page and the Thanksgiving ballot, shortened. Owner decision D-62.
 *
 * The owner's words were "good now but lots of scrolling". Every rule here is
 * scoped to one of those two pages, and none of them removes a control: the
 * page carries the same links, forms and buttons it did, laid out tighter.
 * ------------------------------------------------------------------ */

@layer pages {

  /* D-62b. "Meals" and "Browse the recipes" on one line. Wrapping is allowed
     so that on a screen too narrow for both the link drops under the heading
     rather than squeezing it. The link keeps its full `--tap-min` height.
     MEASURED 2026-09-30: the two share a line at 320 and wider, and at 280 the
     link sits under the heading. */
  .bb-title-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);

    & > .bb-title {
      margin: 0;
    }

    & > .bb-all-recipes {
      inline-size: auto;
      margin: 0;
      padding-inline: var(--space-3);
      white-space: nowrap;
    }
  }

  /* D-62a. A meal with an open round and nothing on it: one row, and the row
     is the Nominate link. The anchor holds the heading and the cue; "No dishes
     yet" is a sibling under it and is NOT part of the link. The anchor's
     `::after` is stretched over the whole card, so a tap anywhere on the row,
     the empty line and the card's padding included, lands on the link.
     `position: relative` on the card is what the stretch is measured from.

     MEASURED 2026-09-30 in Chrome at 390, 1024, 1280 and 1440 with
     `elementFromPoint`, on every compact row: the middle of "No dishes yet",
     the left padding, a point inside the top-left and the bottom-right corner
     and a point right of the empty line all resolved to the row's link. With
     the `::after` switched off, four of those points tried on the first row
     at 390 resolved to it zero times. Every row measured 64.8px tall at 390
     and 71.8px at 1280, against `--tap-min`, which is 54px and 60px there. */
  .bb-meal-empty {
    position: relative;
    gap: 0;
    align-content: center;
    min-block-size: var(--tap-min);
    padding-block: var(--space-2);

    &:has(> .bb-meal-empty-link:hover) {
      background: var(--color-surface-2);
    }
  }

  .bb-meal-empty-link {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    column-gap: var(--space-3);
    color: inherit;
    text-decoration: none;

    &::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: var(--radius-sm);
    }

    /* The focus ring goes round the whole row, which is the target, rather
       than round the heading's line, which is only the anchor's own box. */
    &:focus-visible {
      outline: none;
    }

    &:focus-visible::after {
      outline: 3px solid var(--color-focus);
      outline-offset: 3px;
    }

    & > .bb-meal-heading {
      margin: 0;
      font-size: var(--text-base);
    }
  }

  /* "Nominate a dish". Coloured and underlined like a link, because the row
     around it is one. */
  .bb-meal-empty-cue {
    color: var(--color-link);
    font-size: var(--text-sm);
    font-weight: 700;
    white-space: nowrap;
    text-decoration: underline;
    text-underline-offset: 0.18em;
    text-decoration-thickness: 0.09em;
  }

  /* "No dishes yet". Muted, because it is the absence of content. */
  .bb-meal-empty-note {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  /* D-62c. Two columns of meal cards on a wide screen.

     The column is widened by its INLINE SIZE, the page's width less the
     sidebar's, and not by raising the cap alone. `.bb-main` is
     `inline-size: 100%` with the sidebar's width as a start margin, so the cap
     alone lets the box run off the right edge: MEASURED 2026-09-30 at 1280,
     keeping only `max-inline-size: 62rem` made the page 1580px wide. `100%`
     rather than `100vw`; whether a desktop scrollbar would make the viewport
     unit overflow is NOT MEASURED, and `100%` does not depend on the answer.

     A column is at least 21rem, the width in the design review's mock the
     owner chose, so the second column appears only where two fit. MEASURED
     2026-09-30: a fixed two columns at 1024, which is exactly 64em, made every
     card 315px wide, broke dish names into one or two words a line, and pushed
     one "Take this off the list" button past the edge of its card. With this
     rule 1024 is one column of 644px, and 1280 and 1440 are two.

     Only while there is a meal to lay out, and only on this page: `.bb-meals`
     is emitted by `crate::routes::meals` and nowhere else. The footer is NOT
     widened and keeps the width it has on every page. */
  @media (min-width: 64em) {
    body:has(.bb-nav) .bb-main:has(> .bb-meals > li) {
      inline-size: calc(100% - min(17rem, 82vw));
      max-inline-size: 62rem;
    }

    /* D-106e: the columns are a newspaper's, not a grid's. A grid put each
       pair of meals in a row as tall as the taller one, so "Wednesday dinner,
       No dishes yet" (72px) sat beside an open ballot (1,224px) over a hole
       the height of the difference: 1,785px of blank beside cards at 1280
       and 1,725px at 1440 on the look fixture (MEASURED in Chrome), and the
       two-column page was taller than the one-column page at 1024. Here the
       meals flow down the first column and then down the second, each card
       whole, with the same column width as before, so a second column
       still appears only where two fit: 644px at 1024 is one column, 900px
       at 1280 and 992px at 1440 are two. The order of the meals is the
       order of the days, read down and then across, like a newspaper. */
    .bb-main > .bb-meals {
      display: block;
      columns: 21rem;
      column-gap: var(--space-3);
    }

    .bb-main > .bb-meals > li {
      break-inside: avoid;
      margin-block-end: var(--space-3);
    }
  }

  /* D-63d. The bring list in two columns on a wide screen, D-62c's rule
     applied to this page: the main column takes the width beside the sidebar,
     and rows sit in columns at least 21rem wide, so a second column appears
     only where two fit. `.bb-bring` is emitted by `crate::routes::requests`
     and nowhere else. The footer is NOT widened. Kayos included this as a
     rendering detail and the owner can veto it. */
  @media (min-width: 64em) {
    body:has(.bb-nav) .bb-main:has(> .bb-bring) {
      inline-size: calc(100% - min(17rem, 82vw));
      max-inline-size: 62rem;
    }

    .bb-main > .bb-bring > .bb-bring-list {
      grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr));
      align-items: start;
    }
  }

  /* D-62d. Tighter rows on the Thanksgiving form only. A choice row stays at
     the `--tap-min` floor `.bb-check` declares; what shrinks is the padding
     inside it and the gap between rows. */
  .bb-feast .bb-check {
    padding-block: var(--space-2);
  }

  .bb-feast .bb-ballot {
    gap: var(--space-1);
  }

  /* The walk's R3: "No preference" carries no count, so with the shared
     `1fr auto` grid its box took the count's width as well and stood wider
     than every row above it. SUPERSEDED 2026-10-07 BY D-106: every row is a
     card the width of the column now, and the count sits inside it, so a row
     with no count is the same width as its neighbours by construction. The
     rule that stood here is deleted. */

  /* The organiser index. Each board's name is the dashboard's control,
     `.bb-panel-action`, since D-105 (before it, a block link at the tap floor
     in bold text; inline in a sentence the walk measured these links 20px tall,
     its R1), with what the board is for on the line under it.

     The gap between boards is what ties each line to the control above it.
     MEASURED in Chrome at 390px on the first three boards: without it a line
     sat 3px under its own control and 8 to 9px over the next one, so it read
     as belonging to either; with it, 3px under and 26 to 27px over. */
  .bb-o-index {
    display: grid;
    gap: var(--space-4);
  }
}

/* ------------------------------------------------------------------ *
 * D-72. An open page that keeps up.
 *
 * Numbers change where they are, as text, and need no rule. What a page would
 * add, remove or move is OFFERED by one small control in the sticky header,
 * and these rules are what keep the owner's rule that nothing moves under a
 * thumb: the offer is out of the flow, so its appearing moves no row, and an
 * empty line that a live number can fill is not drawn while it is empty.
 * ------------------------------------------------------------------ */
@layer components {

  /* Hung under the header, which is sticky and so is the box this is placed
     against: it stays on screen wherever the reader has scrolled, covers
     nothing in the header itself, and takes no room in the page. One tap
     target at the floor every control on this site keeps. */
  .bb-live-offer {
    position: absolute;
    inset-block-start: calc(100% + var(--space-2));
    inset-inline: var(--space-4);
    inline-size: fit-content;
    max-inline-size: calc(100% - 2 * var(--space-4));
    margin: 0 auto;
    border-radius: var(--radius);
    background: var(--color-accent);
    box-shadow: 0 0.25rem 1rem var(--color-line-strong);

    & a {
      display: flex;
      align-items: center;
      justify-content: center;
      min-block-size: var(--tap-min);
      padding: var(--space-2) var(--space-4);
      color: var(--color-on-accent);
      font-weight: 700;
      text-align: center;
      text-decoration: none;
    }

    & a:focus-visible {
      outline: 3px solid var(--color-focus);
      outline-offset: 3px;
      border-radius: var(--radius);
    }
  }

  /* NO `display` ON THE OFFER ITSELF, deliberately: the browser's own rule for
     `hidden` is what keeps it off a page without the bundle, and an author
     rule setting `display` here would beat it. The link inside is the flex
     box. */
}

/* ------------------------------------------------------------------ *
 * The dashboard's look: a cabin in the pines, with harvest on the meal and
 * need panels. Owner decision D-102: "i want more style and vibe. minimal is
 * only for the useability."
 *
 * So every control keeps the shape and the floor it had, and the LOOK gets
 * the warmth: a scene drawn in the page under the title, a pine edge and a
 * mark on every dashboard card, and on the two harvest cards a warm wash, an
 * autumn edge and three leaves. All of it is inline SVG in the markup and
 * custom properties here, so nothing is fetched from anywhere and the dark
 * theme reaches every colour through the token list above.
 *
 * Text never sits on a picture. The title sits on the banner's plain sky and
 * the scene is the row under it; the leaves and marks sit beside the words of
 * a heading, never behind them.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The banner: the dashboard's title on a plain sky, and the scene under it.
     Only the dashboard passes a scene beside its title, so only its title row
     becomes a banner; /meals's title row is untouched. */
  .bb-title-row:has(> .bb-scene) {
    display: grid;
    /* One column the whole width. Without the template and `stretch`, the
       row's own `justify-content: space-between` sized the column to its
       content and the drawing stopped short of the banner's right edge:
       MEASURED in Chrome at 1280, 514px of drawing in a 560px banner, and
       either of the two alone puts it at 558px, the banner's inside. */
    grid-template-columns: minmax(0, 1fr);
    justify-content: stretch;
    align-items: stretch;
    gap: 0;
    overflow: hidden;
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--scene-sky);

    & > .bb-title {
      padding: var(--space-5) var(--space-4) 0;
      color: var(--color-text);
    }
  }

  /* The drawing fills the banner's width and keeps its ground at the bottom;
     on a wide column its sky is what is trimmed. */
  .bb-scene {
    display: block;
    inline-size: 100%;
    block-size: 7.5rem;
  }

  .bb-scene-fleck {
    fill: var(--scene-fleck);
  }

  .bb-scene-moon {
    fill: var(--scene-moon);
  }

  .bb-scene-far {
    fill: var(--scene-far);
  }

  .bb-scene-snow {
    fill: var(--scene-snow);
  }

  .bb-scene-near {
    fill: var(--scene-near);
  }

  .bb-scene-pine-back {
    fill: var(--scene-pine-back);
  }

  .bb-scene-pine {
    fill: var(--scene-pine);
  }

  .bb-scene-wood {
    fill: var(--scene-wood);
  }

  .bb-scene-roof {
    fill: var(--scene-roof);
  }

  .bb-scene-logs {
    fill: none;
    stroke: var(--scene-roof);
    stroke-width: 1.2;
  }

  .bb-scene-window {
    fill: var(--scene-window);
  }

  .bb-scene-glow {
    fill: var(--scene-window);
    opacity: 0.25;
  }

  .bb-scene-smoke {
    fill: var(--scene-smoke);
  }

  /* The treeline's ground (D-106). */
  .bb-scene-ground {
    fill: var(--scene-ground);
  }

  /* The smoke drifts up from the chimney, and only on a device that has not
     asked for less motion. The base layer shortens every animation under
     `reduce` to 0.01ms and leaves an endless one endless; rather than lean on
     that, this one is never started there at all. MEASURED in Chrome through
     the estate's Browserless with `reduce` emulated: the puffs' computed
     `animation-name` is `none`, and `bb-smoke` without it. */
  @media (prefers-reduced-motion: no-preference) {
    .bb-scene-smoke > circle {
      animation: bb-smoke 6s ease-in-out infinite;
      transform-box: fill-box;
      transform-origin: center;
    }

    .bb-scene-smoke > circle:nth-child(2) {
      animation-delay: -2s;
    }

    .bb-scene-smoke > circle:nth-child(3) {
      animation-delay: -4s;
    }
  }

  @keyframes bb-smoke {
    0% {
      opacity: 0;
      transform: translate(0, 4px) scale(0.7);
    }

    30% {
      opacity: 0.9;
    }

    100% {
      opacity: 0;
      transform: translate(5px, -10px) scale(1.3);
    }
  }

  /* D-121: the dashboard's cabin grows. Its people: a beanie, a coat and
     dark legs, seen from behind; each figure's two paints come from the
     class on its group. */
  .bb-person-legs {
    fill: var(--scene-iron);
  }

  .bb-person-coat {
    fill: var(--person-coat);
  }

  .bb-person-hat {
    fill: var(--person-hat);
  }

  .bb-person-1 {
    --person-coat: var(--scene-paint-red);
    --person-hat: var(--scene-paint-gold);
  }

  .bb-person-2 {
    --person-coat: var(--scene-paint-blue);
    --person-hat: var(--scene-paint-red);
  }

  .bb-person-3 {
    --person-coat: var(--scene-paint-gold);
    --person-hat: var(--scene-paint-green);
  }

  .bb-person-4 {
    --person-coat: var(--scene-paint-green);
    --person-hat: var(--scene-paint-orange);
  }

  .bb-person-5 {
    --person-coat: var(--scene-paint-orange);
    --person-hat: var(--scene-paint-blue);
  }

  /* And the countdown's parts (D-121): the cars, the smoker, the woodpile,
     the pumpkins, the wreath, the lantern, the sled, the tracks, and the
     window dark once the trip is over. */
  .bb-scene-paint-red {
    fill: var(--scene-paint-red);
  }

  .bb-scene-paint-blue {
    fill: var(--scene-paint-blue);
  }

  .bb-scene-glass {
    fill: var(--scene-sky);
  }

  .bb-scene-iron {
    fill: var(--scene-iron);
  }

  .bb-scene-logend {
    fill: var(--scene-logend);
    stroke: var(--scene-wood);
    stroke-width: 0.8;
  }

  .bb-scene-pumpkin,
  .bb-scene-pumpkin-rib {
    fill: var(--scene-paint-orange);
    stroke: var(--scene-roof);
    stroke-width: 0.35;
  }

  .bb-scene-stem {
    fill: var(--scene-paint-green);
  }

  .bb-scene-seam {
    fill: none;
    stroke: var(--scene-smoke);
    stroke-width: 0.6;
  }

  .bb-scene-wreath {
    fill: none;
    stroke: var(--scene-paint-green);
    stroke-width: 1.9;
  }

  .bb-scene-bow {
    fill: var(--scene-paint-red);
  }

  .bb-scene-hook {
    fill: none;
    stroke: var(--scene-iron);
    stroke-width: 0.7;
  }

  .bb-scene-sled {
    fill: var(--scene-paint-red);
  }

  .bb-scene-runner {
    fill: none;
    stroke: var(--scene-iron);
    stroke-width: 0.9;
    stroke-linecap: round;
  }

  .bb-scene-tracks {
    fill: none;
    stroke: var(--scene-track);
    stroke-width: 1.7;
    stroke-linecap: round;
  }

  .bb-scene-window-out {
    fill: var(--scene-roof);
  }

  /* The roof lights: a bulb and a soft ring of its own colour around it, so
     at night each one glows. */
  .bb-scene-lights circle {
    stroke-width: 2.2;
    stroke-opacity: 0.35;
  }

  .bb-scene-bulb-red {
    fill: var(--scene-bulb-red);
    stroke: var(--scene-bulb-red);
  }

  .bb-scene-bulb-green {
    fill: var(--scene-bulb-green);
    stroke: var(--scene-bulb-green);
  }

  .bb-scene-bulb-amber {
    fill: var(--scene-window);
    stroke: var(--scene-window);
  }

  .bb-scene-bulb-blue {
    fill: var(--scene-bulb-blue);
    stroke: var(--scene-bulb-blue);
  }

  /* They twinkle, red and amber against green and blue, the owner's call
     (D-121), and only where the device has not asked for less motion: under
     `reduce` the animation is never started and every bulb is on and still,
     the way the smoke is held. */
  @media (prefers-reduced-motion: no-preference) {
    .bb-scene-bulb-red,
    .bb-scene-bulb-amber {
      animation: bb-twinkle 2.6s ease-in-out infinite;
    }

    .bb-scene-bulb-green,
    .bb-scene-bulb-blue {
      animation: bb-twinkle 2.6s ease-in-out -1.3s infinite;
    }
  }

  @keyframes bb-twinkle {
    0%,
    100% {
      opacity: 1;
    }

    50% {
      opacity: 0.45;
    }
  }

  /* THE SHARED CARDS (D-106). D-102 drew these on the dashboard as
     `bb-dash` and `bb-dash-harvest`; they are every page's now, on ANY card
     that carries the class: a dashboard panel, a meal on /meals, a panel on
     the Thanksgiving ballot. A later slice styling its own page uses these
     two classes and the marks, and writes no card of its own.

     A card of the cabin's: a pine edge along the top and the larger radius. */
  .bb-cabin {
    border-radius: var(--radius);
    border-block-start: 0.3rem solid var(--color-pine);
  }

  /* A card of the harvest's: the same shape with a warm wash and an autumn
     edge, for the Thanksgiving things. Text on the wash is D-102's ratio,
     body 14.12:1 light and 10.84:1 dark, muted 7.27:1 and 7.40:1. */
  .bb-harvest {
    border-radius: var(--radius);
    border-block-start: 0.3rem solid var(--color-pumpkin);
    background: var(--tint-harvest);
  }

  /* The heading on either card: the display face, its mark before the words,
     and on a harvest card the garland pushed to the far end. A panel's
     heading and a meal's are the two heading hooks a card draws. */
  .bb-main :is(.bb-cabin, .bb-harvest) > :is(.bb-panel-heading, .bb-meal-heading) {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* The mark before a heading's words. Pine on the cabin's cards, pumpkin on
     the harvest's. */
  .bb-panel-mark {
    flex: none;
    display: grid;
    inline-size: 1.35rem;
    block-size: 1.35rem;
    color: var(--color-pine);

    & > svg {
      inline-size: 100%;
      block-size: 100%;
      fill: currentColor;
    }
  }

  .bb-harvest .bb-panel-mark {
    color: var(--color-pumpkin);
  }

  /* On a meal's heading the garland is drawn before the words and shown after
     them, so the words stay the last thing in the heading, which is how the
     meals page's tests and its card finder read a heading. */
  .bb-meal-heading > .bb-garland {
    order: 1;
  }

  /* The garland beside a page title: the Thanksgiving ballot's. Larger than
     a heading's, because the title is. The row does not wrap for it: at 390
     "Thanksgiving dinner" filled the row and the garland dropped to a line of
     its own under the title (LOOKED AT in Chrome); the title wraps instead
     and the leaves stay at its side. */
  .bb-title-row:has(> .bb-garland) {
    flex-wrap: nowrap;

    & > .bb-title {
      min-inline-size: 0;
    }
  }

  .bb-title-row > .bb-garland {
    flex: none;
    inline-size: 5.5rem;
    block-size: 1.85rem;
  }

  .bb-mark-stroke {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  /* Three leaves at the end of a harvest heading, pushed to the far side.
     It gives way before the words do: "How the vote came out" with its mark
     and a garland that would not shrink wrapped to two lines at 390
     (LOOKED AT in Chrome, D-106), and the words are what the heading is for. */
  .bb-garland {
    /* The large shrink factor is what makes it give way FIRST: flex shares
       the shortfall by factor times basis, and with 1 the words span, at
       three times the garland's basis, took most of it and wrapped anyway
       (LOOKED AT in Chrome at 390, dark theme). */
    flex: 0 999 3.75rem;
    min-inline-size: 2.5rem;
    margin-inline-start: auto;
    inline-size: 3.75rem;
    block-size: 1.25rem;

    & > svg {
      display: block;
      inline-size: 100%;
      block-size: 100%;
    }
  }

  .bb-garland-stem {
    fill: none;
    stroke: var(--color-line-strong);
    stroke-width: 1.2;
    stroke-linecap: round;
  }

  .bb-garland-a {
    fill: var(--color-pumpkin);
  }

  .bb-garland-b {
    fill: var(--color-cranberry);
  }

  .bb-garland-c {
    fill: var(--color-wheat);
  }
}

/* ------------------------------------------------------------------ *
 * "What we still need", listed. Owner decision D-102: the panel lists every
 * meal whose vote is open, each a row that opens its card on /meals, and
 * every thing still needed with how many more of it. Each meal row is one
 * control at the tap floor; the things are lines, as the nights are.
 * ------------------------------------------------------------------ */

@layer pages {

  /* "Meals" and "Still needs somebody" inside "What we still need": small
     capitals over each half, the shape `.bb-post-kind` gives "Announcement". */
  .bb-main .bb-needed-head {
    margin-block-start: var(--space-3);
    font-size: var(--text-sm);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
  }

  /* The meals whose vote is open, one row each. */
  .bb-needed-meals {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* One meal: the whole row is its link, the name, what it needs and the
     chevron, the way every other dashboard control is one box to press. On a
     phone what it needs sits under the name; on the wide layout, beside it. */
  .bb-needed-meal {
    min-inline-size: 0;
  }

  .bb-needed-link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "name chev"
      "state chev";
    align-items: center;
    gap: var(--space-1) var(--space-3);
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    line-height: var(--leading-tight);
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
      text-decoration: none;
    }

    & > .bb-action-chev {
      grid-area: chev;
    }
  }

  @media (min-width: 64em) {
    .bb-needed-link {
      grid-template-columns: minmax(0, 1fr) auto auto;
      grid-template-areas: "name state chev";
    }
  }

  .bb-needed-name {
    grid-area: name;
    min-inline-size: 0;
    font-weight: 700;
  }

  /* "Needs a dish" or "3 dishes to vote on": a word in a pill, with an edge,
     so the two states differ by shape and words and not by colour alone. */
  .bb-needed-state {
    grid-area: state;
    justify-self: start;
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-sage);
    border-radius: 1rem;
    background: var(--tint-sage);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
  }

  .bb-needed-state.bb-needed-wants {
    border-color: var(--color-pumpkin);
    background: var(--tint-accent);
  }

  /* The things still needed: the nights list's shape, a name and how many
     more, a dotted rule between rows. */
  .bb-needed-things {
    display: grid;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-needed-thing {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--space-3);
    padding-block-end: var(--space-1);
    border-block-end: 1px dotted var(--color-line-strong);

    &:last-child {
      padding-block-end: 0;
      border-block-end: 0;
    }
  }

  .bb-needed-what {
    min-inline-size: 0;
    font-weight: 700;
  }

  .bb-needed-left {
    color: var(--color-text-muted);
    text-align: end;
  }

  /* "And 8 more" (D-104): the last row of a list longer than five, a meal
     row's control holding only its words and the chevron, laid out as one
     line. With a meal row's two lines on a phone, the second (what a meal
     needs) is empty here and the words sat 4.2px above the chevron's middle;
     as one line they are level with it (MEASURED in Chrome at 390). At 1280
     it changes nothing measured. */
  .bb-needed-more {
    min-inline-size: 0;

    & > .bb-needed-link {
      grid-template-columns: minmax(0, 1fr) auto;
      grid-template-areas: "name chev";
    }
  }
}

/* ------------------------------------------------------------------ *
 * The voting pages: /meals and /meals/thanksgiving. Owner decision D-106.
 *
 * "i want fable to hit every page now ... it should be nice and warm! but
 * easy and clean!" and, of this page, "i want it beautiful." Every control
 * keeps the shape and the floor it had. What changes is the look: a meal is
 * a card of the cabin's, the Thanksgiving dinner a card of the harvest's,
 * each dish is one card with its tick, its count and its recipe, and the
 * closing day is a notice in the harvest's colours.
 *
 * THE DISH ROW IS A CARD WITH NAMED AREAS, and that is the fix for the long
 * name: with the tick, the Recipe button and the count side by side at 390,
 * a 70-character name wrapped to eight lines beside the button (the owner's
 * report; D-105 measured three lines for "Sweet potato casserole"). On a
 * phone the name now takes the whole row but the count, and the button sits
 * under it on a line of its own; from 48em the three are side by side as
 * before, because there the name has the room. The heights are measured in
 * D-106's record.
 * ------------------------------------------------------------------ */

@layer pages {

  /* "Voting on these meals closes at the end of ..." at the top of /meals:
     a notice in the harvest's colours with the snowflake, in place of the
     plain page note it was. A flex row so the mark sits on the first line of
     the sentence. */
  .bb-closing {
    display: flex;
    align-items: start;
    gap: var(--space-2);
    margin: 0;
    max-inline-size: var(--measure);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-inline-start: 0.3rem solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-harvest);

    & > .bb-panel-mark {
      margin-block-start: 0.15rem;
      color: var(--color-pumpkin);
    }
  }

  /* One dish: a card. The tick's label is the first line (the whole of it but
     the count, so a long name has the width), the count at its end, the
     Recipe button under the name, and the reader's own "Take this off the
     list" under everything. A closed round's name, a bare span, takes the
     tick's place. The card carries the border and the tint the tick's label
     used to, so the whole row lights when it is ticked.

     CHANGED 2026-10-08 BY D-112 ON /meals ONLY: the D-112 section at the
     end of this file lays the member page's card out again (two lines, the
     label and a strip, at every width) at a higher specificity through
     `.bb-meals`. The rules here still draw the Thanksgiving ballot's rows,
     which have no recipe and no button, and are not edited. */
  .bb-dish {
    /* The first track is the label's inset, the box and the gap between them,
       so the Recipe button on its own line starts where the name starts. */
    grid-template-columns: calc(var(--space-4) + 1.5rem + var(--space-3)) minmax(0, 1fr) auto;
    grid-template-areas:
      "tick tick count"
      ". recipe recipe"
      "mine mine mine";
    gap: var(--space-1) 0;
    border: 2px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    &:has(> .bb-check > input:checked) {
      border-color: var(--color-accent);
      background: var(--tint-accent);
    }
  }

  /* The label keeps its padding and its floor, which is what makes the first
     line of the card the thumb's target; the box and the tint are the card's
     now. */
  .bb-dish > .bb-check {
    grid-area: tick;
    border: 0;
    border-radius: 0;
    background: transparent;

    &:has(input:checked) {
      border: 0;
      background: transparent;
    }
  }

  /* A closed round's dish name, and the totals' option names. */
  .bb-dish > span:first-child {
    grid-area: tick;
    padding: var(--space-2) var(--space-3);
  }

  /* The count, in a small outlined badge at the end of the first line: the
     real number, which every ballot shows (D-5). Outlined and not filled, so
     it reads the same on a ticked card as on a plain one. Wide enough for two
     figures before it grows, so a column of counts is a column. */
  .bb-dish-count {
    grid-area: count;
    justify-self: end;
    align-self: center;
    min-inline-size: 2.5rem;
    margin-inline-end: var(--space-3);
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-line-strong);
    border-radius: 1rem;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    text-align: center;
  }

  /* A card with a Recipe button on its own line keeps a little room under
     the button. The button's own placement is in its one rule above. */
  .bb-dish:has(> .bb-dish-recipe) {
    padding-block-end: var(--space-2);
  }

  .bb-dish > .bb-decline {
    grid-area: mine;
    inline-size: auto;
    margin: 0 var(--space-3) var(--space-3);
  }

  /* With the room, the three side by side: the name, the button, the count.
     The room is the BALLOT's, not the screen's: on the wide layout /meals is
     two columns of cards (D-62c), and at 1280 a card's ballot is about 408px
     wide, less than a phone's whole screen, where the first build's
     viewport query put the three side by side and the 70-character name
     wrapped to seven lines (MEASURED in Chrome). A ballot is its own
     container, and the row switches when the ballot is 560px wide: a
     one-column page at 1024 (a 610px ballot) is side by side, a phone and
     the two-column layout stack.

     SUPERSEDED 2026-10-08 BY D-112 ON /meals: the member page's card keeps
     its two lines at every width (the D-112 section at the end of this
     file wins the query below by specificity), so a dish reads the same on
     a phone and on the wide page. The query still reaches the Thanksgiving
     ballot's rows, where it changes nothing visible: those rows have no
     recipe and no button to lay beside the name. */
  .bb-ballot {
    container-type: inline-size;
  }

  @container (min-width: 560px) {
    .bb-dish {
      grid-template-columns: minmax(0, 1fr) auto auto;
      grid-template-areas:
        "tick recipe count"
        "mine mine mine";
      column-gap: var(--space-3);
    }

    .bb-dish:has(> .bb-dish-recipe) {
      padding-block-end: 0;
    }
  }

  /* "Cast votes" is sent with `data-bb-by-button` (D-106, extending D-66 and
     D-82): the bundle hides it when it takes the form, because a tick is the
     vote there and the sentence under it already says so. `.bb-submit` sets
     its own `display`, which beats the browser's rule for `hidden`, so the
     rule is said here, as `.bb-share-button[hidden]` is. Without the bundle
     the attribute does nothing and the button is the ballot's. */
  .bb-submit[hidden] {
    display: none;
  }

  /* A question on the Thanksgiving ballot: the harvest's wash and an autumn
     line, with the legend in the display face and its leaf. The legend sits
     INSIDE the card as its title (2026-10-08, the owner on his phone: drawn
     on the top border, its tinted box stuck out above the card's edge). A
     floated legend is not drawn on the border; it takes its place in the
     question's grid like any other row, so the grid's gap spaces it.
     MEASURED in Chrome at 412, light and dark: the legend's top 19px inside
     the card, nothing above its edge, the card 392px against 382 before.
     The background is none on purpose: the rule further up paints the
     legend the surface colour, which would now draw a box inside the card.
     D-114 moved the float, the width, the margin, the padding and the
     background to one rule at the end of this file, which does the same
     for every member page's question cards; this keeps the look. */
  .bb-feast-question {
    border-color: var(--color-pumpkin);
    background: var(--tint-harvest);

    & > legend {
      display: flex;
      align-items: center;
      gap: var(--space-2);
      font-family: var(--font-display);
      font-size: 1.25rem;
      font-weight: 400;
      line-height: var(--leading-tight);
    }

    & > legend > .bb-panel-mark {
      color: var(--color-pumpkin);
    }
  }
}

/* ------------------------------------------------------------------ *
 * Getting in: the gate, every sign-in step, Change your PIN, Your Info,
 * the email stop page, and the not-found and error pages. Owner decision
 * D-109, style group G3.
 *
 * These are the first pages a relative sees, and most of them are drawn
 * with no header, no drawer and no footer on purpose: a stranger, or a
 * visitor holding only the forwarded password, must not be handed the
 * site's paths (crate::app::Shell's `links` default). So the welcome has to
 * come from the page itself. Each bare page passes the dashboard's cabin
 * scene beside its title (`crate::art::CabinScene`), and D-102's banner
 * rule above lays the title on the sky with the cabin under it. Nothing is
 * fetched, and nothing in the drawing names a route or a person. The pages
 * that have a header (Your Info, Change your PIN) keep D-106's split: the
 * header's pines are their drawing and the scene stays off them.
 *
 * What a page asks for is in one card of the cabin's (`bb-panel bb-cabin
 * bb-door`): the sentence, the refusal when there is one, and the form. The
 * other ways on ("Start again", "Forgotten it?") are the dashboard's
 * controls (`bb-panel-action` with the chevron), a row to press at the
 * floor rather than a word inside a sentence to aim at. A dead end (locked
 * out, turned off, a page that is not here) is D-106's notice
 * (`bb-closing`) with a mark. Your Info is five cards of the cabin's, each
 * with its mark. Every control keeps the shape and the floor it had.
 * ------------------------------------------------------------------ */

@layer components {

  /* The door card: the one card on a sign-in step. `.bb-panel` leaves the
     dashboard's gap under a card, for the next subject; under this card
     there is only its own ways on, so the column's gap is enough. */
  .bb-door {
    margin-block-end: 0;
    gap: var(--space-3);
  }

  /* The sign-in forms. Each is laid out by the form default (the zero-
     specificity rule above); they are named here so that `bb-claim-pin`,
     which D-109 renamed from `bb-claim`, has a rule of its own: the RSVP
     claim prompt is `aside.bb-claim` with its sage card, and MEASURED before
     the rename, the "Set your PIN" form was drawn inside that card. A step of
     air above the first label, so the form does not run into the sentence
     over it. */
  .bb-who,
  .bb-contact-key,
  .bb-new,
  .bb-claim-pin,
  .bb-pin,
  .bb-reset,
  .bb-change-pin {
    margin-block-start: var(--space-1);
  }

  /* The ways on from a step, under its card: a column of the dashboard's
     controls with the small gap between them, not the column's. */
  .bb-door-ways {
    display: grid;
    gap: var(--space-2);
  }

  /* The link inside a page-level refusal: Your Info's "Your car was not
     saved. See why under Your cars." at the top of the page (D-51). The
     floor rule above reaches a link in a page-level note and not one in a
     refusal; MEASURED in Chrome at 390 before this rule, 47px against the
     54px floor. The same shape that rule gives. */
  .bb-main > .bb-form-error a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
  }
}

@layer pages {

  /* THE GATE AT NIGHT. The door keeps its own dark palette in both themes
     (the gate block above re-points the colour tokens from `<body>`), and
     the scene beside its title is drawn at night to match: the values D-102
     gave the dark theme, read from the `--dm-` list the two dark doors
     alias, so the cabin's window is lit in the gate's amber and the moon is
     up. The scene's tokens are re-pointed here and nowhere else. */
  body:has(.bb-gate) {
    --scene-sky: var(--dm-scene-sky);
    --scene-fleck: var(--dm-scene-fleck);
    --scene-moon: var(--dm-scene-moon);
    --scene-far: var(--dm-scene-far);
    --scene-snow: var(--dm-scene-snow);
    --scene-near: var(--dm-scene-near);
    --scene-pine-back: var(--dm-scene-pine-back);
    --scene-pine: var(--dm-scene-pine);
    --scene-wood: var(--dm-scene-wood);
    --scene-roof: var(--dm-scene-roof);
    --scene-window: var(--dm-scene-window);
    --scene-smoke: var(--dm-scene-smoke);
    --scene-ground: var(--dm-scene-ground);
  }

  /* The gate's banner: the name on the night sky over the cabin. The gate's
     own title rule keeps its size and its amber line under the words; the
     banner's edge is the gate's line colour, which the gate block already
     re-points `--color-line` to. */
  body:has(.bb-gate) .bb-title-row:has(> .bb-scene) > .bb-title {
    padding-block-start: var(--space-6);
  }
}

/* ------------------------------------------------------------------ *
 * G1, "Your trip": the dashboard with its feed and the author's Change and
 * Remove, /bring, and /rsvp with the claim pages. Owner decision D-107,
 * after D-106 put the cabin and the harvest on the shell and the voting
 * pages: "all pages should get full attention."
 *
 * Every control keeps the shape, the words and the floor it had. What
 * changes is the look, drawn from the vocabulary D-106b records: the
 * cabin's card (a pine edge, the larger radius) and the harvest's (the warm
 * wash, an autumn edge), the display face on a heading with a mark before
 * its words, and the outlined badge the voting page gives a count. Every
 * colour is a token the dark theme re-points; nothing is fetched.
 *
 * ONE SECTION, after D-106's, because three style groups write this file at
 * once and a group's rules in one place merge without touching another's.
 * A rule here that re-shapes a hook with an earlier rule wins by coming
 * later in the same layer at the same or higher specificity; no earlier
 * rule is edited. Where an earlier `:last-child` rule took a row's rule
 * away, it is said again here, because a later layer-mate would otherwise
 * put it back.
 * ------------------------------------------------------------------ */

@layer pages {

  /* THE DASHBOARD'S FEED. A post is a card with a pine edge along its
     start and the author's name in the display face; an announcement is the
     harvest's, the wash and an autumn edge, so the organisers' word is told
     apart by shape and colour as well as by the word "Announcement" above
     the name, which stays. */
  .bb-feed > .bb-post {
    border-radius: var(--radius);
    border-inline-start: 0.3rem solid var(--color-pine);
  }

  .bb-feed > .bb-post.bb-post-announcement {
    border-inline-start-color: var(--color-pumpkin);
    background: var(--tint-harvest);
  }

  .bb-feed .bb-post-author {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  /* A line saying what a relative did: a pine dot where a post has its
     edge, so the feed reads as one spine of news, and its rule in the
     stronger line, which is seen on the panel's ground. The dot sits on the
     first line's middle: the line's own top padding plus half a line. */
  .bb-feed > .bb-did {
    position: relative;
    padding-inline-start: calc(0.3rem + var(--space-4));
    border-block-end: 1px dotted var(--color-line-strong);

    &::before {
      content: "";
      position: absolute;
      inset-inline-start: 0;
      inset-block-start: calc(var(--space-1) + 0.5em);
      inline-size: 0.5rem;
      block-size: 0.5rem;
      border-radius: 50%;
      background: var(--color-pine);
    }
  }

  /* THE AUTHOR'S OWN CHANGE AND REMOVE (D-91). Each summary is the house's
     outlined control now, a button to press with a chevron that turns when
     it is open, in place of the link-coloured word with a disclosure
     triangle: the owner's standing ask (D-105) is that a way on is a
     tappable button. The floor is the one the earlier rule holds. What a
     summary opens onto sits in a sand inset under the pair. */
  .bb-post-own {
    gap: var(--space-2);
    margin-block-start: var(--space-3);
  }

  .bb-post-control {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    line-height: var(--leading-tight);
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }

    /* The chevron: two borders of a small box, turned. No markup, no image. */
    &::after {
      content: "";
      flex: none;
      inline-size: 0.55rem;
      block-size: 0.55rem;
      margin-block-start: -0.2rem;
      border-inline-end: 2px solid var(--color-text-muted);
      border-block-end: 2px solid var(--color-text-muted);
      rotate: 45deg;
      transition: rotate 180ms ease, margin 180ms ease;
    }

    &:hover {
      background: var(--color-surface-2);
    }
  }

  details[open] > .bb-post-control {
    background: var(--color-surface-2);

    &::after {
      margin-block-start: 0.2rem;
      rotate: 225deg;
    }
  }

  .bb-post-change > .bb-post-form,
  .bb-post-remove > .bb-post-form,
  .bb-post-change > .bb-form-error {
    margin-block-start: var(--space-2);
  }

  .bb-post-change > .bb-post-form,
  .bb-post-remove > .bb-post-form {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--tint-sand);
  }

  /* WHO IS COMING: the headcount in the display face, the one line a
     relative scans for, and each night's count in the outlined badge the
     voting page gives a dish's count, so a column of numbers is a column.
     `.bb-cabin > .bb-panel-lead` is the headcount alone: no other cabin
     card on the dashboard draws a lead line. */
  .bb-cabin > .bb-panel-lead {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  .bb-night {
    align-items: center;
    padding-block: var(--space-1);
    border-block-end: 1px dotted var(--color-line-strong);
  }

  .bb-night:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  .bb-night-count {
    min-inline-size: 2.5rem;
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-line-strong);
    border-radius: 1rem;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    text-align: center;
  }

  /* THE DAYS: a day's name under its date is a pill of the harvest's, so
     "Thanksgiving" is found at a glance down the list. */
  .bb-day {
    gap: var(--space-1);
    padding-block: var(--space-1);
    border-block-end: 1px dotted var(--color-line-strong);
  }

  .bb-day:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  .bb-day-name {
    justify-self: start;
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-pumpkin);
    border-radius: 1rem;
    background: var(--tint-harvest);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
  }

  /* THE CLAIM PROMPT, on the dashboard and /rsvp: the one thing addressed
     to the reader. Sage still, a question and not a problem, in the cabin's
     card shape with a sage edge along its top and the question in the
     display face. The two buttons are unchanged. */
  .bb-claim {
    border: 1px solid var(--color-line);
    border-block-start: 0.3rem solid var(--color-sage);
    border-radius: var(--radius);
    background: var(--tint-sage);
  }

  .bb-claim-ask {
    font-family: var(--font-display);
    font-size: 1.25rem;
    line-height: var(--leading-tight);
  }

  /* WHAT WAS ANSWERED FOR A COVERED OR LISTED PERSON: a card of the cabin's
     whose first line, the household's answer, is in the display face, and a
     rule between the lines. */
  .bb-covered {
    border: 1px solid var(--color-line);
    border-block-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  .bb-covered-line {
    margin: 0;
    padding-block: var(--space-1);
    border-block-end: 1px dotted var(--color-line-strong);
  }

  .bb-covered-line:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  .bb-covered-line:first-child {
    font-family: var(--font-display);
    font-size: 1.25rem;
    line-height: var(--leading-tight);
  }

  /* "PEOPLE WE HAVE ASKED", under the household's own form: a card of the
     cabin's with the pine before its heading and a rule between the lines.
     Its list has no class, so the base layer leaves it its bullets; they go
     here. */
  .bb-claims {
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-block-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  .bb-claims-heading {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  .bb-claims > ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-claim-line {
    margin: 0;
    padding-block: var(--space-2);
    border-block-end: 1px dotted var(--color-line-strong);
    color: var(--color-text);
  }

  .bb-claim-line:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  /* THE RSVP FORM. Each question is a card of the cabin's: a pine line all
     round rather than the thick top edge, because a legend is drawn on the
     fieldset's top border and would cut an edge in two (D-106c measured it
     on the Thanksgiving ballot's questions). The legend is in the display
     face with a mark before its words: the pine on "Are you coming", the
     snowflake on the nights, the cabin on where you will sleep. Since
     D-114 (the end of this file) the legend is not on the border at all:
     it is the card's title, inside it. */
  .bb-rsvp > fieldset {
    padding: var(--space-4);
    border: 2px solid var(--color-pine);
    border-radius: var(--radius);
  }

  .bb-rsvp > fieldset > legend {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* One person: a card with the pine along its start, which a legend on the
     top border cannot cut. The row number stays the quietest thing on it:
     the body face, small, as its own rule draws it. */
  .bb-rsvp > fieldset.bb-party-member {
    border: 1px solid var(--color-line);
    border-inline-start: 0.3rem solid var(--color-pine);
    background: var(--color-surface);
  }

  .bb-rsvp > fieldset.bb-party-member > legend.bb-party-member-legend {
    display: block;
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: 1.3;
  }

  /* THE BRING LIST. The add form is a card of the cabin's with its question
     in the display face; "Still needs somebody" is the harvest's card, the
     wheat and the garland on its heading as on the dashboard's "What we
     still need", which lists the same rows; "Somebody is bringing these" is
     the cabin's. The rows, their cards and the swipe are unchanged in
     shape: a white row on the wash is the row the dashboard draws. The
     section's top edge and wash are the shared cards' rules; the three
     other sides are drawn here, because a section had no border before. */
  .bb-bring-ask {
    padding: var(--space-4);
    border: 1px solid var(--color-line);
    border-block-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius);
    background: var(--color-surface);
  }

  .bb-bring-ask-what {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  .bb-bring:is(.bb-cabin, .bb-harvest) {
    padding: var(--space-4);
    border-inline: 1px solid var(--color-line);
    border-block-end: 1px solid var(--color-line);
  }

  .bb-bring.bb-cabin {
    background: var(--color-surface);
  }

  .bb-bring-heading {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* The card as a modal (D-73h): the cabin's edge and radius, its title in
     the display face. In the row it has no box, as before. */
  .bb-bring-card.bb-bring-card-modal {
    border-block-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius);
  }

  .bb-bring-card-title {
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
  }

  /* "Nothing is on the list yet.", the line right under the add form when
     the list is empty: a notice in the harvest's colours, the shape /meals
     gives its closing day, so an empty page still has a warm thing on it. */
  .bb-bring-ask + .bb-note {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-inline-start: 0.3rem solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-harvest);
  }
}

/* ------------------------------------------------------------------ *
 * G2, "The cabin": /sleeping, /parking and the recipe pages. Owner decision
 * D-108, carrying D-106's ask to these pages: "all pages should get full
 * attention" and "simple meant simple to use, not simple and basic looking."
 *
 * Every control keeps the shape and the floor it had. What changes is the
 * look: the sleeping board, the driveway's lanes, the recipe search and a
 * recipe's two halves are cards of the cabin's (D-106b), each headed by a
 * mark drawn for the page (a bed, a car, the pot, a head of wheat, a book, a
 * web page); the organisers' reply and "Nothing matched" wear the harvest.
 * Every colour is a token the dark theme already re-points; there is no new
 * token.
 *
 * SCOPED TO THE MEMBER PAGES. The organiser boards draw the same hooks (the
 * bed list, the lanes and spaces, the recipe list and the entry form), and
 * they are another group's to style, so every rule below reaches its hook
 * through a wrapper only the member page carries: `.bb-sleep-board`, a lane
 * that is `.bb-cabin`, `.bb-park-*`, `.bb-recipe-results`, `.bb-recipe-about`,
 * `.bb-recipe-part` and `.bb-recipe-way`.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The sleeping board ------------------------------------------- */

  /* The organisers' reply (D-92) is a card of the harvest's: the wash and
     the autumn edge say "a message to this household" where the accent edge
     it carried said "announcement". The garland is drawn before the words
     and shown after them, the meal heading's trick, so the words stay the
     last thing in the heading. Their words in the display face, as something
     said rather than a field. */
  .bb-bed-reply.bb-harvest {
    border-inline-start: 1px solid var(--color-line);

    & > .bb-panel-heading > .bb-garland {
      order: 1;
    }

    & > .bb-bed-reply-words {
      font-family: var(--font-display);
      font-size: var(--text-lg);
      font-weight: 400;
    }
  }

  .bb-sleep-board > .bb-sleep-list {
    gap: var(--space-2);
  }

  /* One room on the member's board: the bed down the row's left, the room's
     name in the display face, the bed and who is in it under it, on the
     quiet surface inside the white card. The organiser's placing board draws
     the same hooks and is not reached: this is `.bb-sleep-board`'s. */
  .bb-sleep-board .bb-sleep-spot {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "mark label"
      "mark bed"
      "mark who";
    column-gap: var(--space-3);
    align-items: start;
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface-2);

    & > .bb-panel-mark {
      grid-area: mark;
      margin-block-start: 0.15rem;
      color: var(--color-line-strong);
    }

    & > .bb-sleep-label {
      grid-area: label;
      font-family: var(--font-display);
      font-size: var(--text-lg);
      font-weight: 400;
      line-height: var(--leading-tight);
    }

    & > .bb-sleep-description {
      grid-area: bed;
      font-size: var(--text-sm);
    }

    & > .bb-sleep-status {
      grid-area: who;
      justify-self: start;
    }

    /* A room with somebody in it lights its bed in pine; a held one in sage,
       the colour its note already carries. Colour is the second signal: the
       words under the name are the first. */
    &:has(> .bb-sleep-taken) > .bb-panel-mark {
      color: var(--color-pine);
    }

    &:has(> .bb-sleep-reserved) > .bb-panel-mark {
      color: var(--color-sage);
    }
  }

  /* "Rooms are not sorted out yet": the quiet surface inside the card, with
     the pine edge. */
  .bb-sleep-board > .bb-sleep-pending {
    border-inline-start-color: var(--color-pine);
    background: var(--color-surface-2);
  }

  /* The driveway ------------------------------------------------- */

  /* The cars reminder and the report form: each a card of the cabin's whose
     heading, control or boxes sit a step apart. */
  .bb-park-cars,
  .bb-park-where {
    gap: var(--space-3);
  }

  /* The lanes are cards of the cabin's and carry the panel's own spacing
     under them, so the driveway's gap goes. */
  .bb-driveway:has(> .bb-cabin) {
    gap: 0;
  }

  /* One space, with the car down its left. The mark is drawn for every space
     and HIDDEN, keeping its column, while the car's line is empty, so a car
     arriving on an open page writes text into a box that does not move
     (D-72, the same rule the owner's blank line keeps). The four lines keep
     their order and each its row; "Empty" takes the car's row while the
     car's line is empty, which is when it is shown. */
  .bb-lane-block.bb-cabin .bb-space {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "mark label"
      "mark car"
      "mark owner"
      "mark state";
    column-gap: var(--space-3);
    align-items: center;

    & > .bb-panel-mark {
      grid-area: mark;
      inline-size: 1.75rem;
      block-size: 1.75rem;
    }

    & > .bb-space-label {
      grid-area: label;
    }

    & > .bb-space-car,
    & > .bb-space-none {
      grid-area: car;
    }

    & > .bb-space-owner {
      grid-area: owner;
    }

    & > .bb-space-state {
      grid-area: state;
    }

    &:has(> .bb-space-car:empty) > .bb-panel-mark {
      visibility: hidden;
    }
  }

  /* "The best order to arrive in": the numbers in pine and bold, because the
     number is the content (the base layer keeps an ordered list's markers),
     and a dotted rule between cars. */
  .bb-park-arrival .bb-arrival-car {
    padding-block: var(--space-1);
    border-block-end: 1px dotted var(--color-line-strong);

    &::marker {
      color: var(--color-pine);
      font-weight: 700;
    }

    &:last-child {
      border-block-end: 0;
    }
  }

  /* Blocked in: each car's row on the quiet surface inside the card. */
  .bb-park-blocked .bb-blocked-item {
    background: var(--color-surface-2);
  }

  /* The recipe search -------------------------------------------- */

  /* The search card: its heading, the box and "Not in the list?" a step
     apart. */
  .bb-recipe-find {
    gap: var(--space-3);
  }

  /* "Nothing matched" wears the harvest: the wash and the autumn edge in
     place of the strong grey line, the pot on its first sentence. It still
     sets no `display`, so `.bb-recipe-none`'s own `[hidden]` override is
     what hides it. */
  .bb-recipe-none.bb-harvest {
    border: 1px solid var(--color-line);
    border-block-start: 0.3rem solid var(--color-pumpkin);
  }

  .bb-recipe-none-lead {
    display: flex;
    align-items: start;
    gap: var(--space-2);

    & > .bb-panel-mark {
      margin-block-start: 0.15rem;
      color: var(--color-pumpkin);
    }
  }

  /* A result in the member's list: a pine edge down its leading side, so a
     column of 230 reads as a column of the cabin's cards without 230 thick
     top edges. `li.bb-recipe` and not `.bb-recipe` here, because the test
     that holds the search's hiding rule reads the first `.bb-recipe {` in
     this file and this must not be it. */
  .bb-recipe-results > li.bb-recipe {
    border-inline-start: 0.3rem solid var(--color-pine);
  }

  /* "One of ours." and "Added here, from <site>" on a result, and who typed
     it on a recipe's page: a sage pill with an edge, the dashboard's badge
     shape, so the family's own recipes stand out by shape and words and not
     by colour alone. */
  .bb-recipe-results .bb-recipe-ours,
  .bb-recipe-about > .bb-recipe-ours {
    justify-self: start;
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-sage);
    border-radius: 1rem;
    background: var(--tint-sage);
    color: var(--color-text);
  }

  /* "What is in it" under a result: the outlined button the meals page's
     Recipe wears (D-105a), still the plain link it was. */
  .bb-recipe-results .bb-recipe-see {
    justify-self: start;
    justify-content: center;
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    line-height: var(--leading-tight);
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
      text-decoration: none;
    }
  }

  /* The name when there is no button, in the display face. */
  .bb-recipe-results .bb-recipe-name {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* One recipe --------------------------------------------------- */

  /* The lines under the title: who typed it (the pill), the one line about
     it at the lead's size, and where it came from. */
  .bb-recipe-about {
    display: grid;
    gap: var(--space-2);
    justify-items: start;
    max-inline-size: var(--measure);

    & > .bb-recipe-summary {
      font-size: var(--text-lg);
      color: var(--color-text);
    }
  }

  /* The ingredients inside their card: a dotted rule between lines and no
     margin above the list, the heading's gap being the card's. */
  .bb-recipe-part > .bb-ingredients {
    margin-block-start: 0;
    gap: 0;
  }

  .bb-recipe-part .bb-ingredient {
    padding-block: var(--space-2);
    border-block-end: 1px dotted var(--color-line-strong);

    &:last-child {
      border-block-end: 0;
    }
  }

  /* The method, numbered: each line of it is a step with its number in a
     pine disc, counted here so the markup stays one paragraph per line, which
     is what the author typed. The number is drawn in `--color-on-accent`:
     white on pine by day, the page's dark brown on the night's pine. */
  .bb-recipe-steps {
    display: grid;
    gap: var(--space-3);
    counter-reset: step;
  }

  .bb-recipe-steps > .bb-method {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-3);
    margin: 0;
    max-inline-size: none;
    counter-increment: step;

    &::before {
      content: counter(step);
      display: grid;
      place-items: center;
      inline-size: 1.75rem;
      block-size: 1.75rem;
      border-radius: 50%;
      background: var(--color-pine);
      color: var(--color-on-accent);
      font-size: var(--text-sm);
      font-weight: 700;
      line-height: 1;
    }
  }

  /* Adding a recipe ---------------------------------------------- */

  /* "Type it in" is reached by its anchor from the search's "Nothing
     matched" and from under the box; the header is sticky, so the heading
     needs room over it when the browser scrolls to the anchor. */
  .bb-recipe-way > .bb-panel-heading {
    scroll-margin-block-start: 7rem;
  }

  /* On the preview, where it came from heads the card, in the display face. */
  .bb-recipe-way > .bb-recipe-from {
    font-family: var(--font-display);
    font-size: 1.25rem;
    line-height: var(--leading-tight);
    color: var(--color-text);
  }

  /* What the page did not give: the harvest's wash behind the accent edge,
     so "this needs you" reads as a notice and not as a refusal. */
  .bb-recipe-way > .bb-recipe-gap {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--tint-harvest);
  }
}

/* ------------------------------------------------------------------ *
 * D-112 meals voting: /meals itself, on the owner's phone.
 *
 * The owner, on his Pixel at 412px in the dark theme: "the meal voting
 * page is a failure on my mobile screen. looks all jumbled ... the recipe
 * button is floating all nonsensical. we have a checkbox that now feels
 * lame and dated, the one voting digit feels unattached and unstyled."
 * MEASURED on the live page and on his screenshot before this section:
 * every dish stacked three unlike controls (the browser's tick with the
 * name and a loose count pill at the edge, then a small Recipe button
 * indented to the name on a line of its own, then a full-width "Take this
 * off the list"); the instruction stood inside each of the five meals that
 * had dishes, above and below them; and the page changed shape down its
 * length, a meal with dishes a tall card and a meal without a thin row
 * with a small red link.
 *
 * WHAT THIS SECTION DOES. Every control keeps its words, its markup and
 * its floor; a tap does what it did.
 * - A dish is ONE card with two lines. The first is the tick's label, the
 *   whole width but the count: the box, drawn here as a rounded tile that
 *   fills in the accent with a tick when it is checked, then the name,
 *   then the count in a badge that fills the same way. The second is a
 *   strip at the card's foot, under a hairline, holding what else there is
 *   to press: Recipe at its start and the reader's own "Take this off the
 *   list" at its end. The same two lines at every width: D-106's
 *   side-by-side row from a 560px ballot is not kept, so a dish reads the
 *   same on a phone and in the wide page's two columns.
 * - SUPERSEDED 2026-10-08 BY D-113, the section at the end of this file:
 *   the tile below became every tick and pick on the site, and then the
 *   owner chose a vote button for the ballots, so an open row here draws
 *   "Vote" / "Voted" from that section and no tile. The next bullet is
 *   kept as the record of what D-112 built.
 * - The box is still the real `<input type="checkbox">`. `appearance:
 *   none` takes the browser's drawing off it and nothing else: it keeps
 *   its name and value, the keyboard toggles it, a screen reader reads it,
 *   and with no stylesheet it is a checkbox. The tick inside is a
 *   `::before` cut with `clip-path`, so nothing is fetched. Under forced
 *   colours the browser's own drawing comes back, because a tile drawn in
 *   background colours would lose its checked state there (NOT MEASURED:
 *   the render tool has no forced-colours mode).
 * - The instruction is said ONCE, above the list: `crate::routes::meals::
 *   MealsView` draws it there, and a round that takes one tick keeps "Pick
 *   one." on its own card. The pair of sentences under a ballot that
 *   `crate::instant` swaps between stays in every form, because the bundle
 *   finds it there; the stylesheet shows the pair on the first meal that
 *   carries one and on no meal after it.
 * - An empty meal is the same card as a full one: the cabin's edge, the
 *   pot before its heading in the display face, and "Nominate a dish" as
 *   the outlined button a full card ends in, at the end of the heading's
 *   line. Its whole card is still the link (D-62a's stretched `::after`).
 *   The cost against D-62a's compact row is height, in D-112's record.
 *
 * SCOPED TO /meals through `.bb-meals`, which `crate::routes::meals` emits
 * and nothing else does (MEASURED by grep: one emitter). The Thanksgiving
 * ballot draws `.bb-dish` and `.bb-check` too and is not reached; the
 * organiser board draws its own hooks. Every colour is a token the dark
 * theme re-points; the one new hook is `bb-dish-acts`, the strip.
 *
 * ONE SECTION, at the end, after G1's and G2's, for the reason G1 gives: a
 * later rule at the same or higher specificity wins without an earlier one
 * being edited. The one earlier rule this leans on unchanged is
 * `.bb-dish-recipe`, which a test reads as the one rule for its class; its
 * `grid-area` does nothing inside the strip, which is a flex row.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The page's one instruction, a direct child of the column (nothing else
     with this class is), drawn closer to the list it governs than the
     column's own gap: it is about the boxes under it, not a notice. */
  .bb-main > .bb-ballot-heading {
    margin-block-end: calc(var(--space-3) - var(--space-5));
  }

  /* One dish, one card. Two columns, the label and the badge; two rows,
     the label's line and the strip. The card carries the edge and the
     tint; ticked, both go to the accent. */
  .bb-meals .bb-dish {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "tick count"
      "acts acts";
    gap: 0;
    border: 2px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    &:has(> .bb-check > input:checked) {
      border-color: var(--color-accent);
      background: var(--tint-accent);
    }

    /* The keyboard ring goes round the card, which is the thing that
       lights, rather than round the label's box inside it, and not round
       the input as well: the base layer's `:focus-visible` rule reaches the
       input, and MEASURED with the first tile focused at 412, the tile drew
       that ring inside the card's. One ring. The input's own ring is taken
       off every tick and pick by the D-113 section at the end of this
       file, where the rule that stood here moved. */
    &:has(> .bb-check > input:focus-visible) {
      outline: 3px solid var(--color-focus);
      outline-offset: 3px;
    }
  }

  /* The label: the first line, the box and the name, at the floor, with
     the card's own edge and tint instead of a box of its own. */
  .bb-meals .bb-dish > .bb-check {
    grid-area: tick;
    align-items: start;
    gap: var(--space-3);
    min-block-size: var(--tap-min);
    padding: var(--space-3) var(--space-2) var(--space-3) var(--space-4);
    border: 0;
    border-radius: 0;
    background: transparent;

    &:has(input:checked) {
      border: 0;
      background: transparent;
    }

    &:has(input:focus-visible) {
      outline: none;
    }
  }

  /* THE BOX WAS DRAWN HERE as a rounded tile, scoped to /meals. D-113 lifted
     that rule, unchanged, to every `.bb-check` input on the site (the
     section at the end of this file), and then the owner asked for a vote
     button on the two ballots instead of a tick, so this page's open rounds
     draw `.bb-vote` from the same section. Nothing about the box is said
     here any more. */

  /* A closed round's name, a bare span where the label would be: the same
     inset, so names line up down a card whether the round is open or not. */
  .bb-meals .bb-dish > span:first-child {
    grid-area: tick;
    align-self: center;
    padding: var(--space-3) var(--space-2) var(--space-3) var(--space-4);
  }

  /* THE COUNT BADGE WAS DRAWN HERE for /meals alone: a badge at the end of
     the first line with a small tick cut out before the number, the second
     surface on a plain card and the accent on a ticked one. D-113 moved
     that rule, unchanged in look, to the end of this file, where it reaches
     both ballots and the badge's new place inside an open row's label. */

  /* The strip at the card's foot: what else there is to press, under a
     hairline inset to the card's own inset, Recipe at the start and the
     reader's own button at the end. It wraps where the two do not fit one
     line, the button keeping the end of its line. */
  .bb-meals .bb-dish-acts {
    grid-area: acts;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    margin: 0 var(--space-3) 0 var(--space-4);
    padding: var(--space-2) 0;
    border-block-start: 1px solid var(--color-line);
  }

  /* "Take this off the list" in the strip: its own width at the end of
     the line, the lighter line and the muted words, so it is the quieter
     of the two and still a button at the floor (`.bb-decline`'s). */
  .bb-meals .bb-dish-acts > .bb-decline {
    inline-size: auto;
    margin: 0 0 0 auto;
    padding: var(--space-2) var(--space-3);
    border-color: var(--color-line);
    background: transparent;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* "Nominate a dish" under a ballot: the same outlined button, its own
     width at the end of the card's line rather than the whole width, so a
     full card ends the way an empty card's heading line does. */
  .bb-meals .bb-suggest {
    inline-size: auto;
    justify-self: end;
    margin-block-start: var(--space-2);
    padding-inline: var(--space-3);
  }

  /* The two sentences under a ballot, one shown at a time by the bundle:
     quieter than the dishes they sit under, now that the page's
     instruction is above the list and these say only how a tick is
     saved. */
  .bb-meals form[data-bb-instant] > .bb-note {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
  }

  /* Said once. The pair stays in every ballot form, where `crate::instant`
     finds it; the stylesheet shows it on the first meal that carries a
     pair for that kind of round and hides it on every meal after. By kind,
     because a round that takes one tick says something different from one
     that takes several. The sentences only, never the button that carries
     the same by-button mark: "Cast votes" is every form's own without the
     bundle. */
  .bb-meals > li:has(form[data-bb-instant]:not([data-bb-one]) p[data-bb-by-tick])
    ~ li form[data-bb-instant]:not([data-bb-one]) :is(p[data-bb-by-tick], p[data-bb-by-button]),
  .bb-meals > li:has(form[data-bb-instant][data-bb-one] p[data-bb-by-tick])
    ~ li form[data-bb-instant][data-bb-one] :is(p[data-bb-by-tick], p[data-bb-by-button]) {
    display: none;
  }

  /* An empty meal: the same card as a full one. D-62a's compact row keeps
     its markup and its whole-card link; what changes is the look: the
     cabin's edge and radius, a full card's inset, the pot and the display
     face on the heading, and the cue as the outlined button a full card
     ends in. `.bb-cabin` is not on the row (a test reads the row's class
     list), so the cabin's two lines are said again here. */
  .bb-meals > .bb-meal-empty {
    gap: var(--space-2);
    align-content: start;
    padding: var(--space-4);
    border-radius: var(--radius);
    border-block-start: 0.3rem solid var(--color-pine);
  }

  /* The heading takes its whole line, as on a full card, and the cue the
     line after it, at the end. Not beside the heading: at 412 "Wednesday
     dinner" in the display face plus the button is wider than the card and
     "Friday lunch" is not, so the two empty meals were two shapes (MEASURED
     before this rule, 161.8px and 125.8px). */
  .bb-meals .bb-meal-empty-link {
    align-items: center;
    gap: var(--space-2) var(--space-3);

    & > .bb-meal-heading {
      flex-basis: 100%;
      display: flex;
      align-items: center;
      gap: var(--space-2);
      font-family: var(--font-display);
      font-size: 1.25rem;
      font-weight: 400;
      line-height: var(--leading-tight);
    }
  }

  /* "No dishes yet" at the start of the cue's line, so the card reads
     heading, then what there is, then the button, the way a full card
     does. The note follows the link in the markup and is outside it
     (D-62a, so it is not part of the link's name), so it is drawn beside
     the button by rising the height of the button's line: that line is
     `--tap-min` tall (the cue never wraps) under the card's `--space-2`
     gap, and both are the tokens here. Only when nothing sits between the
     two: with an organiser's caption or a saved line between them the
     note stays in flow under them. The width is held short of the
     button's, so on a narrow screen the note wraps rather than runs under
     it. A tap on the note still lands on the link: D-62a's stretched
     `::after` is positioned, so it paints over the note. */
  .bb-meals .bb-meal-empty-link + .bb-meal-empty-note {
    display: flex;
    align-items: center;
    min-block-size: var(--tap-min);
    max-inline-size: calc(100% - 10.5rem);
    margin-block-start: calc(-1 * (var(--tap-min) + var(--space-2)));
  }

  .bb-meals .bb-meal-empty-cue {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-inline-start: auto;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-base);
    font-weight: 700;
    text-decoration: none;
  }
}

/* ------------------------------------------------------------------ *
 * D-113 tick tile: every tick and pick on the site, and the vote button
 * on the two ballots.
 *
 * The owner, on his Pixel 7 at 412px, called the browser's checkbox "lame
 * and dated". D-112 drew the /meals tick as a tile and he asked, of the
 * other tick and pick controls: "Yes, everywhere". Then, asked "would a
 * vote button do us better than the check box?", he picked "Vote button,
 * all ballots". So this section is two decided shapes and nothing else:
 *
 * THE TILE, for every `<input type="checkbox">` and `<input type="radio">`
 * that is the direct child of a `.bb-check` label. MEASURED by reading
 * crates/web/src on 2026-10-08: 17 such inputs in 9 files, and the one
 * checkbox outside a label is the drawer's `.bb-nav-state`, clipped out of
 * sight and not a control a reader looks at. `crate::app`'s test
 * `every_tick_and_pick_is_drawn_as_one_of_two_decided_shapes` walks the
 * source for an input these selectors do not reach.
 * - A checkbox is D-112's tile, lifted unchanged: 1.5rem square, 2px of
 *   the control border on the surface, 0.45rem corners; checked, filled in
 *   the accent with the tick cut from its `::before` by clip-path in the
 *   accent's text colour (7.49:1 light and 8.40:1 dark on the tokens).
 * - A radio button is the round one of the same family: the same size,
 *   border, colours and fill, `border-radius: 50%`, and a 0.75rem dot in
 *   the accent's text colour where the checkbox has a tick.
 * - Both are still the real controls. `appearance: none` takes the
 *   browser's drawing off and nothing else: name and value, the keyboard,
 *   a screen reader and the page with no stylesheet all have a checkbox or
 *   a radio button. Under forced colours the browser's own drawing comes
 *   back, because a tile drawn in background colours would lose its state
 *   there (NOT MEASURED: the render tool has no forced-colours mode).
 * - One keyboard ring: the label's, which `.bb-check:has(input:focus-visible)`
 *   in the components layer draws (on /meals, the card's). The input's own
 *   ring from the base layer's `:focus-visible` is taken off here, as D-112
 *   measured it drawing a second ring inside the first.
 * - `margin: 0` replaces the components layer's `0.15rem 0 0`, which sat the
 *   browser's smaller box on the first text line; the tile is a line tall.
 *
 * THE VOTE BUTTON, for an open row on /meals and on /meals/thanksgiving.
 * The row's label carries `bb-vote` beside `bb-check`; inside it the real
 * input (a checkbox on a round that takes several, a radio button on one
 * that takes one, so moving a vote is the browser's own radio behaviour),
 * the dish's name, the count badge, and the two words "Vote" and "Voted".
 * The input is clipped to a point the way the drawer's state is: still
 * focusable, still read as a checkbox or radio button named by the label's
 * words. The words are drawn as the button, outlined in the accent reading
 * "Vote", filled in the accent with a tick cut-out reading "Voted" when the
 * input is checked; the one not in force is hidden, so a screen reader
 * hears one of them (NOT MEASURED with a screen reader). D-114 changed how:
 * `visibility: hidden` in the button's own cell rather than `display:
 * none`, so both words size the cell and "Vote" is as wide as "Voted". It
 * also fixed where the parts of the row sit (the row's rule below). The
 * card fills with it as D-112's did. The
 * instruction sentences that explained ticking are gone from both pages;
 * the button says it.
 *
 * ONE SECTION, at the end, after D-112's, for the reason G1 gives: a later
 * rule at the same or higher specificity wins without an earlier one being
 * edited. The tile lives in the components layer, where `.bb-check` is; the
 * vote rules in the pages layer, where the ballots' cards are.
 * ------------------------------------------------------------------ */

@layer components {

  .bb-check > input[type="checkbox"],
  .bb-check > input[type="radio"] {
    appearance: none;
    display: grid;
    place-content: center;
    inline-size: 1.5rem;
    block-size: 1.5rem;
    margin: 0;
    border: 2px solid var(--color-line-strong);
    border-radius: 0.45rem;
    background: var(--color-surface);
    cursor: pointer;

    &::before {
      content: "";
      inline-size: 0.9rem;
      block-size: 0.9rem;
      background: var(--color-on-accent);
      clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
      transform: scale(0);
    }

    &:checked {
      border-color: var(--color-accent);
      background: var(--color-accent);

      &::before {
        transform: scale(1);
      }
    }

    &:focus-visible {
      outline: none;
    }
  }

  .bb-check > input[type="radio"] {
    border-radius: 50%;

    &::before {
      inline-size: 0.75rem;
      block-size: 0.75rem;
      border-radius: 50%;
      clip-path: none;
    }
  }

  @media (forced-colors: active) {
    .bb-check > input[type="checkbox"],
    .bb-check > input[type="radio"] {
      appearance: auto;

      &::before {
        display: none;
      }
    }
  }
}

@layer pages {

  /* The row's label as the vote row, D-114: ONE FIXED LAYOUT, so the button
     and the count are in the same place on every row whatever the name.
     Two columns: the name, and a column as wide as the button. The button
     sits at the top of that column and the count directly under it,
     centred; the name takes the rest of the width and wraps inside it,
     starting level with the button. The button's row is exactly the
     button's height (both `--tap-min`), so a name of any length makes the
     row taller UNDER the button and never moves the button or the count.
     D-113 drew this as a flex row wrapping by the name's own width, and
     where the badge and the button landed changed with the name: the owner,
     2026-10-08, "rendering in different places based on the title length".
     Not three columns on one line: MEASURED with this rule's areas set to
     "name count vote", the name's column was 109.6px at 412 and 17.6px at
     320, where the live breakfast dish stood 29 lines tall.
     The padding is the ballot's tighter one (D-62d) on both pages; the
     floor is the button's. The same specificity as D-112's label rule
     above, so this one wins by order. */
  .bb-dish > .bb-check.bb-vote {
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content;
    grid-template-rows: var(--tap-min) auto;
    grid-template-areas:
      "name vote"
      "name count";
    align-items: start;
    gap: 0 var(--space-3);
    padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);

    /* The real control, clipped to a point and kept in the tab order, as
       `.bb-nav-state` is. The tile rule above gave it a size and a tick;
       both go. */
    & > input {
      position: absolute;
      display: block;
      inline-size: 1px;
      block-size: 1px;
      margin: 0;
      padding: 0;
      border: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;

      &::before {
        content: none;
      }
    }

    /* The name, down the whole row. At least the button's height and its
       words centred in that, so a name of one line reads level with the
       button and a longer one starts at the button's top. A word too long
       for the column breaks inside it rather than running under the
       button. */
    & > .bb-dish-name {
      grid-area: name;
      display: flex;
      align-items: center;
      min-block-size: var(--tap-min);
      min-inline-size: 0;
      overflow-wrap: anywhere;
    }

    /* The count under the button, centred on it, one width for a count of
       one or two figures (MEASURED: 63px at 412 for every count rendered,
       0 to 12), so its box is the same on every row too. A row
       with no count ("No preference") leaves the place empty and the
       button where it is on the rows that have one. */
    & > .bb-dish-count {
      grid-area: count;
      justify-self: center;
      align-self: start;
      min-inline-size: 3.5rem;
      margin: var(--space-1) 0 0;
    }

    /* Both words in the button's place, one over the other, so the column
       is as wide as the wider of them ("Voted") on every row and pressing
       never changes the button's size; the one not in force is hidden
       below. Exactly `--tap-min` tall, the button's row's height. */
    & > .bb-vote-say,
    & > .bb-vote-said,
    & > .bb-vote-unsent {
      grid-area: vote;
      block-size: var(--tap-min);
      white-space: nowrap;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.35rem;
      min-inline-size: 5rem;
      min-block-size: var(--tap-min);
      padding: 0 var(--space-3);
      border: 2px solid var(--color-accent);
      border-radius: var(--radius-sm);
      background: var(--color-surface);
      color: var(--color-accent);
      font-weight: 700;
      line-height: 1.2;
    }

    & > .bb-vote-said {
      visibility: hidden;
      background: var(--color-accent);
      color: var(--color-on-accent);

      &::before {
        content: "";
        flex: none;
        inline-size: 0.8rem;
        block-size: 0.8rem;
        background: currentColor;
        clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
      }
    }

    &:has(> input:checked) {
      & > .bb-vote-say {
        visibility: hidden;
      }

      & > .bb-vote-said {
        visibility: visible;
      }
    }

    /* HC-06: "Chosen", outlined like "Vote", on a ticked row whose vote is
       not stored, while no bundle has taken the form; the bundle saves each
       tick and marks this `hidden` (`crate::instant`), and a row with a
       stored vote does not draw it. `hidden` is a marker here and not what
       hides it: the rule above gives it a `display`, which beats the user
       agent's `[hidden]`, and visibility decides, as for the other two. */
    & > .bb-vote-unsent {
      visibility: hidden;
    }

    &:has(> input:checked):has(> .bb-vote-unsent:not([hidden])) {
      & > .bb-vote-said {
        visibility: hidden;
      }

      & > .bb-vote-unsent {
        visibility: visible;
      }
    }
  }

  /* The count badge, D-112's look on both ballots: a small tick cut out
     before the number so it reads as "voted for by this many", the second
     surface on a plain card and the accent on a voted one. Inside an open
     row's label it sits under the button (D-114, the row's rule above,
     which also sets its margin and width there); on a closed row, where
     there is no label, it keeps D-112's place at the end of the first
     line. The
     bundle writes a new number into the span's text and the cut-out is a
     `::before`, so it stays. Tabular figures and a two-figure minimum. */
  .bb-dish .bb-dish-count {
    grid-area: count;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    align-self: center;
    justify-self: end;
    min-inline-size: 3rem;
    margin: var(--space-2) var(--space-3) var(--space-2) 0;
    padding: 0.2rem var(--space-2);
    border: 2px solid var(--color-line);
    border-radius: 1rem;
    background: var(--color-surface-2);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.3;

    &::before {
      content: "";
      flex: none;
      inline-size: 0.7rem;
      block-size: 0.7rem;
      background: currentColor;
      clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
    }
  }

  .bb-dish > .bb-check.bb-vote:has(> input:checked) > .bb-dish-count {
    border-color: var(--color-accent);
    background: var(--color-accent);
    color: var(--color-on-accent);
  }
}

/* ------------------------------------------------------------------ *
 * D-114 the vote row and the question titles: two things the owner saw
 * on his phone on 2026-10-08.
 *
 * THE VOTE ROW. "the vote button on the meals voting page is rendering in
 * different places based on the title length." The fix is D-113's row
 * rule, changed in place above rather than overridden from here: one
 * fixed layout, a column for the name and a column as wide as "Voted",
 * the button at the top of that column and the count under it.
 * MEASURED in Chrome at 412 with the bundle, light and dark, before it:
 * the button's top-left corner inside its row was at x 217.6 or 230.5
 * ("Voted" is 102.9px wide, "Vote" 90) and at y 9, 50, 72 or 99 by the
 * name, and the count in six places. After it, on all 10 open rows of
 * /meals and all 14 of the ballot: the button at (217.6, 9), 102.9 by
 * 54, and the count at (237.6, 67.5); one place per page at 320, 390 and
 * 1280 too.
 *
 * THE QUESTION TITLES. "rsvp page has the same title issue that the
 * thanksgiving dinner voting page had with the titles." A `legend` is
 * drawn on its fieldset's top border by default, and the older rules
 * painted it the surface colour with a little padding so it read as a
 * heading in a gap; on a card with its own edge and wash that is a box
 * sticking out of the card's top. The ballot's questions were fixed on
 * 2026-10-08 by floating the legend: a floated legend is not drawn on the
 * border and takes its place inside the card like any other row. This
 * rule does that ONCE for every fieldset a member's page draws as a card,
 * and the ballot's own rule (D-106's section, "A question on the
 * Thanksgiving ballot") keeps only its look. `crate::app`'s test
 * `every_fieldset_title_sits_inside_its_card` walks the source for a
 * fieldset this list does not name.
 * MEASURED in Chrome, light and dark, at 320, 390, 412 and 1280 (/rsvp
 * with the bundle; /sleeping's preview carries none): before it, /rsvp's
 * three questions and each person, and /sleeping's question, had the
 * legend's top on the card's top edge (0px inside), painted the surface
 * colour; after it, the top is 20px inside /rsvp's question cards, 19
 * inside each person's and /sleeping's (22 and 21 at 1280's larger
 * root), with no box of its own. The ballot's are 19, as before.
 *
 * ONE SECTION, at the end, for the reason G1 gives: a later rule at the
 * same or higher specificity wins without an earlier one being edited.
 * Each selector here is (0,1,1), the same as the pages layer's earlier
 * legend rules that set these properties (`.bb-rsvp legend` and the
 * ballot's `.bb-feast-question > legend`), so this one wins by order; the
 * components layer's legend rules lose to the pages layer whatever their
 * specificity.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The title inside its card: out of the border, the whole width, no
     box of its own, spaced by the fieldset's own gap like any other row.
     Every one of these fieldsets is a grid (its components rule), so the
     float only takes the legend off the border; the grid places it. */
  .bb-rsvp-response > legend,
  .bb-rsvp-nights > legend,
  .bb-rsvp-housing > legend,
  .bb-rsvp-parking > legend,
  .bb-rsvp-pledge > legend,
  .bb-party-member > legend,
  .bb-feast-question > legend,
  .bb-beds-nights > legend {
    float: inline-start;
    inline-size: 100%;
    margin: 0;
    padding: 0;
    background: none;
  }
}

/* ------------------------------------------------------------------ *
 * G4, "The organiser, part 1": the hub at /o, the meals board, the
 * Thanksgiving dinner board, the trip dates board, the cabin address and
 * the post box. Owner decision D-110, carrying D-106's ask ("all pages
 * should get full attention" and "simple meant simple to use, not simple
 * and basic looking") to the organiser's own pages.
 *
 * Every control keeps its words, its address and the floor. What changes
 * is the look: each board's panels are cards of the cabin's or the
 * harvest's (D-106b), headed by a mark drawn for the page (the pot, the
 * leaf, the snowflake, the cabin, the envelope, the key); the hub groups
 * its eleven boards into four such cards; a dish row on a board is one
 * white row with its count in the badge the voting page gives a count,
 * which fixes the count sitting on a line of its own under the name
 * (LOOKED AT on D-113's render of the meals board at 412). Every colour is
 * a token the dark theme already re-points; there is no new token and no
 * new drawing.
 *
 * SCOPED TO THESE PAGES. The organiser's other boards are G5's: every rule
 * below reaches its hook through a class only these six pages carry
 * (`.bb-o-group`, `.bb-o-index`, `.bb-say`, `.bb-cabin-board`, `.bb-trip-*`,
 * `.bb-board`, `.bb-board-pick`, `.bb-o-options`, `.bb-o-totals`) or a
 * shared card hook that G5 reaches the same way.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The hub ---------------------------------------------------------- */

  /* Four cards hold the eleven boards: the trip, the food, the cabin and
     the people. Each board is still D-105's control with the chevron and
     what it is for on the line under it; the line is muted and the
     control's words are not, so the name is what the eye lands on. The
     list keeps D-105's gap between boards inside a card. */
  .bb-o-group > .bb-o-index {
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-o-index > li {
    color: var(--color-text-muted);
  }

  .bb-o-index > li > .bb-panel-action {
    margin-block: 0 var(--space-1);
    color: var(--color-text);
  }

  /* The post box ----------------------------------------------------- */

  /* The box is a card of the cabin's with the envelope; the words above
     the box stay the field's label. The list under it is the feed's own
     rows (G1's rules reach `.bb-feed` here too), in a card with the pine. */
  .bb-say > .bb-announce {
    max-inline-size: none;
  }

  /* The cabin address ------------------------------------------------ */

  .bb-cabin-board > .bb-cabin-edit {
    max-inline-size: none;
  }

  /* The trip dates board --------------------------------------------- */

  /* The days as they stand, and what a move touches: rows with the dotted
     rule the dashboard's nights have, in place of a bulleted list. */
  .bb-trip-days,
  .bb-trip-changes {
    padding-inline-start: 0;
    list-style: none;

    & > li {
      padding-block: var(--space-1);
      border-block-end: 1px dotted var(--color-line-strong);
    }

    & > li:last-child {
      border-block-end: 0;
    }
  }

  /* The headed lists of what a move touches, inside one card: each heading
     is a sub-heading in the display face with a short rule under it, pine
     on the preview's cabin card and pumpkin on the saved board's harvest
     card. The heading hooks are unchanged (a test reads them by their
     markup); the card is what is new. */
  :is(.bb-trip-changeset, .bb-trip-told) > .bb-board-heading {
    margin: var(--space-2) 0 0;
    padding-block-end: var(--space-1);
    border-block-end: 2px solid var(--color-pine);
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  :is(.bb-trip-changeset, .bb-trip-told) > .bb-board-heading:first-of-type {
    margin-block-start: 0;
  }

  .bb-trip-told > .bb-board-heading {
    border-block-end-color: var(--color-pumpkin);
  }

  /* A form that follows its card's heading or a sentence needs no rule
     over it: the card's edge is the boundary. After a list it keeps the
     rule, which is where it tells the list from the box under it. The
     column cap is lifted inside a card, whose own width is the measure.
     NAMED CARDS ONLY, every one on this group's six pages, so a card of
     G5's that holds a form is not reached. */
  :is(.bb-trip-move, .bb-trip-names, .bb-board-empty, .bb-board-closing, .bb-board-add, .bb-o-cooks, .bb-o-close, .bb-o-setup, .bb-o-settle, .bb-o-options) > :is(.bb-panel-heading, .bb-panel-lead, .bb-panel-note, .bb-note) + .bb-open-vote {
    margin-block-start: 0;
    padding-block-start: 0;
    border-block-start: 0;
  }

  :is(.bb-trip-move, .bb-trip-names, .bb-board-empty, .bb-board-closing, .bb-board-add, .bb-o-cooks, .bb-o-close, .bb-o-setup, .bb-o-settle, .bb-o-options) > .bb-open-vote {
    max-inline-size: none;
  }

  /* The meals board -------------------------------------------------- */

  /* A meal is a card of the cabin's with the pot before its name, as its
     card on /meals is; the Thanksgiving dinner's is the harvest's with the
     leaf and the garland. The closing day and "Add a meal" are cabin cards
     too. The shared heading rule lays the display face out; what is here
     is the inside of a card. */

  /* One round of voting is an inset on the sand, so a meal with two rounds
     reads as two groups and the rows inside are seen against the sand and
     not against the card. */
  .bb-board .bb-board-round {
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--tint-sand);
  }

  /* "Vote 1 chooses 3 dishes. Open for votes." is the round's title. */
  .bb-board .bb-board-round-what {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* A dish on the board is one white row: the name and its count on the
     first line, and "How to cook it", when the round offers it, on a line
     of its own under the name, the outlined button at its own width, the
     way /meals puts Recipe under a dish (D-112). Before this the count sat
     on a line of its own under the button (LOOKED AT at 412). */
  .bb-board .bb-board-dish {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "name count"
      "how how";
    align-items: center;
    gap: var(--space-2) var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    & > span:first-child {
      grid-area: name;
      min-inline-size: 0;
      overflow-wrap: anywhere;
    }

    & > .bb-dish-count {
      grid-area: count;
    }

    & > .bb-open-vote {
      grid-area: how;
      justify-self: start;
    }

    & > .bb-open-vote > .bb-decline {
      inline-size: auto;
      min-block-size: var(--tap-min);
      margin: 0;
      padding-inline: var(--space-4);
    }
  }

  /* The count on a board row: the badge a dish's count wears on the voting
     page (D-112c), the tick cut-out before the number so it reads as
     "ticked by this many". The same declarations as that page's rule,
     which is scoped to its own `.bb-dish` and cannot reach a board row. */
  .bb-board .bb-dish-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    min-inline-size: 3rem;
    padding: 0.2rem var(--space-2);
    border: 2px solid var(--color-line);
    border-radius: 1rem;
    background: var(--color-surface-2);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.3;

    &::before {
      content: "";
      flex: none;
      inline-size: 0.7rem;
      block-size: 0.7rem;
      background: currentColor;
      clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
    }
  }

  /* The nothing-yet row inside a round: muted words on the sand, no box. */
  .bb-board .bb-board-dishes > .bb-dish-empty {
    padding-block: var(--space-1);
  }

  /* The close's review: a dish is its tick's label, the whole row, with the
     tile, the name and the count badge on one line, and a ticked row lit
     in the accent's tint with the badge filled, so what the organiser is
     about to confirm is seen at a glance. The row's own box goes; the label
     has one. */
  .bb-board .bb-board-pick {
    display: block;
    padding: 0;
    border: 0;
    background: none;

    & > .bb-check {
      grid-template-columns: auto 1fr auto;
      align-items: center;
      gap: var(--space-3);
      padding: var(--space-2) var(--space-3);
    }

    /* `grid-area: auto`, because the voting page's bare `.bb-dish-count`
       rule names an area (`count`) this label's grid does not have, and
       the browser then put the badge in an implicit fifth column on a
       second row (MEASURED in Chrome at 412: columns "27px 143px 0px 0px
       54px", the badge at row 2). The label's own tint when ticked is the
       components layer's `.bb-check:has(input:checked)`; only the badge's
       fill is added here. */
    & > .bb-check > .bb-dish-count {
      grid-area: auto;
      margin: 0;
    }

    & > .bb-check:has(> input:checked) > .bb-dish-count {
      border-color: var(--color-accent);
      background: var(--color-accent);
      color: var(--color-on-accent);
    }
  }

  /* The Thanksgiving dinner board ------------------------------------ */

  /* The sides and the ways to cook, while the vote is open: each option one
     white row on the harvest's wash with "Take off" at its end, and the
     box that adds one under the list behind its rule. */
  .bb-o-options .bb-board-dish {
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    & > span:first-child {
      min-inline-size: 0;
      overflow-wrap: anywhere;
    }

    & > .bb-open-vote > .bb-decline {
      inline-size: auto;
      min-block-size: var(--tap-min);
      margin: 0;
      padding-inline: var(--space-4);
    }
  }

  /* The totals: each question's name in the display face, and each option
     one white row on the wash with its count in the badge (the voting
     page's rule reaches `.bb-dish .bb-dish-count` here). */
  .bb-o-totals .bb-ballot-heading {
    margin-block-start: var(--space-2);
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  .bb-o-totals .bb-ballot-heading:first-of-type {
    margin-block-start: 0;
  }

  .bb-o-totals .bb-ballot > .bb-dish {
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    & > span:first-child {
      min-inline-size: 0;
      overflow-wrap: anywhere;
    }

    /* The same reset as the review row's: the voting page names an area
       this two-column row does not have. */
    & > .bb-dish-count {
      grid-area: auto;
    }
  }

  .bb-o-totals .bb-ballot > .bb-dish-empty {
    padding-block: var(--space-1);
  }
}

/* ------------------------------------------------------------------ *
 * G5, "The organiser, part 2": the people board and its two previews, the
 * PIN codes, the gate password, the beds board, the driveway board and the
 * recipe box. Owner decision D-111, carrying D-106's ask to the organiser's
 * side: "all pages should get full attention" and "simple meant simple to
 * use, not simple and basic looking."
 *
 * Every control keeps the shape and the floor it had, and every sentence
 * its words (two sentences are split into a heading and a note; two lists
 * get a heading they never had). Each board is cards of the cabin's
 * (D-106b) headed by a mark; what is handed to the organiser to pass on or
 * weigh (a code just minted, the password in force, what turning somebody
 * off changes, what a merge deletes, the recipes taken out of the list)
 * wears the harvest. One new mark, the person. No new token.
 *
 * SCOPED TO THE ORGANISER'S PAGES. Every rule reaches its hook through a
 * class only these six files draw (`bb-people-*`, `bb-pins`, `bb-code`,
 * `bb-gate-*`, `bb-beds-*`, `bb-asks`, `bb-o-lane`, `bb-o-add`,
 * `bb-recipe-box`, `bb-recipes-out`) or a hook only they draw (`bb-people`,
 * `bb-person`, `bb-issue`). The hooks another group's file also draws
 * (`bb-board-heading`, `bb-trip-changes`, `bb-intro`, `bb-panel-heading`)
 * are not restyled here; the previews stopped drawing the first two.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The people, on three boards ---------------------------------- */

  /* A person's row on the boards that list people (People, PIN codes, who
     has a bed): the pine down its leading side, as a result on /recipes and
     a post on the dashboard wear it, so a column of twenty-five reads as the
     cabin's without twenty-five thick top edges; on the sand inside the
     white card; the name in the display face. `display: grid` because the
     base rule sets none and the beds board measured two spans running
     together without it. */
  .bb-people {
    gap: var(--space-3);
  }

  .bb-people > .bb-person {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-inline-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  .bb-people .bb-person-name {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* A turned-off person: dimmed, the edge dashed, the way an empty space
     on the driveway is drawn. The "Turned off" tag is the first signal and
     this the second. */
  .bb-people > .bb-person-off {
    border-inline-start-style: dashed;
    background: var(--color-surface);

    & .bb-person-name {
      color: var(--color-text-muted);
    }
  }

  /* The people board ---------------------------------------------- */

  /* "Somebody cannot get in?": the question as a card's heading, the way to
     the PIN board as the dashboard's control under it, no air between. */
  .bb-people-help {
    gap: var(--space-3);

    & > .bb-panel-action {
      margin-block-start: 0;
    }
  }

  .bb-people-board {
    gap: var(--space-3);
  }

  /* The tags beside a name (Owner, Organiser, Turned off, No PIN yet ...):
     pills on the surface, words first as before. */
  .bb-people-board .bb-person-tag {
    padding: 0.1rem var(--space-2);
    border: 1px solid var(--color-line-strong);
    border-radius: 1rem;
    background: var(--color-surface);
    font-size: var(--text-sm);
  }

  .bb-people-board .bb-person-head {
    row-gap: var(--space-1);
  }

  .bb-people-board .bb-person-note {
    margin: 0;
  }

  /* "Two records for one person?" is a card of the cabin's now; the gap and
     the rule its own rule drew between it and the list go, because a card
     is its own boundary. The pine edge is the cabin's (a later layer than
     the base rule's 1px line). */
  .bb-people-merge {
    margin-block-start: 0;
    padding-block-start: var(--space-4);
    gap: var(--space-3);
  }

  /* The two previews (turn off, fold together): each part a card, their
     lines a list with a dotted rule between them and no bullets, the way
     back the dashboard's control under the button. */
  .bb-people-preview {
    gap: var(--space-3);
  }

  .bb-people-lines {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;

    & > li {
      padding-block: var(--space-2);
      border-block-end: 1px dotted var(--color-line-strong);
    }

    & > li:last-child {
      padding-block-end: 0;
      border-block-end: 0;
    }
  }

  /* PIN codes ------------------------------------------------------ */

  /* The code just minted: a card of the harvest's with the digits large on
     a line of their own, loosely tracked, on the white, so the boundaries
     of the value are visible (the l/1 and O/0 case the base rule records). */
  .bb-code {
    gap: var(--space-3);

    & > .bb-secret {
      margin: 0;
    }

    & > .bb-secret > code {
      padding: var(--space-2) var(--space-4);
      background: var(--color-surface);
      font-size: var(--text-3xl);
      letter-spacing: 0.12em;
    }
  }

  .bb-pins {
    gap: var(--space-3);
  }

  /* One person on the PIN board: the name and its note (locked out, never
     set a PIN) stacked, the button under them on a phone and at the row's
     end where there is room; the note a pill, the same shape as a tag. */
  .bb-pins .bb-issue {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1) var(--space-4);
    align-items: center;
    max-inline-size: none;

    & > .bb-person-note {
      justify-self: start;
      padding: 0.1rem var(--space-2);
      border: 1px solid var(--color-line-strong);
      border-radius: 1rem;
      background: var(--color-surface);
      font-size: var(--text-sm);
    }

    & > .bb-submit {
      margin-block-start: var(--space-2);
    }
  }

  @media (min-width: 40em) {
    .bb-pins .bb-issue {
      grid-template-columns: minmax(0, 1fr) auto;
      grid-template-areas:
        "name button"
        "note button";

      & > .bb-person-name {
        grid-area: name;
      }

      & > .bb-person-note {
        grid-area: note;
      }

      & > .bb-submit {
        grid-area: button;
        inline-size: auto;
        margin-block-start: 0;
      }
    }
  }

  /* The gate password ---------------------------------------------- */

  /* The password in force: a card of the harvest's; the label and the date
     as a plain pair of lines inside it rather than a second box; the
     password itself on the white, larger. */
  .bb-gate-now {
    gap: var(--space-3);

    & > .bb-facts {
      padding: 0;
      border: 0;
      background: none;
    }

    & > .bb-secret {
      margin: 0;
    }

    & > .bb-secret > code {
      background: var(--color-surface);
      font-size: var(--text-xl);
      letter-spacing: 0.08em;
    }
  }

  .bb-gate-next {
    gap: var(--space-3);

    & > .bb-rotate {
      max-inline-size: none;
    }
  }

  /* The beds ------------------------------------------------------- */

  .bb-beds-list,
  .bb-beds-put,
  .bb-beds-who,
  .bb-beds-asks,
  .bb-beds-out {
    gap: var(--space-3);
  }

  .bb-beds-put > .bb-beds-place,
  .bb-beds-out > .bb-beds-publish {
    max-inline-size: none;
  }

  /* One bed on the organiser's board, the member's row (D-108b) on the
     other side of the desk: the bed down the row's left, the bed's name in
     the display face, the room and what it is under it, whether it can be
     given out, and who is in it. The bed is pine with somebody in it, sage
     when the room is kept back, and the row is dashed when the bed is not
     in use; the strong line's colour while it waits. The words say the same
     first. */
  .bb-beds-list .bb-sleep-spot {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "mark label"
      "mark bed"
      "mark status"
      "mark who";
    column-gap: var(--space-3);
    align-items: start;
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface-2);

    & > .bb-panel-mark {
      grid-area: mark;
      margin-block-start: 0.15rem;
      color: var(--color-line-strong);
    }

    & > .bb-sleep-label {
      grid-area: label;
      font-family: var(--font-display);
      font-size: var(--text-lg);
      font-weight: 400;
      line-height: var(--leading-tight);
    }

    & > .bb-sleep-description {
      grid-area: bed;
      font-size: var(--text-sm);
    }

    & > .bb-sleep-status {
      grid-area: status;
      font-size: var(--text-sm);
    }

    & > .bb-person-note {
      grid-area: who;
    }

    &.bb-spot-full > .bb-panel-mark {
      color: var(--color-pine);
    }

    /* Who is in it is the answer the organiser came for: body colour and
       bold where "Nobody yet" above stays muted. */
    &.bb-spot-full > .bb-person-note {
      color: var(--color-text);
      font-weight: 700;
    }

    &.bb-spot-kept > .bb-panel-mark {
      color: var(--color-sage);
    }

    &.bb-spot-out {
      outline: 1px dashed var(--color-line-strong);
      outline-offset: -1px;
      background: var(--color-surface);
    }
  }

  /* What households asked for: one row per household on the sand with the
     pine down its side; the household in the display face, the facts under
     it, what they asked for, their reason in their own words (this is the
     one page that can show it, D-29), what they were told, and the box to
     write back. */
  .bb-asks {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-ask {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-inline-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);

    & > .bb-person-note {
      margin: 0;
    }

    & > .bb-beds-note {
      margin-block-start: var(--space-2);
      max-inline-size: none;
    }
  }

  .bb-ask-party {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  .bb-ask-for {
    font-weight: 700;
  }

  /* Their reason, in their words: the display face, as something said (the
     organisers' reply on /sleeping wears it, D-108b), in the body colour. */
  .bb-ask-why {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
    color: var(--color-text);
  }

  /* The driveway --------------------------------------------------- */

  .bb-o-lane,
  .bb-o-add {
    gap: var(--space-3);
  }

  .bb-o-add > .bb-driveway-add {
    max-inline-size: none;
  }

  /* The lane inside its card: no box of its own, the spaces a step apart,
     counted from the road. */
  .bb-o-lane > .bb-lane {
    gap: var(--space-3);
    padding: 0;
    border: 0;
    background: none;
    counter-reset: place;
  }

  /* One space: its place from the road in a pine disc before its name (the
     heading says which end is the road; the number says how far in), the
     name in the display face, its form under them, on the sand. */
  .bb-o-lane .bb-space {
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface-2);
    counter-increment: place;
  }

  .bb-o-lane .bb-space-name {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;

    &::before {
      content: counter(place);
      display: grid;
      flex: none;
      place-items: center;
      inline-size: 1.75rem;
      block-size: 1.75rem;
      border-radius: 50%;
      background: var(--color-pine);
      color: var(--color-on-accent);
      font-family: var(--font-body);
      font-size: var(--text-sm);
      font-weight: 700;
      line-height: 1;
    }
  }

  .bb-o-lane .bb-space-edit {
    max-inline-size: none;
  }

  .bb-o-lane > .bb-lane-turn {
    max-inline-size: none;
  }

  /* The recipe box ------------------------------------------------- */

  .bb-recipe-box {
    gap: var(--space-3);

    & > .bb-recipe-entry {
      max-inline-size: none;
    }
  }

  /* Taken out of the list: a card of the harvest's; each removed recipe a
     white card inside it, and its way back an outlined button in sage (a
     thing put right, the colour a saved answer wears), not the cranberry
     the recipe page's take-out button wears. */
  .bb-recipes-out {
    gap: var(--space-3);

    & > .bb-recipes {
      margin-block-start: 0;
    }

    & .bb-recipe-remove {
      margin-block-start: var(--space-2);
      padding-block-start: var(--space-3);
    }

    & .bb-recipe-remove .bb-submit {
      border-color: var(--color-sage);
      color: var(--color-sage);

      &:hover,
      &:active {
        background: var(--color-sage);
        color: var(--color-on-accent);
      }
    }
  }
}

/* ------------------------------------------------------------------ *
 * D-115, the RSVP in one pass and the beds by night (2026-10-12). The
 * owner: "we need to streamline the beds and rsvp so i can start getting
 * users online". Where to sleep is one list with a private reason box, on
 * the RSVP and on /sleeping; the RSVP asks about a reserved parking spot;
 * /parking says whether the household asked; the organisers' driveway
 * lists who asked and why; the beds board is one section per night.
 *
 * Every colour is a token the dark theme already re-points, and every
 * control is at the floor. The new questions wear the RSVP's card (D-107)
 * and the board's rows wear D-111e's.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The private reason box under a question: a step below the last choice,
     shorter than a post box because a reason is a sentence or two. */
  .bb-why {
    margin-block-start: var(--space-3);

    & > .bb-textarea {
      min-block-size: 5rem;
    }
  }

  /* /parking's reminder and the organisers' list of who asked: cards whose
     heading, words and control sit a step apart, as the cars card's do. */
  .bb-park-ask,
  .bb-park-asks {
    gap: var(--space-3);
  }

  /* "Not coming" or "Not sure yet" beside a household on the beds board: a
     pill on the surface, the way the people board tags a person (D-111b). */
  .bb-ask-standing {
    justify-self: start;
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--color-line-strong);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
  }

  /* NIGHT BY NIGHT. Each night is a disclosure on the sand, its summary the
     night in the display face and how it stands, at the floor; open, every
     bed that night is a row on the surface with the pine down its side
     (dashed line colour when nobody is in it, pumpkin when it holds more than
     the organiser wrote down), and who still needs a bed is under them. */
  .bb-beds-nights-card,
  .bb-beds-people {
    gap: var(--space-3);
  }

  .bb-beds-night,
  .bb-beds-give {
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  .bb-beds-night-head,
  .bb-beds-give-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-1) var(--space-3);
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }
  }

  .bb-beds-night-when {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    line-height: var(--leading-tight);
  }

  .bb-beds-night-status,
  .bb-beds-give-needs {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  .bb-night-beds {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0 var(--space-3) var(--space-3);
    list-style: none;
  }

  .bb-night-bed {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-inline-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    &.bb-night-empty {
      border-inline-start-style: dashed;
      border-inline-start-color: var(--color-line-strong);
    }

    &.bb-night-over {
      border-inline-start-color: var(--color-pumpkin);
    }
  }

  .bb-night-bed-name {
    font-weight: 700;
  }

  .bb-night-bed-kept {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  .bb-night-bed-who {
    grid-column: 1 / -1;
  }

  .bb-night-empty > .bb-night-bed-who {
    color: var(--color-text-muted);
  }

  /* More people than the organiser wrote down: said in words, bold, and the
     row's edge turns the alarm's pumpkin. A warning only (D-115): nothing is
     refused. */
  .bb-night-bed-over {
    grid-column: 1 / -1;
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--color-text);
  }

  .bb-night-waiting {
    margin: 0;
    padding: 0 var(--space-4) var(--space-3);
  }

  .bb-night-waiting-head,
  .bb-beds-give-name {
    font-weight: 700;
  }

  /* Who still needs a bed, one disclosure each: the person's own form, the
     bed and the nights, under their name. */
  .bb-beds-people {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-beds-person {
    display: block;
  }

  .bb-beds-give > .bb-beds-place {
    max-inline-size: none;
    padding: 0 var(--space-3) var(--space-3);
  }

  .bb-beds-anyone {
    margin-block-start: var(--space-2);
  }

  /* The nights as tick tiles in a wrapping row, under their title. */
  .bb-beds-nights {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
    min-inline-size: 0;
    margin: 0;
    padding: 0;
    border: 0;

    & > legend {
      font-weight: 700;
    }
  }

  /* "Change nights" on a placed row: the house's outlined control, as a
     post's own "Change" is (D-91), opening onto the tick boxes. */
  .bb-beds-change {
    margin-block-start: var(--space-2);

    & > .bb-beds-renight {
      max-inline-size: none;
      margin-block-start: var(--space-2);
    }
  }

  .bb-beds-change-head {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }
  }
}

/* ------------------------------------------------------------------
 * D-116, the RSVP and beds, part 2 (2026-10-12). The owner: "i want the
 * full list on rsvp and sleeping shows only the assignments. requesting is
 * rsvp only." /sleeping draws the household's own beds night by night; the
 * RSVP says when the room asked for has been kept back; the beds board
 * says which beds are free each night; the dashboard says what the
 * household answered.
 *
 * Every colour is a token the dark theme already re-points.
 * ------------------------------------------------------------------ */

@layer pages {

  /* The household's own beds: one block per run of nights, the nights in
     the display face, then each person and their bed on a row of the
     surface with the pine down its side (dashed when not placed yet). */
  .bb-own-beds {
    gap: var(--space-3);
  }

  .bb-own-nights,
  .bb-own-people {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-own-nights {
    gap: var(--space-4);
  }

  .bb-own-night {
    display: grid;
    gap: var(--space-2);
  }

  .bb-own-when {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  .bb-own-person {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-inline-start: 0.3rem solid var(--color-pine);
    border-radius: var(--radius-sm);
    background: var(--color-surface);

    &.bb-own-waiting {
      border-inline-start-style: dashed;
      border-inline-start-color: var(--color-line-strong);
    }
  }

  .bb-own-name {
    font-weight: 700;
    overflow-wrap: anywhere;
  }

  .bb-own-bed {
    overflow-wrap: anywhere;
  }

  .bb-own-waiting > .bb-own-bed {
    color: var(--color-text-muted);
  }

  /* The RSVP's sentence when the room asked for was kept back since: bold,
     on the warm wash, so it is read before the list under it. */
  .bb-sleep-gone {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-inline-start: 0.3rem solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    font-weight: 700;
  }

  /* Which beds are free on a night, under that night's beds. */
  .bb-night-free {
    margin: 0;
    padding: 0 var(--space-4) var(--space-2);
  }

  .bb-night-free-head {
    font-weight: 700;
  }

  /* The reserved beds nobody is in that night, on a line of their own under
     the free ones and not counted with them (D-117). Drawn as the free line
     is; its own bold head is what says it is a different list. */
  .bb-night-reserved {
    margin: 0;
    padding: 0 var(--space-4) var(--space-2);
  }

  .bb-night-reserved-head {
    font-weight: 700;
  }

  /* What the household answered, under "Who is coming". */
  .bb-own-answer {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);

    & > .bb-own-answer-line {
      margin: 0;
    }
  }
}

/* ------------------------------------------------------------------
 * D-119: the organiser driveway board's "Move a car" card, which points to
 * /parking, where an organiser's form lists every car. A card of the cabin's
 * whose heading, words and control sit a step apart, as the board's lane and
 * add cards do (`bb-o-lane`, `bb-o-add`). Only /o/parking draws `bb-o-move`.
 * ------------------------------------------------------------------ */

@layer pages {
  .bb-o-move {
    gap: var(--space-3);
  }
}

/* ------------------------------------------------------------------ *
 * D-120, the cabin fund (the owner, 2026-10-12): the page at /pot, which
 * shows the family's pledged total and the reader's own household's
 * pledge; the same pledge question on the RSVP; and the organisers'
 * board at /o/pledges, which lists each household's pledge.
 *
 * No goal, no target, no progress bar: nothing here draws a bar or a
 * fraction, and the total is a number on its own. Every colour is a token
 * the dark theme already re-points; there is no new token.
 * ------------------------------------------------------------------ */

@layer pages {

  /* Either card on these pages: a little more room between its rows than a
     dashboard panel, because the total wants space around it. */
  .bb-fund {
    gap: var(--space-3);
  }

  /* The total: the one number the page is for, in the display face. Body
     text colour, not the accent, so its contrast is the body's in both
     themes. It wraps rather than overflowing a 320px phone. */
  .bb-fund-total {
    margin: 0;
    font-family: var(--font-display);
    font-size: 2rem;
    line-height: 1.1;
    color: var(--color-text);
    overflow-wrap: anywhere;
  }

  /* The household's own amount, inside its sentence. */
  .bb-fund-own {
    font-weight: 700;
    color: var(--color-text);
  }

  .bb-pledge-form {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
    margin: 0;
  }

  /* "$" and the box on one line: the box takes the rest of the line up to
     a width that still reads as a money box rather than a sentence. */
  .bb-pledge-line {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-inline-size: 0;
  }

  .bb-pledge-dollar {
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--color-text);
  }

  .bb-pledge-amount {
    flex: 1 1 auto;
    min-inline-size: 0;
    max-inline-size: 14rem;
    font-variant-numeric: tabular-nums;
  }

  .bb-pledge-acts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
  }

  /* "Take our pledge back": the secondary shape the people board's buttons
     take (outlined, no accent fill), at the tap floor. */
  .bb-pledge-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    font-weight: 700;
    line-height: var(--leading-tight);
    cursor: pointer;

    &:hover {
      background: var(--color-surface-2);
    }
  }

  /* The question on the RSVP: a card like the questions above it (D-107);
     `.bb-rsvp > fieldset` draws the card and D-114 its title. */
  .bb-rsvp-pledge {
    display: grid;
    gap: var(--space-2);
    min-inline-size: 0;
  }

  /* The organisers' list: one row per household, its name at the start and
     its amount at the end, the amounts lined up as figures. */
  .bb-pledge-list {
    display: grid;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-pledge-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-1) var(--space-3);
    min-inline-size: 0;
    padding-block: var(--space-2);
    border-block-end: 1px solid var(--color-line);

    &:last-child {
      border-block-end: 0;
    }
  }

  .bb-pledge-who {
    min-inline-size: 0;
    overflow-wrap: anywhere;
  }

  .bb-pledge-sum {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
}

/* ------------------------------------------------------------------
 * D-122, pets and the organisers' bed editor (2026-10-13). A household
 * member can be a pet, ticked on the RSVP's own row; the beds board edits
 * the list of beds. Every colour is a token the dark theme already
 * re-points.
 * ------------------------------------------------------------------ */
@layer pages {
  /* The kind of animal shows once its row's pet tick is ticked, with no
     JavaScript; a browser without :has shows it always. */
  .bb-party-member:not(:has(input[name^="member_pet_"]:checked)) > .bb-pet-kind {
    display: none;
  }

  .bb-pet-kind {
    display: grid;
    gap: var(--space-1);
  }

  /* One bed's "Change or remove", under its row, the whole width of the
     card. In the text column beside the bed mark it was about 205px wide at
     320px (measured on the render): the kinds of place and "Offered on the
     RSVP" were cut off in their boxes and the shared tick wrapped to four
     lines. */
  .bb-beds-list .bb-sleep-spot > .bb-bed-edit {
    grid-column: 1 / -1;
    min-inline-size: 0;
  }

  .bb-bed-form {
    display: grid;
    gap: var(--space-3);
    min-inline-size: 0;

    /* The boxes sit inside their bold `.bb-field` word, so without this what
       an organiser typed read bold too, unlike every RSVP box (measured on
       the 412px render). The word stays bold; the answer is body weight. */
    & .bb-input {
      font-weight: 400;
    }
  }

  .bb-bed-edit > .bb-people-form {
    margin-block-start: var(--space-2);
  }

  /* Who a bed holds and who asked for it, said before anybody presses
     "Remove this bed". */
  .bb-bed-touch {
    margin: var(--space-2) 0 0;
    color: var(--color-text);
    overflow-wrap: anywhere;
  }

  .bb-bed-touch-head {
    font-weight: 700;
  }

  .bb-beds-add {
    margin-block-start: var(--space-3);
  }
}

/* D-126: the organisers' "Food to bring". The list of meals is the index's
   own rows; each meal's check sheet is one form of rows, each row a tick, a
   name, an amount and its word, with the recipe line it came from under it.
   A row is two lines: the tick tile and the name box, then the amount and its
   word, so on a phone no box is squeezed under the tap floor. */
@layer pages {
  .bb-o-bring-meals {
    display: grid;
    gap: var(--space-3);
    margin: var(--space-3) 0 0;
    padding: 0;
    list-style: none;
  }

  .bb-o-bring-meal {
    display: grid;
    gap: var(--space-1);
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--color-line);
  }

  .bb-o-bring-name {
    margin: 0;
    font-weight: 700;
  }

  .bb-sheet-back {
    margin-block-end: var(--space-3);
  }

  .bb-sheet-notes {
    margin: 0;
    padding-inline-start: var(--space-4);
    color: var(--color-text-muted);
    max-inline-size: var(--measure);
  }

  .bb-sheet-form {
    display: grid;
    gap: var(--space-3);
  }

  .bb-sheet-rows {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-sheet-row {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 2px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    min-inline-size: 0;
  }

  /* The panels and boxes may shrink inside a grid: without it a long name
     held a box open past a 320px screen. */
  .bb-o-bring,
  .bb-sheet,
  .bb-sheet-name,
  .bb-sheet-unit {
    min-inline-size: 0;
  }

  /* The tick tile and the name side by side, the name taking what is left.
     A line somebody is bringing has no tick, and its name has the line. */
  .bb-sheet-line {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: stretch;
    gap: var(--space-2);
  }

  .bb-sheet-line-held {
    grid-template-columns: minmax(0, 1fr);
  }

  .bb-sheet-line > .bb-check {
    align-items: center;
    padding: var(--space-2) var(--space-3);
  }

  /* The amount and its word: a short box for the count, the word beside it. */
  .bb-sheet-amount {
    display: grid;
    grid-template-columns: 5rem minmax(0, 1fr);
    align-items: center;
    gap: var(--space-2);
  }

  .bb-sheet-count {
    text-align: center;
  }

  .bb-sheet-unit-fixed {
    font-weight: 700;
  }

  .bb-sheet-held,
  .bb-sheet-check,
  .bb-sheet-from {
    margin: 0;
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
  }

  .bb-sheet-from {
    color: var(--color-text-muted);
  }

  .bb-sheet-check {
    font-weight: 700;
    color: var(--color-cranberry);
  }

  .bb-sheet-held {
    color: var(--color-text);
  }

  /* "Add a line" and "Add this recipe": outlined, full width, under the
     filled button's weight, at the tap floor. */
  .bb-sheet-more {
    inline-size: 100%;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;

    &:hover {
      background: var(--color-surface-2);
    }
  }

  .bb-sheet-recipe {
    display: grid;
    gap: var(--space-2);
  }

  /* D-126 and D-144 on /bring: a meal's line says which meals it is for and
     how much each needs ("2 cups for Wednesday dinner, 1 cup for Friday
     dinner"), in the quiet line under the title. */
  .bb-bring-amount {
    font-weight: 700;
  }

  /* D-145: on a meal's line the reader can bring less of than it still
     needs, why ("You arrive Friday, so you can bring the part for Friday to
     Sunday: up to 24."), under the meals it is for, on the row and in its
     card. The text colour and not the muted one, because it is the answer
     to "why can I not press this". */
  .bb-bring-reach {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  /* D-146: on the reader's own share an organizer put down, who did ("Cobb
     Hayes put you down for this."), in the same place and size as D-145's
     sentence, on the row and in its card. */
  .bb-bring-put-down {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.3;
  }
}

/* D-127, photos slice P1: a person's picture beside their name, in the four
   places the owner chose (Your Info, Who is coming, the feed's lines and the
   organisers' people board). A round picture the height of a line of text,
   sitting on the line with the name; without a picture the name is drawn
   alone, so nothing here draws a placeholder. */
@layer components {
  .bb-face {
    display: inline-block;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    margin-inline-end: var(--space-2);
    border-radius: 50%;
    border: var(--border);
    background: var(--color-surface-2);
    object-fit: cover;
    vertical-align: middle;
  }

  /* Your Info: the reader's own picture, big enough to see it is theirs. */
  .bb-face.bb-face-large {
    display: block;
    inline-size: 6rem;
    block-size: 6rem;
    margin-block-end: var(--space-3);
  }

  /* Who is coming: the people not heard from yet, each a picture and a name,
     wrapping like words so a long list stays a few lines on a phone. */
  .bb-named-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin: 0;
    margin-block-start: var(--space-1);
    padding: 0;
    list-style: none;
  }

  .bb-named {
    display: inline-flex;
    align-items: center;
  }

  .bb-named-name {
    color: var(--color-text);
  }

  /* The picture's form on Your Info. The file box is a native control and is
     left as the phone draws it; this only gives it the box's width and room. */
  .bb-picture-form {
    display: grid;
    gap: var(--space-2);
    margin-block-end: var(--space-3);
  }

  .bb-file {
    inline-size: 100%;
    min-block-size: var(--tap-min);
  }

  /* The people board lines a row's name and tags up on their baseline,
     which would sit a picture on it like a letter and leave it above the
     name (LOOKED AT, 412 wide): the picture is centred on the line instead,
     and the row's own gap spaces it. */
  .bb-person-head > .bb-face {
    align-self: center;
    margin-inline-end: 0;
  }

  /* The people board's take-down sits under a row's own buttons. */
  .bb-people-picture {
    margin-block-start: var(--space-2);
  }
}

/* D-128, photos slice P2: the trip album. This year's photos as a grid of
   squares under the add form, each opening its own page with the photo large,
   its caption under it and the steps to the next one; and on the dashboard a
   photo line's three small squares. */
@layer components {
  /* Three across a phone, more as the column widens. Every square is the same
     size whatever shape the photo is: the grid is for finding one, the
     photo's page for looking at it. */
  .bb-album-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  @media (min-width: 48rem) {
    .bb-album-grid {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
  }

  .bb-album-cell {
    margin: 0;
  }

  .bb-album-thumb {
    display: block;
    min-block-size: var(--tap-min);
    border: var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface-2);

    & > img {
      display: block;
      inline-size: 100%;
      block-size: auto;
      aspect-ratio: 1;
      object-fit: cover;
    }
  }

  /* The photo's page: the photo the column's width, never taller than most
     of the screen, so a tall photo still leaves its caption in view. */
  .bb-album-photo {
    margin: 0 0 var(--space-3);
  }

  .bb-album-large {
    display: block;
    inline-size: 100%;
    block-size: auto;
    max-block-size: 75vh;
    object-fit: contain;
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  .bb-album-caption {
    margin-block-start: var(--space-2);
    font-size: var(--text-lg);
    color: var(--color-text);
  }

  .bb-album-by {
    margin-block: 0 var(--space-3);
    color: var(--color-text-muted);
  }

  /* Newer, all photos, older: three buttons on one row, the middle one always
     there, so the two steps stay where a thumb expects them. */
  .bb-album-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-end: var(--space-3);
  }

  .bb-album-step {
    display: inline-flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: 700;
    text-decoration: none;

    &:hover {
      background: var(--color-surface-2);
      text-decoration: none;
    }
  }

  .bb-album-own,
  .bb-album-organiser {
    margin-block-start: var(--space-4);
  }

  /* The dashboard: a photo line's squares under its words, each a button at
     the tap floor that opens the photo. */
  .bb-did-photos {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-2);
  }

  .bb-did-photo {
    display: block;
    inline-size: var(--tap-large);
    block-size: var(--tap-large);
    min-block-size: var(--tap-min);
    border: var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface-2);

    & > img {
      display: block;
      inline-size: 100%;
      block-size: 100%;
      object-fit: cover;
    }
  }
}

/* D-129, a relative's own post: "What's on your mind?" at the top of the
   feed, the photos a post came with, its hearts and the replies under it. */
@layer pages {

  /* The box: the words, the file box and "Post it" stacked, the photo
     form's shape (`.bb-picture-form`), with a rule under it so the list it
     adds to starts below. */
  .bb-compose {
    display: grid;
    gap: var(--space-2);
    margin-block-end: var(--space-4);
    padding-block-end: var(--space-4);
    border-block-end: 1px dotted var(--color-line-strong);
  }

  /* A post's photos: squares three across, each opening its page in the
     album, never under the tap floor. */
  .bb-post-photos {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
  }

  .bb-post-photo {
    display: block;
    min-block-size: var(--tap-min);
    border: var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface-2);

    & > img {
      display: block;
      inline-size: 100%;
      block-size: auto;
      aspect-ratio: 1;
      object-fit: cover;
    }
  }

  /* The organisers' board: a post's photos one to a row, each small with
     its take-down beside it (three across left the button's words four
     lines tall at 412, LOOKED AT). */
  .bb-board-photos {
    display: grid;
    gap: var(--space-3);
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
  }

  .bb-post-photo-own {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);

    & > .bb-post-photo {
      flex: none;
      inline-size: 5.5rem;
    }
  }

  /* The heart (D-132, the owner's "think facebook thumbs up button"): a
     toggle showing the drawing and how many, no words. The reader's own
     heart is the cranberry, filled, with the cranberry count; otherwise an
     outline in the muted text colour. The tap box is at least the floor
     both ways, so a heart with no count is still a square to press; on the
     feed the drawn pill is smaller inside it (section 4 of D-130's block). Who
     gave hearts is the names line above the button, in the muted text. */
  .bb-heart-form {
    margin: 0;
  }

  .bb-heart {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-block-size: var(--tap-min);
    min-inline-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    line-height: var(--leading-tight);
    cursor: pointer;

    &:hover {
      background: var(--color-surface-2);
    }

    &[aria-pressed="true"] {
      border-color: var(--color-cranberry);
    }
  }

  .bb-heart-mark {
    display: inline-flex;
    color: var(--color-text-muted);

    & > svg {
      inline-size: 1.25rem;
      block-size: 1.25rem;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.4;
      stroke-linejoin: round;
    }

    &.bb-heart-given {
      color: var(--color-cranberry);
    }

    &.bb-heart-given > svg {
      fill: currentColor;
    }
  }

  .bb-heart-count {
    font-variant-numeric: tabular-nums;
  }

  .bb-heart[aria-pressed="true"] > .bb-heart-count {
    color: var(--color-cranberry);
  }

  /* Who gave hearts: a small filled heart and the names, nothing else; the
     count is on the button. Still `--text-sm`, which is the floor (D-130),
     so it is quieter by having fewer words and no count, not by smaller
     type. The words "Hearts from" are for a screen reader alone, which does
     not see the drawing: clipped to a point, as `.bb-nav-state` is. */
  .bb-hearts-who {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);

    & > .bb-heart-mark {
      vertical-align: -0.125em;
      margin-inline-end: var(--space-1);

      & > svg {
        inline-size: 1em;
        block-size: 1em;
      }
    }
  }

  .bb-hearts-spoken {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The replies: a column set in from the post's start under a rule, each
     reply its author's name and picture over its words. */
  .bb-replies {
    margin: var(--space-3) 0 0;
    padding: 0 0 0 var(--space-3);
    border-inline-start: 2px solid var(--color-line);
    list-style: none;
  }

  .bb-reply {
    padding-block: var(--space-2);

    & + .bb-reply {
      border-block-start: 1px dotted var(--color-line-strong);
    }
  }

  .bb-reply-author {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-weight: 700;
  }

  .bb-reply-body {
    margin: var(--space-1) 0 0;
  }

  .bb-reply-new {
    margin-block-start: var(--space-3);
  }

  .bb-reply-new > .bb-post-form,
  .bb-reply-new > .bb-form-error {
    margin-block-start: var(--space-2);
  }

  .bb-reply-new > .bb-post-form {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--tint-sand);
  }
}

/* D-130, the Home feed redesign: the owner, 2026-10-23, "bigbear feed needs
   a redesign now, its crowded and pinned comments take up the entire screen.
   smaller font maybe." Folded from the design pass of the same day
   (direction B, "tidy + one size smaller"; the size step itself is the base
   layer's root, above).

   SCOPE: every rule that touches a shared hook is under `.bb-feed-panel`, the
   class Home's feed section has in place of `bb-panel bb-cabin`; the other
   rules style classes only Home emits. Several hooks drawn here are shared
   with other pages - `.bb-post-control`, `.bb-post-form`, `.bb-post-remove`,
   `.bb-reply`, `.bb-replies`, `.bb-post-author`, `.bb-post-kind` and the
   `.bb-feed > .bb-post` card are also emitted by crates/web/src/organiser/
   feed.rs (/o/say), and the first three by other pages - so an unscoped rule
   would restyle those pages too.

   Every colour is one of the site's own tokens, so the dark theme follows by
   itself: the dark block re-points the same custom properties.

   Last in the pages layer, so at equal specificity these win over the
   dashboard's and D-129's rules above, as they did over the served
   stylesheet when the design pass measured them. */
@layer pages {


  /* 1. The feed is a section on the page, not a card holding cards.
     Today the posts are cards inside the "What is happening" card inside the
     page's padding: three insets, and a post's words get 296 of 412 px. The
     section keeps its id (bb-feed), its heading and its mark; the posts and
     the activity card become the cards. */
  .bb-feed-panel {
    display: grid;
    gap: var(--space-3);
    min-inline-size: 0;
    margin-block-end: var(--space-5);
  }
  .bb-main .bb-feed-panel > .bb-panel-heading {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 400;
    line-height: var(--leading-tight);
  }

  /* 2. "What's on your mind?" starts as one line that opens. A <details>,
     so it opens with no script. Drawn open when a post was refused (the
     reason and the typed words are inside). */
  .bb-compose-fold {
    min-inline-size: 0;
  }
  .bb-compose-open {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-block-size: var(--tap-large);
    padding-block: var(--space-1);
    padding-inline: var(--space-4) var(--space-2);
    border: 2px solid var(--color-line-strong);
    border-radius: 2rem;
    background: var(--color-surface);
    color: var(--color-text-muted);
    list-style: none;
    cursor: pointer;
    &::-webkit-details-marker {
      display: none;
    }
    &:hover {
      background: var(--color-surface-2);
    }
  }
  .bb-compose-ask {
    min-inline-size: 0;
  }
  /* The pill at the end of the line says what pressing it does, in words. */
  .bb-compose-go,
  .bb-compose-close {
    flex: none;
    padding: var(--space-2) var(--space-4);
    border-radius: 2rem;
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: var(--leading-tight);
  }
  .bb-compose-go {
    background: var(--color-accent);
    color: var(--color-on-accent);
  }
  .bb-compose-close {
    display: none;
    border: 2px solid var(--color-line-strong);
    color: var(--color-text);
  }
  .bb-compose-fold[open] > .bb-compose-open {
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    font-family: var(--font-display);
    font-size: var(--text-lg);
    & > .bb-compose-go {
      display: none;
    }
    & > .bb-compose-close {
      display: inline-block;
      font-family: var(--font-body);
    }
  }
  /* The form inside: no rule under it any more (the fold is the edge), a
     smaller first box, and the note in the floor size. */
  .bb-compose-fold > .bb-compose {
    margin: 0;
    padding: var(--space-3) var(--space-1) var(--space-2);
    border: 0;
  }
  .bb-compose .bb-textarea {
    min-block-size: 6rem;
  }
  .bb-compose > .bb-note {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
  }

  /* 3. A post card: tighter, the name beside the face, the bar at the
     bottom. The pine edge (a relative's post) and the harvest wash with the
     pumpkin edge (an announcement) are kept from D-106. */
  .bb-feed-panel .bb-feed > .bb-post {
    padding: var(--space-3) var(--space-4) 0;
  }
  .bb-post-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    & > .bb-face {
      flex: none;
      inline-size: 2.25rem;
      block-size: 2.25rem;
      margin: 0;
    }
  }
  /* The name takes what the face and "Change" leave, down to 6rem, so on a
     360 phone "Change" still sits on the name's line. */
  .bb-post-who {
    flex: 1 1 0;
    min-inline-size: 6rem;
  }
  .bb-feed-panel .bb-post-author {
    font-family: var(--font-display);
    font-size: var(--text-base);
    font-weight: 700;
    line-height: var(--leading-tight);
  }
  /* "Announcement" stays a WORD (a class is invisible to a screen reader),
     under the name now rather than above it, in the floor size and sentence
     case. */
  .bb-feed-panel .bb-post-kind {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    line-height: var(--leading-tight);
    color: var(--color-text-muted);
  }
  .bb-post-text {
    margin-block-start: var(--space-2);
    & > .bb-post-body + .bb-post-body {
      margin-block-start: var(--space-2);
    }
  }
  /* A long post shows its first five lines and "Read the whole post".
     The server draws the <details> only on a long post; the clamp is CSS on
     the words before it, so with no script and no :has() the whole post
     simply shows. */
  /* A mask reads only the gradient's alpha, so its colour is never drawn:
     it is a token so the rule that every colour is one holds here without
     an exception. */
  .bb-post:has(> .bb-post-long:not([open])) > .bb-post-text {
    max-block-size: calc(5 * var(--leading-body) * 1em);
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, var(--color-text) 55%, transparent);
    mask-image: linear-gradient(to bottom, var(--color-text) 55%, transparent);
  }
  .bb-post-long-less {
    display: none;
  }
  .bb-post-long[open] {
    & .bb-post-long-more {
      display: none;
    }
    & .bb-post-long-less {
      display: inline;
    }
  }

  /* 4. The controls. Each keeps the 3rem tap floor (54 px on a phone) as
     its pressable box; what is DRAWN is a smaller pill inside it, so a row
     of them reads as quiet and still takes a thumb. The pill is a
     pseudo-element, so the markup is the buttons and summaries of today. */
  .bb-feed-panel :is(.bb-post-bar, .bb-post-head, .bb-reply, .bb-post-long, .bb-replies-earlier) :is(.bb-heart, .bb-post-control) {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-block-size: var(--tap-min);
    padding: 0 var(--space-3);
    border: 0;
    background: none;
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: var(--leading-tight);
    &::before {
      content: "";
      position: absolute;
      inset: 0.5rem 0;
      z-index: -1;
      border: 2px solid var(--color-line-strong);
      border-radius: 2rem;
      background: var(--color-surface);
    }
    &:hover::before {
      background: var(--color-surface-2);
    }
  }
  .bb-feed-panel details[open] > .bb-post-control {
    background: none;
    &::before {
      background: var(--color-surface-2);
    }
  }
  .bb-feed-panel .bb-heart[aria-pressed="true"]::before {
    border-color: var(--color-cranberry);
    background: var(--tint-cranberry);
  }
  /* The bar: Heart and Reply on one line under a dotted rule. A control
     that is opened takes the whole width under it (D-91's rule for the old
     pair), so its box is never squeezed. */
  .bb-post-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--space-2);
    margin-block-start: var(--space-2);
    padding-block: var(--space-1);
    border-block-start: 1px dotted var(--color-line-strong);
    & > details[open] {
      flex-basis: 100%;
      padding-block-end: var(--space-2);
    }
    & .bb-heart-form,
    & .bb-reply-new {
      margin: 0;
    }
  }
  /* D-140: "Pin" and "Unpin", an organiser's control at the end of an
     announcement's bar, after the heart and "Reply". The feed's pill (drawn
     smaller inside the 3rem tap box, as section 4 draws the others) with a
     DASHED pumpkin edge and a pushpin in pumpkin, so it reads as an
     organiser's tool. Its words are always there. Only an organiser's page
     draws it, and only Home draws these classes, so they need no
     `.bb-feed-panel` scope (D-130d). */
  .bb-pin-form {
    margin: 0;
    margin-inline-start: auto;
  }
  .bb-pin-button {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: var(--tap-min);
    padding: 0 var(--space-3);
    border: 0;
    background: none;
    color: var(--color-text);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: var(--leading-tight);
    cursor: pointer;
    &::before {
      content: "";
      position: absolute;
      inset: 0.5rem 0;
      z-index: -1;
      border: 2px dashed var(--color-pumpkin);
      border-radius: 2rem;
      background: var(--color-surface);
    }
    &:hover::before {
      background: var(--color-surface-2);
    }
  }
  .bb-pin-mark {
    display: inline-flex;
    & > svg {
      inline-size: 1rem;
      block-size: 1rem;
      fill: var(--color-pumpkin);
    }
  }
  /* Who gave hearts: above the bar (D-130), names only since D-132. */
  .bb-feed-panel .bb-hearts-who {
    margin-block-start: var(--space-2);
  }
  /* The author's own "Change": at the end of the name line, one control in
     place of today's two. Opened, it takes the line's whole width and holds
     the change box, Save, and under it "Remove this post" with today's one
     plain-words confirmation. */
  .bb-post-mine {
    flex: none;
    margin-inline-start: auto;
    &[open] {
      flex-basis: 100%;
      margin-inline-start: 0;
    }
    & > .bb-post-form,
    & > .bb-form-error,
    & > .bb-post-remove {
      margin-block-start: var(--space-2);
    }
  }
  .bb-feed-panel :is(.bb-post-mine > .bb-post-form, .bb-post-mine .bb-post-remove > .bb-post-form, .bb-reply-new > .bb-post-form) {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--tint-sand);
  }

  /* 5. Replies: soft bubbles under the bar, the newest two shown, the
     earlier ones behind "Show N earlier replies" (a <details>). */
  .bb-feed-panel .bb-replies {
    display: grid;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
    padding: 0;
    border: 0;
  }
  .bb-replies-more {
    display: grid;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
  }
  .bb-feed-panel .bb-reply {
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--tint-sand);
    font-size: var(--text-sm);
    & + .bb-reply {
      border: 0;
    }
    & .bb-face {
      inline-size: 1.5rem;
      block-size: 1.5rem;
      margin: 0;
    }
    & > .bb-post-mine {
      margin-block-start: var(--space-1);
      margin-inline-start: 0;
    }
  }
  .bb-feed-panel .bb-reply-author {
    font-size: var(--text-sm);
  }
  .bb-feed-panel .bb-reply-body {
    margin-block-start: var(--space-1);
  }

  /* 6. What relatives did: one soft card per run of lines, in the floor
     size, the pine dot kept. The lines are the same <li>s in the same order;
     only the drawing joins neighbours, so the feed's one order (core's
     Placed) is untouched. */
  .bb-feed-panel .bb-feed > .bb-did {
    padding: var(--space-2) var(--space-3) var(--space-2) calc(var(--space-3) + 1rem);
    border: 1px solid var(--color-line);
    border-block-width: 0;
    background: var(--color-surface);
    font-size: var(--text-sm);
    line-height: 1.4;
    &::before {
      inset-inline-start: var(--space-3);
      inset-block-start: calc(var(--space-2) + 0.7em - 0.25rem);
    }
    & .bb-face {
      inline-size: 1.5rem;
      block-size: 1.5rem;
    }
  }
  .bb-feed-panel .bb-feed > .bb-did + .bb-did {
    margin-block-start: calc(-1 * var(--space-3));
    border-block-start: 1px dotted var(--color-line-strong);
  }
  .bb-feed-panel .bb-feed > :is(:not(.bb-did) + .bb-did, .bb-did:first-child) {
    border-block-start-width: 1px;
    border-start-start-radius: var(--radius-sm);
    border-start-end-radius: var(--radius-sm);
    padding-block-start: var(--space-3);
    &::before {
      inset-block-start: calc(var(--space-3) + 0.7em - 0.25rem);
    }
  }
  .bb-feed-panel .bb-feed > :is(.bb-did:has(+ :not(.bb-did)), .bb-did:last-child) {
    border-block-end: 1px solid var(--color-line);
    border-end-start-radius: var(--radius-sm);
    border-end-end-radius: var(--radius-sm);
    padding-block-end: var(--space-3);
  }

  /* 7. The banner on Home, compact: the same title and the same growing
     cabin (D-121), less of the screen. Keyed on the page holding the feed
     and not only on a title row holding the scene: the door draws the
     scene in its title row too (MEASURED in the rendered door, 2026-10-23),
     and the owner asked for this on Home. */
  .bb-main:has(> .bb-feed-panel) {
    padding-block-start: var(--space-4);
  }
  .bb-main:has(> .bb-feed-panel) > .bb-title-row:has(> .bb-scene) > .bb-title {
    padding: var(--space-3) var(--space-4) 0;
  }
  .bb-main:has(> .bb-feed-panel) > .bb-title-row > .bb-scene {
    block-size: 6rem;
  }
}

/* D-131, photos slice P3: the pyjama contest. Who is ahead in words at the
   top, the entry form, then every entry as a card: the photo the column's
   width so the whole of it is what people vote on, whose pyjamas, what they
   are wearing, the count and who voted, and what the reader can do. The
   organisers' board lists the same entries with a take-down each and a
   set-aside for each vote. Every control is a plain form and at the tap
   floor. */
@layer pages {
  /* Who is ahead, or who won: the first card, its sentence the largest text
     on the page after the title. */
  .bb-pj-standing {
    display: grid;
    gap: var(--space-2);
  }

  .bb-pj-ahead {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--color-text);
  }

  /* Once it has closed, the winning photos under the sentence. */
  .bb-pj-winners {
    display: grid;
    gap: var(--space-3);
    margin: var(--space-3) 0 0;
    padding: 0;
    list-style: none;
  }

  .bb-pj-winner {
    margin: 0;
  }

  .bb-pj-photo-img {
    display: block;
    inline-size: 100%;
    block-size: auto;
    max-block-size: 70vh;
    object-fit: contain;
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  /* The entry form: whose, the photo, the words, stacked like the album's. */
  .bb-pj-enter-panel > .bb-pj-entered {
    margin-block: 0 var(--space-3);
    color: var(--color-sage);
    font-weight: 700;
  }

  .bb-pj-enter {
    margin-block-start: var(--space-2);
  }

  .bb-pj-whose {
    margin: 0;
    padding: 0;
    border: 0;
  }

  .bb-pj-whose-list {
    display: grid;
    gap: var(--space-2);
    margin: var(--space-1) 0 0;
    padding: 0;
    list-style: none;
  }

  .bb-pj-whose-one {
    margin: 0;
  }

  .bb-pj-problem {
    margin-block-end: var(--space-2);
  }

  /* The entries, one card each, one column on a phone and two from a
     tablet's width. */
  .bb-pj-entries {
    display: grid;
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  @media (min-width: 48rem) {
    .bb-pj-entries {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  .bb-pj-empty {
    margin: 0;
    color: var(--color-text-muted);
  }

  .bb-pj-entry {
    display: grid;
    gap: var(--space-1);
    align-content: start;
    margin: 0;
    padding: var(--space-3);
    border: var(--border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    scroll-margin-block-start: 6rem;
  }

  .bb-pj-photo {
    display: block;
    min-block-size: var(--tap-min);
    margin-block-end: var(--space-2);
  }

  .bb-pj-who {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--color-text);
  }

  .bb-pj-by,
  .bb-pj-yours {
    margin: 0;
    color: var(--color-text-muted);
  }

  .bb-pj-caption {
    margin: 0;
    color: var(--color-text);
    overflow-wrap: anywhere;
  }

  .bb-pj-count {
    margin: var(--space-1) 0 0;
    font-weight: 700;
    color: var(--color-text);
  }

  /* A vote set aside: said in words, with the reason, under the count. */
  .bb-pj-aside {
    margin: 0;
    padding-inline-start: var(--space-2);
    border-inline-start: 3px solid var(--color-pumpkin);
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
  }

  .bb-pj-tick,
  .bb-pj-unvote,
  .bb-pj-withdraw {
    margin-block-start: var(--space-2);
  }

  .bb-pj-tick > .bb-submit,
  .bb-pj-unvote > .bb-submit {
    inline-size: 100%;
  }

  /* Taking a vote back is the quieter of the two buttons: outlined, not
     filled, so the card's one filled button is always the one that votes. */
  .bb-pj-back {
    background: var(--color-surface);
    color: var(--color-text);
    border-color: var(--color-line-strong);

    &:hover {
      background: var(--color-surface-2);
      color: var(--color-text);
    }
  }

  /* The entries an organiser took down, each with who and why. */
  .bb-pj-taken-list,
  .bb-pj-past-list {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-pj-taken {
    margin: 0;
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
  }

  .bb-pj-taken-panel,
  .bb-pj-past,
  .bb-pj-board {
    margin-block-start: var(--space-4);
  }

  /* The organisers' board: one card per entry, a small square to know it by,
     its votes one to a line, each with its own set-aside. */
  .bb-o-pj-entry {
    display: grid;
    gap: var(--space-2);
  }

  .bb-o-pj-photo {
    display: block;
    inline-size: 8rem;
    block-size: 8rem;
    object-fit: cover;
    border: var(--border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
  }

  .bb-o-pj-votes {
    display: grid;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .bb-o-pj-vote {
    margin: 0;
    padding-block-start: var(--space-2);
    border-block-start: 1px dotted var(--color-line-strong);
  }

  .bb-o-pj-voter {
    margin: 0;
    font-weight: 700;
    color: var(--color-text);
  }
}

/* ------------------------------------------------------------------ */
/* D-138: "it worked" is a toast                                       */
/* ------------------------------------------------------------------ */

@layer components {

  /* The owner: "make it as a quick toast style popup", and then "Toast
     everywhere". `crate::toast::Toast` draws every one, and its module header
     says why each part is what it is. In short:

     - Fixed, so it takes no space on the page and nothing moves for it.
     - Lifted 6rem off the bottom edge, more than the sticky "Send it" and
       "Cast votes" bars are tall, so it never sits on them. MEASURED in
       Chrome at 360 and 412 on an 800px screen: "Send it" from 740 to 800,
       the toast's bottom edge at 698.
     - While it shows, a finger on it lands on IT and not on what it hides:
       MEASURED in Chrome at 412, the Thanksgiving ballot's "Put my name in to
       cook" sits under it when the page opens, and a tap that went through
       would press a button the reader cannot see. When it has gone it
       takes no tap at all: the fade ends with `pointer-events: none` as well
       as transparent (MEASURED the same way: a tap there then reaches the
       button).
     - Under the drawer and its veil (`z-index` 3 against their 5 and 4).
     - It goes away by itself with no script, and the going is an animation
       DELAY, not a long animation, on purpose: the base layer's
       reduced-motion rule squeezes every animation's duration to 0.01ms and
       leaves the delay alone, so for a reader who asked for less motion the
       toast is there, with no slide and no fade, and then gone, at the same
       moment as for everybody else. That is why `bb-toast-out` is named in
       `ONE_SHOT_ANIMATIONS` (`crate::layout::assets`); the slide in waits
       for `no-preference` like every other movement here.
     - It fades to transparent and stays in the page, so what a screen reader
       could read there before, it can read there still (NOT MEASURED with a
       screen reader).
     - How long it stays is a class, `bb-toast-for-<seconds>`, chosen by
       `crate::toast::seconds_for`: longer for a longer sentence. A class and
       not a style on the element, because a page carries no inline style. */
  .bb-toast {
    --bb-toast-for: 8s;
    position: fixed;
    inset-inline: var(--space-4);
    inset-block-end: calc(6rem + env(safe-area-inset-bottom, 0px));
    z-index: 3;
    inline-size: fit-content;
    max-inline-size: min(28rem, calc(100vw - 2 * var(--space-4)));
    margin: 0 auto;
    padding: var(--space-3) var(--space-4);
    border: 2px solid var(--color-sage);
    border-radius: var(--radius);
    background: var(--tint-sage);
    color: var(--color-text);
    font-size: var(--text-base);
    font-weight: 700;
    line-height: 1.4;
    box-shadow: 0 0.25rem 1rem var(--color-line-strong);
    animation: bb-toast-out 800ms ease-in var(--bb-toast-for) forwards;
  }

  .bb-toast-for-8 {
    --bb-toast-for: 8s;
  }

  .bb-toast-for-12 {
    --bb-toast-for: 12s;
  }

  .bb-toast-for-16 {
    --bb-toast-for: 16s;
  }

  .bb-toast-for-20 {
    --bb-toast-for: 20s;
  }

  .bb-toast-for-25 {
    --bb-toast-for: 25s;
  }

  .bb-toast-for-30 {
    --bb-toast-for: 30s;
  }

  .bb-toast-for-40 {
    --bb-toast-for: 40s;
  }

  @media (prefers-reduced-motion: no-preference) {
    .bb-toast {
      animation:
        bb-toast-in 240ms ease-out,
        bb-toast-out 800ms ease-in var(--bb-toast-for) forwards;
    }
  }

  @keyframes bb-toast-in {
    from {
      opacity: 0;
      translate: 0 0.75rem;
    }
  }

  @keyframes bb-toast-out {
    to {
      opacity: 0;
      pointer-events: none;
    }
  }
}

/* D-141, THE FEED KEEPS UP BY ITSELF: what the bundle adds to a row that
   arrives while Home is open. The owner, 2026-10-25: at the top of the feed
   new posts slide in by themselves. The slide is the row's block size going
   from nothing to its own height, with its padding, while it fades in; the
   bundle sets the start and the end and takes both off after `SLIDE_MS`
   (crate::live, and a test holds the two numbers together). Under reduced
   motion the base layer makes every transition 0.01ms, so the row simply
   appears. A page without the bundle never draws this class. */
@layer pages {
  .bb-feed-arriving {
    overflow: hidden;
    transition: block-size 280ms cubic-bezier(0.2, 0.7, 0.2, 1),
      padding-block 280ms cubic-bezier(0.2, 0.7, 0.2, 1),
      opacity 280ms cubic-bezier(0.2, 0.7, 0.2, 1);
  }
}

/* D-141, THE HOME FEED AS A TIMELINE, WITH A BOARD OF PINNED ANNOUNCEMENTS.
   The owner's picks of 2026-10-26 from the design pass of that day: the
   look is C, the timeline (no cards: a face column, everything else lined
   up with the name, a thin rule between posts, announcements and the board
   as harvest bands with the pumpkin edge, replies on a thread line, activity
   lines as quiet single lines), which is A's markup restyled; and on the
   board the newest pinned announcement starts open and the rest are folded.
   THIS REVERSES THE CARDS OF D-102 AND D-106 ON HOME'S FEED; the rest of the
   site keeps them.

   Folded from the design's feed-shared.css, feed-a.css and feed-c.css,
   which it loaded in that order AFTER this stylesheet when it measured them,
   so this block is the last in the file and wins where those files won.
   SCOPE as D-130d: every rule on a hook /o/say or another page shares is
   under `.bb-feed-panel`; the rest style classes only Home draws, or that
   the bundle adds. Every colour is a site token, so dark follows. */
@layer pages {

  /* 1. THE BAR. D-72's offer, as the feed's bar: still in the sticky header,
     out of the flow, sent hidden and unhidden only by the bundle, still a
     link to this page so a failed fetch falls back to a load. A pill with an
     arrow up. ONLY while the bundle has made it the feed's bar
     (`crate::live::FEED_OFFER_CLASS`); as "This page has changed" it keeps
     D-72's look, on Home and on every other page. */
  .bb-live-offer.bb-live-offer-feed {
    border-radius: 2rem;

    & a {
      gap: var(--space-2);
      padding: 0 var(--space-5) 0 var(--space-4);
      font-size: var(--text-base);
      line-height: var(--leading-tight);
    }
    & svg {
      flex: none;
      inline-size: 1.125rem;
      block-size: 1.125rem;
      fill: currentColor;
    }
    & a:focus-visible {
      border-radius: 2rem;
    }
  }

  /* 2. A POST THAT ARRIVED WHILE THE PAGE WAS OPEN: the word "New" after the
     name for the life of the page (a word, not only a colour), and a warm
     wash that fades once it has landed (section 15). */
  .bb-post-new-tag {
    display: inline-block;
    margin-inline-start: var(--space-2);
    padding: 0.0625rem var(--space-2);
    border-radius: 2rem;
    background: var(--tint-accent);
    color: var(--color-accent-deep);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: 1.3;
    vertical-align: 0.0625rem;
  }

  /* 3. THE HEART KEEPS ITS WIDTH: its count is written in place by an open
     page (D-72's numbers), so the button is wide enough for two digits from
     the start and "Reply" beside it never shifts when a count appears. */
  .bb-feed-panel .bb-heart {
    min-inline-size: 4.25rem;
    justify-content: center;
  }
  /* The count and who gave hearts are drawn always and are empty for none,
     because an open page writes them as text and never adds an element
     (D-72). Empty, neither takes any room: the count is not drawn, and the
     names line goes with its heart drawing. */
  .bb-heart-count:empty,
  .bb-hearts-who:has(> .bb-hearts-names:empty) {
    display: none;
  }

  /* 4. PIN, UNPIN AND AN ORGANISER'S OWN CHANGE at the end of a bar, apart
     from the heart and "Reply" (D-140, D-141). Opened, Change takes the
     bar's whole width, as every opened control in the bar does. */
  .bb-post-org {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-inline-start: auto;
    &:has(> details[open]) {
      flex-basis: 100%;
    }
    & > .bb-pin-form,
    & > .bb-post-mine {
      margin-inline-start: 0;
    }
  }

  /* 5. A FACE FOR EVERYBODY: a person with no picture gets their initials on
     a tint, so every name starts at the same place. Never under the text
     floor. */
  .bb-face-initials {
    display: inline-grid;
    place-items: center;
    flex: none;
    overflow: hidden;
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0;
  }
  .bb-face-sage {
    background: var(--tint-sage);
  }
  .bb-face-accent {
    background: var(--tint-accent);
  }
  .bb-face-cranberry {
    background: var(--tint-cranberry);
  }
  .bb-face-sand {
    background: var(--tint-sand);
  }

  /* 6. THE BOARD: a harvest band across the column with the pumpkin edge, a
     pushpin and the words "Pinned announcements" (the kind stays a WORD). */
  .bb-pinboard {
    min-inline-size: 0;
    margin-inline: calc(-1 * var(--space-4));
    border-block: 0.25rem solid var(--color-pumpkin);
    border-block-end: 1px solid var(--color-line);
    background: var(--tint-harvest);
    overflow: hidden;
  }
  .bb-pinboard-title {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-4);
    color: var(--color-text-muted);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: 600;
    line-height: var(--leading-tight);
    & > svg {
      flex: none;
      inline-size: 1.125rem;
      block-size: 1.125rem;
      fill: var(--color-pumpkin);
    }
  }
  .bb-pinned {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .bb-feed-panel .bb-pinned > .bb-post-pinned {
    margin: 0;
    padding: 0;
    border: 0;
    border-block-start: 1px dotted var(--color-line-strong);
    border-radius: 0;
    background: none;
    scroll-margin-block-start: 6rem;
  }

  /* 7. A PINNED ROW: folded, the face, the name, the first two lines and a
     chevron; the whole row is the fold's summary, so the whole row is the
     tap target. Open, the whole announcement under the name. */
  .bb-pin-fold {
    min-inline-size: 0;
  }
  .bb-pin-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: var(--space-3);
    row-gap: 0.125rem;
    align-items: start;
    min-block-size: var(--tap-min);
    padding: var(--space-2) var(--space-4);
    list-style: none;
    cursor: pointer;
    &::-webkit-details-marker {
      display: none;
    }
    &:hover {
      background: var(--tint-accent);
    }
    & > .bb-face {
      grid-row: 1 / span 2;
      inline-size: 2rem;
      block-size: 2rem;
      margin: 0.125rem 0 0;
    }
  }
  .bb-pin-who {
    grid-column: 2;
    color: var(--color-text);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: var(--leading-tight);
  }
  .bb-pin-gist {
    grid-column: 2;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    color: var(--color-text);
    font-size: var(--text-sm);
    line-height: 1.4;
  }
  .bb-pin-chev {
    grid-column: 3;
    grid-row: 1 / span 2;
    align-self: center;
    display: grid;
    & > svg {
      inline-size: 1.25rem;
      block-size: 1.25rem;
      fill: none;
      stroke: var(--color-text-muted);
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
  }
  .bb-pin-fold[open] > .bb-pin-row {
    align-items: center;
    & > .bb-face {
      grid-row: 1;
      margin: 0;
    }
    & .bb-pin-gist {
      display: none;
    }
    & .bb-pin-chev > svg {
      rotate: 90deg;
    }
  }
  .bb-pin-body {
    padding: 0 var(--space-4) var(--space-1) calc(var(--space-4) + 2rem + var(--space-3));
    & > .bb-post-text {
      margin-block-start: 0;
    }
    & > .bb-hearts-who {
      margin-block-start: var(--space-2);
    }
  }
  /* More than three pinned: the rest are one tap away. */
  .bb-pinned-more {
    border-block-start: 1px dotted var(--color-line-strong);
    padding: 0 var(--space-4);
    & > .bb-pinned {
      margin-inline: calc(-1 * var(--space-4));
    }
  }
  /* The feed's pill (D-130's section 4 draws it only inside a bar, a head,
     a reply or a long post's fold, so this summary needs its own). */
  .bb-feed-panel .bb-pinned-more > .bb-post-control {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    min-block-size: var(--tap-min);
    padding: 0 var(--space-3);
    margin-inline-start: calc(-1 * var(--space-1));
    border: 0;
    background: none;
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 700;
    list-style: none;
    cursor: pointer;
    &::-webkit-details-marker {
      display: none;
    }
    &::before {
      content: "";
      position: absolute;
      inset: 0.5rem 0;
      z-index: -1;
      border: 2px solid var(--color-line-strong);
      border-radius: 2rem;
      background: var(--color-surface);
    }
  }

  /* 8. THE STREAM: no cards. One column on the page's own ground, the face
     hanging in a 2.25rem column at the left, everything else lined up with
     the name, a rule between posts. */
  .bb-feed-panel .bb-feed {
    gap: 0;
    border-block-start: 1px solid var(--color-line);
  }
  .bb-feed-panel .bb-feed:empty {
    border-block-start: 0;
  }
  .bb-feed-panel .bb-feed > .bb-post {
    position: relative;
    padding: var(--space-3) 0 0 calc(2.25rem + var(--space-3));
    border: 0;
    border-block-end: 1px solid var(--color-line);
    border-radius: 0;
    background: none;
  }
  .bb-feed-panel .bb-feed > .bb-post > .bb-post-head > .bb-face {
    position: absolute;
    inset-inline-start: 0;
    inset-block-start: var(--space-3);
    inline-size: 2.25rem;
    block-size: 2.25rem;
  }
  .bb-feed-panel .bb-post-head {
    min-block-size: 2.25rem;
  }
  .bb-feed-panel .bb-post-text {
    margin-block-start: var(--space-1);
  }
  .bb-feed-panel .bb-post-bar {
    border-block-start: 0;
    margin-block-start: 0;
  }
  /* An announcement: a harvest band across the column, the pumpkin edge. */
  .bb-feed-panel .bb-feed > .bb-post.bb-post-announcement {
    margin-inline: calc(-1 * var(--space-4));
    padding-inline: calc(var(--space-4) + 2.25rem + var(--space-3)) var(--space-4);
    border-inline-start: 0.3rem solid var(--color-pumpkin);
    background: var(--tint-harvest);
    & > .bb-post-head > .bb-face {
      inset-inline-start: calc(var(--space-4) - 0.3rem);
    }
  }
  /* A post's photos by how many: one is drawn larger, two side by side,
     three or more three across as before. */
  .bb-feed-panel .bb-post-photos-1 {
    grid-template-columns: minmax(0, 13rem);
  }
  .bb-feed-panel .bb-post-photos-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* Replies on a thread line under the words, no bubbles, a face for
     everybody. */
  .bb-feed-panel .bb-replies {
    margin: 0 0 var(--space-3);
    padding-inline-start: var(--space-3);
    border-inline-start: 2px solid var(--color-line);
  }
  .bb-feed-panel .bb-reply {
    padding: var(--space-1) 0;
    background: none;
    & .bb-face {
      inline-size: 1.75rem;
      block-size: 1.75rem;
      margin: 0;
    }
  }
  .bb-feed-panel .bb-reply-author {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

  /* 9. WHAT RELATIVES DID: one quiet line each, no card, the face in the
     same column as a post's so every name starts at the same place. */
  .bb-feed-panel .bb-feed > .bb-did,
  .bb-feed-panel .bb-feed > .bb-did + .bb-did,
  .bb-feed-panel .bb-feed > :is(:not(.bb-did) + .bb-did, .bb-did:first-child),
  .bb-feed-panel .bb-feed > :is(.bb-did:has(+ :not(.bb-did)), .bb-did:last-child) {
    margin: 0;
    padding: var(--space-2) 0;
    border: 0;
    border-block-end: 1px dotted var(--color-line-strong);
    border-radius: 0;
    background: none;
    color: var(--color-text-muted);
    &::before {
      display: none;
    }
  }
  .bb-feed-panel .bb-did-line {
    display: grid;
    grid-template-columns: 2.25rem minmax(0, 1fr);
    column-gap: var(--space-3);
    align-items: start;
    margin: 0;
    & > .bb-face {
      justify-self: center;
      inline-size: 1.75rem;
      block-size: 1.75rem;
      margin: 0;
    }
  }
  .bb-did-text {
    padding-block-start: 0.1rem;
  }

  /* 10. NOTHING YET: the same words on a card of their own, so an empty feed
     reads as a place that will fill rather than a missing section. */
  .bb-feed-panel > .bb-feed-empty {
    margin: 0;
    padding: var(--space-4);
    border: 2px dashed var(--color-line-strong);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text-muted);
    text-align: center;
  }

  /* 11. What arrived, for a screen reader: written by the bundle, never
     drawn on the screen. */
  .bb-feed-heard {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* 12. MOVEMENT, only where the device has not asked for less: the bar
     drops in, a new post's wash fades, the board's chevron turns. Under
     reduce none of it starts and rows simply appear, with "New". */
  @media (prefers-reduced-motion: no-preference) {
    .bb-live-offer.bb-live-offer-feed {
      animation: bb-offer-in 200ms ease-out both;
    }
    .bb-feed-panel .bb-feed > .bb-post-new {
      animation: bb-wash 2.4s ease-out 0.3s both;
    }
    .bb-pin-chev > svg {
      transition: rotate 150ms ease-out;
    }
  }
  @keyframes bb-offer-in {
    from {
      opacity: 0;
      translate: 0 -0.5rem;
    }
    to {
      opacity: 1;
      translate: 0 0;
    }
  }
  @keyframes bb-wash {
    from {
      background-color: var(--tint-accent);
    }
  }
}

/* D-142, the owner's correction of D-141's look, 2026-10-27: "seems rough
   and like it doesn't fit there. i was thinking the one i picked was like
   one single large window." The timeline inside stays exactly as D-141 drew
   it; the whole of "What is happening" (its heading as the window's title,
   the post box, the board, the stream and "Show everything that has
   happened") sits in ONE window that matches the Big Bear card above it, so
   the feed is framed like everything else on the page.

   THE FRAME IS THE BANNER'S, read off the banner's own rules rather than
   restated from memory: `.bb-title-row:has(> .bb-scene)` draws a 1px
   `--color-line` border, `--radius` corners, the `--scene-sky` background
   and no shadow (no box-shadow rule in this file names the title row); on
   Home its title sits `--space-3` from the top and `--space-4` from the
   sides (D-130's section 7), and that inset is the window's padding, so
   "What is happening" sits in its window where "Big Bear" sits in its card.
   The banner's `overflow: hidden` is not copied: it clips the drawing to
   the corners, and nothing in the window reaches a corner (seen in every
   render of this change).

   INSIDE, WHAT RAN TO THE PAGE'S EDGES NOW RUNS TO THE WINDOW'S. The board
   and an announcement's band already bled by `--space-4`, which is now the
   window's own inset. The stream bleeds by the same, so the rules between
   posts and under activity lines run edge to edge of the window like the
   board's, and each row takes the inset back as padding: every face sits
   `--space-4` in from the window's edge, as the board's faces do.

   Last in the file, so at equal specificity these win over D-141's block. */
@layer pages {

  .bb-feed-panel {
    padding: var(--space-3) var(--space-4) var(--space-4);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    background: var(--scene-sky);
  }

  .bb-feed-panel .bb-feed {
    margin-inline: calc(-1 * var(--space-4));
  }
  .bb-feed-panel .bb-feed > .bb-post {
    padding-inline: calc(var(--space-4) + 2.25rem + var(--space-3)) var(--space-4);
  }
  .bb-feed-panel .bb-feed > .bb-post > .bb-post-head > .bb-face {
    inset-inline-start: var(--space-4);
  }
  /* The band is the row's own width now, and its face keeps D-141's place,
     which already counted the inset. Its words start 0.3rem sooner than
     D-141 put them: D-141's padding did not take off the pumpkin edge, so an
     announcement's name sat 5.1px right of every other name in the stream
     (MEASURED in the D-141 render and in the design's own: 73 against 68
     from the page's edge), and the design says everything lines up with
     the name. */
  .bb-feed-panel .bb-feed > .bb-post.bb-post-announcement {
    margin-inline: 0;
    padding-inline-start: calc(var(--space-4) + 2.25rem + var(--space-3) - 0.3rem);
  }
  /* Under the board, the board's own bottom line is the stream's top rule.
     Both run the window's width now, so drawing the stream's as well put
     two rules a gap apart (seen in this change's first render). */
  .bb-feed-panel .bb-pinboard + .bb-feed {
    border-block-start: 0;
  }
  /* AT THE WINDOW'S FOOT the window's own border is the last line. With
     nothing under the stream (no "Show everything that has happened"), its
     last row's rule sat a padding above the border, two lines again (seen
     in this change's first render); the same for the board's
     bottom line when the stream is empty. An empty stream still takes a
     place in the window's grid, and a gap with it, so what stands before it
     reaches over that gap, or the window's foot would be a gap deeper than
     its sides (MEASURED, 29.7 against 17 in the empty feed's first
     render). The empty stream stays where it is in the layout rather than
     going or moving: the bundle measures where it is. */
  .bb-feed-panel > .bb-feed:not(:has(~ .bb-panel-action)) > :last-child {
    border-block-end: 0;
  }
  .bb-feed-panel .bb-pinboard:has(+ .bb-feed:empty) {
    border-block-end: 0;
  }
  .bb-feed-panel > :has(+ .bb-feed:empty) {
    margin-block-end: calc(-1 * var(--space-3));
  }
  .bb-feed-panel .bb-feed > .bb-did,
  .bb-feed-panel .bb-feed > .bb-did + .bb-did,
  .bb-feed-panel .bb-feed > :is(:not(.bb-did) + .bb-did, .bb-did:first-child),
  .bb-feed-panel .bb-feed > :is(.bb-did:has(+ :not(.bb-did)), .bb-did:last-child) {
    padding-inline: var(--space-4);
  }
}

/* ------------------------------------------------------------------
 * D-143 shopping list: everything a person said they will bring on
 * "What we need", with a private tick for each, and the others in their
 * household read only.
 *
 * A row is drawn like `/bring`'s, with the same edge, ground and corners,
 * and its words are `/bring`'s (`.bb-bring-what`, `.bb-bring-title`,
 * `.bb-bring-asker`), with the tick in front of them. The row's own hook is
 * not `.bb-bring-item`: that hook is the bring list's alone
 * (`nothing_but_this_page_draws_a_row_of_the_bring_list`), because its
 * swipe and its card hang off it.
 * The tick is a button, because a tick is a form post with no script, drawn
 * as D-113's tile: 1.5rem square, 2px of the control border on the surface,
 * 0.45rem corners; ticked, filled in the accent with the tick in the
 * accent's text color. The tick is a stroke in the current color rather
 * than a cut-out, so it is still drawn where a forced-colors screen drops
 * backgrounds (reasoned, NOT MEASURED). The button is `--tap-min` square,
 * so the box is small and the thing a thumb hits is not.
 *
 * A ticked row stays where it is, its words muted and the thing struck
 * through, so the next thing is still under the thumb after the page
 * comes back. A household member's row carries the same tile, drawn and
 * not pressable.
 * ------------------------------------------------------------------ */
@layer pages {
  .bb-shop-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: var(--space-2);
    min-inline-size: 0;
    padding: var(--space-1) var(--space-3) var(--space-1) var(--space-1);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
  }

  .bb-shop-form {
    margin: 0;
  }

  .bb-shop-tick,
  .bb-shop-mark {
    display: grid;
    place-items: center;
    inline-size: var(--tap-min);
    block-size: var(--tap-min);
  }

  .bb-shop-tick {
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: inherit;
    cursor: pointer;
  }

  .bb-shop-tile {
    display: grid;
    place-items: center;
    inline-size: 1.5rem;
    block-size: 1.5rem;
    border: 2px solid var(--color-line-strong);
    border-radius: 0.45rem;
    background: var(--color-surface);
    color: var(--color-on-accent);

    & > svg {
      inline-size: 1rem;
      block-size: 1rem;
      fill: none;
      stroke: currentColor;
      stroke-width: 2.6;
      stroke-linecap: round;
      stroke-linejoin: round;
      visibility: hidden;
    }
  }

  .bb-shop-ticked .bb-shop-tile {
    border-color: var(--color-accent);
    background: var(--color-accent);

    & > svg {
      visibility: visible;
    }
  }

  /* The thing and how many, one link to it on "What we need", the whole
     block at least the tap floor tall: the name alone measured 25.5px. Only
     the name is underlined, in the control border's color. */
  .bb-shop-thing {
    align-content: center;
    min-block-size: var(--tap-min);
    color: var(--color-text);
    text-decoration: none;

    & > .bb-bring-title {
      text-decoration: underline;
      text-decoration-color: var(--color-line-strong);
      text-underline-offset: 0.2em;
    }

    &:hover {
      text-decoration: none;
    }
  }

  .bb-shop-ticked .bb-shop-thing {
    color: var(--color-text-muted);

    & > .bb-bring-title {
      text-decoration-line: underline line-through;
    }
  }

  .bb-shop-who {
    margin: var(--space-2) 0 0;
    font-size: var(--text-base);
  }

  /* A thing's anchor, here and on "What we need": a tick lands back on its
     row and a link from here lands on the thing there, each under the
     sticky header, by the 6rem the other landings use. */
  [id^="bb-thing-"] {
    scroll-margin-block-start: 6rem;
  }
}

/* D-147: who is cooking a meal. On /meals, "Cooking: <names>" on the meal's
   card above its menu or ballot, with the reader's button under it ("I'll
   cook this meal", or "I cannot cook after all" on their own); on the
   organizers' board, a panel under the meal's own controls. */
@layer components {
  .bb-meal-cooks {
    margin: 0;
    font-weight: 700;
  }

  /* On the reader's own cook an organizer put down, who did, in the size
     D-146's "put you down for this" uses on /bring. */
  .bb-cook-by {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.3;
  }

  .bb-board-cooks {
    display: grid;
    gap: var(--space-2);
    margin-block-start: var(--space-3);
    padding-block-start: var(--space-3);
    border-block-start: var(--border);

    & > .bb-open-vote {
      margin-block-start: 0;
      padding-block-start: 0;
      border-block-start: 0;
    }
  }

  .bb-board-cooks-heading {
    margin: 0;
    font-size: var(--text-base);
    line-height: var(--leading-tight);
  }

  .bb-board-cook-list {
    display: grid;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;

    & > li {
      display: grid;
      gap: var(--space-1);
    }
  }

  /* A cook whose household's answer now has them arriving after the meal
     (D-145e's case, kept as made): pumpkin, the box for a fact about the
     trip rather than a refusal, as a cancelled meal's note is. */
  .bb-board-cook-late {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border: 2px solid var(--color-pumpkin);
    border-radius: var(--radius-sm);
    background: var(--tint-accent);
  }
}
