/* ============================================================================
   MOBILE SHELL (<= 768px)
   ============================================================================
   Added 2026-09-29. Everything phone-specific in the app lives in this ONE
   file, and nothing else does.

   WHY IT IS A FILE AND NOT A BLOCK IN custom.css
   It began as a section at the end of custom.css, because the design-token
   lint (scripts/check_css_tokens.py) was hard-wired to that single filename
   and a new file would have escaped it in silence. Gael reversed that call on
   2026-09-29: a phone layout is a feature, the project's own guidance is one
   stylesheet per feature, and a separate file is far easier to review and to
   revert than 800 lines appended to an 11,000-line monolith. The lint now
   walks both files, so nothing was traded away for the split.

   CASCADE CONTRACT -- load it AFTER custom.css (see base.html) and never
   before. Every rule here overrides one there, and it relies on source order
   to do it: there is not a single `!important` in this file, and adding one
   means the load order has been broken rather than that a rule needed force.

   THE BREAKPOINTS, AND WHY THERE ARE TWO
   The page layout (sections 2-7) switches at 768px: the width the section
   rail already disappears at (the `@media (max-width: 768px)` block in
   custom.css), so a page's body changes shape at ONE width.

   The BAR (section 1) switches earlier, at 1365px, because below that it
   does not fit with headroom. Even icon-only, the desktop row measures
   ~1265px with an admin signed in and an edition tracked (2026-10-01), and
   ~1281px once the edition Tasks tab lands; a longer edition name adds ~35px
   per ten characters and CI fonts render ~3% wider. Between 769 and 1279 it
   scrolled the whole page sideways -- 241px over at 1024 -- which
   docs/rules/ui.md section 2 forbids. 1366 is a common laptop width, and it
   keeps the desktop row with ~85px clear (Gael, 2026-10-01). The resulting band (drawer, but rail
   and desktop page layout) is the harmless direction: the case to avoid is
   the reverse, a rail gone with a desktop row still overflowing above it.
   tests/e2e/test_main_menu.py sweeps both sides of 1365.

   THE JS GATE, AND WHY THE DRAWER DEPENDS ON IT
   Almost every rule below is scoped to `html[data-nav-js]`, the attribute
   app/static/js/mobile-nav.js stamps when it binds. That is not decoration:
   the drawer HIDES the navigation until a button reveals it, so hiding it
   before the button works would strand a phone with no menu at all. With JS
   off the attribute is absent, every drawer rule below is inert, and the bar
   falls back to wrapping into rows — cramped, and completely usable.

   The table rules are gated the same way for the same reason: their card
   layout needs the `data-label` attributes mobile-table.js writes, so without
   the script the tables stay tables and scroll sideways instead.

   READ-FIRST, NOT READ-ONLY
   The target is reading the agenda, a comm and a speaker on a phone during a
   conference, plus the one-tap actions that go with that (approve, send,
   change a status). Long multi-field forms reflow to one column and stay
   reachable, but they are not the surface this block optimises for.
   ============================================================================ */

/* -- The two mobile-only controls are absent from the desktop bar ---------- */
.nav-burger,
.nav-mobile-title,
.nav-drawer-head,
.nav-backdrop {
    display: none;
}

@media (max-width: 1365px) { /* css-tokens: allow — breakpoint, not a value; custom props are invalid in media queries */

    /* ====================================================================
       1. THE BAR  (<= 1365px -- see THE BREAKPOINTS above)
       ==================================================================== */

    :root {
        /* Everything derived from this follows: --sticky-stack-height,
           --sticky-offset, the scrollport's scroll-padding-top, the agenda
           track header's sticky top. One property, one place.
           app/static/js/context-bar.js only ever republishes this when the
           RENDERED bar is taller than the token, and the mobile bar is three
           44px controls in a 56px box, so it never fires here.

           Re-declared further down with the iPhone's top safe-area inset
           added; that later declaration wins, and this one is what a device
           with no cutouts resolves to. */
        --navbar-height: var(--navbar-height-mobile);
    }

    .navbar {
        /* The desktop bar is `space-between` with two children. The mobile bar
           has four (burger, title, drawer, account) and wants the title to
           take the slack, so the gap does the spacing and the title grows.

           Horizontal and bottom padding only -- the safe-area block below owns
           the top, left and right, and a shorthand `padding` here would reset
           what it sets. */
        gap: var(--space-2);
        padding-bottom: var(--space-2);
    }

    /* `.btn-chrome` (above) already owns the reset, the 44px floor and the
       hover tint. All this width has to do is reveal it. */
    .nav-burger {
        display: inline-flex;
    }

    .nav-mobile-title {
        display: block;
        flex: 1 1 auto;
        /* The bar has room for one line of context and no more. A long edition
           name truncates rather than wrapping the bar to two rows, which would
           break through the --navbar-height floor and push every sticky offset
           in the app down with it. */
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: var(--color-neutral-50);
        font-size: var(--font-size-base);
        font-weight: 600;
    }

    .navbar .nav-right {
        gap: var(--space-2);
        flex-shrink: 0;
    }

    /* -- The drawer itself -------------------------------------------------
       `.nav-links` is the SAME element the desktop bar lays out in a row. It
       is re-laid-out here, never duplicated, so a tab added to base.html is in
       the drawer for free. */
    html[data-nav-js] .navbar .nav-links {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: min(var(--nav-drawer-width), 86vw);
        z-index: var(--nav-drawer-z);
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-1);
        padding: var(--space-3);
        background-color: var(--color-neutral-900);
        border-right: 1px solid var(--color-neutral-700);
        /* A long menu on a short phone must scroll, and `-webkit-overflow-
           scrolling` is not needed on modern Android Chrome. */
        overflow-y: auto;
        overscroll-behavior: contain;
        transform: translateX(-100%);
        /* Under the 300ms motion ceiling, and the project's reduced-motion
           block at the end of tokens.css collapses it to nothing for anyone
           who asked for that. */
        transition: transform 0.2s ease-out; /* css-tokens: allow — duration, not a layout value */
        /* Out of the tab order while closed: a drawer sitting off-screen that
           still catches Tab is the classic invisible-focus bug. */
        visibility: hidden;
    }

    html[data-nav-js][data-nav-open] .navbar .nav-links {
        transform: translateX(0);
        visibility: visible;
    }

    html[data-nav-js] .nav-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: var(--nav-backdrop-z);
        background-color: var(--nav-backdrop-bg);
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.2s ease-out; /* css-tokens: allow — duration, not a layout value */
    }

    html[data-nav-js][data-nav-open] .nav-backdrop {
        opacity: 1;
        visibility: visible;
    }

    /* The page must not scroll behind an open drawer — on a phone that reads
       as the drawer having lost its place. */
    html[data-nav-open] body {
        overflow: hidden;
    }

    /* -- Drawer contents ---------------------------------------------------
       Every nav control becomes a full-width row: a 44px-tall target with its
       icon and its label, which is the shape a thumb expects and the shape
       that makes the two nav LEVELS legible as one list. */
    html[data-nav-js] .nav-drawer-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-2);
        padding-bottom: var(--space-2);
        margin-bottom: var(--space-1);
        border-bottom: 1px solid var(--color-neutral-700);
    }

    html[data-nav-js] .nav-drawer-head__title {
        color: var(--color-neutral-300);
        font-size: var(--font-size-sm);
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.06em; /* css-tokens: allow — typographic tracking, not a spacing value */
    }

    html[data-nav-js] .nav-drawer-close {
        display: inline-flex;
    }

    /* Both levels' labels, revealed together in the drawer.

       They are two DIFFERENT classes on purpose. `.nav-tab-label` belongs to
       the edition tabs and is revealed on the desktop bar at >=1920px, where
       there is finally room for it. `.nav-drawer-label` is the drawer's own
       and is revealed NOWHERE else -- because the first attempt reused
       `.nav-tab-label` for the global tabs, inherited that 1920px reveal, and
       grew the bar to 2039px at a 1920px viewport. The page scrolled sideways,
       which docs/rules/ui.md forbids, and test_main_menu.py's width sweep
       caught it. */
    html[data-nav-js] .navbar .nav-tab-label,
    html[data-nav-js] .navbar .nav-drawer-label {
        display: inline;
    }

    html[data-nav-js] .navbar .nav-links > a,
    html[data-nav-js] .navbar .nav-icon-tab,
    html[data-nav-js] .navbar a.nav-scoped,
    html[data-nav-js] .navbar .nav-home {
        display: flex;
        justify-content: flex-start;
        align-items: center;
        gap: var(--space-3);
        width: 100%;
        min-height: var(--target-size-min);
        padding: var(--space-2) var(--space-3);
        font-size: var(--font-size-base);
    }

    /* A horizontal hairline reads as a list divider; the desktop bar's
       1px-wide vertical pip does not. */
    html[data-nav-js] .navbar .nav-separator {
        width: 100%;
        /* A border rather than a 1px-tall painted box: same hairline, and it
           matches the drawer head's own divider instead of the desktop bar's
           translucent white pip, which is a vertical-pip colour. */
        height: 0;
        background-color: transparent;
        border-top: 1px solid var(--color-neutral-700);
        margin: var(--space-2) 0;
    }

    /* The edition picker is the first thing in the drawer after its own head,
       and it is the widest control there — it names the thing everything
       below it is scoped to. */
    /* A column, not the bar's row: with no edition tracked the picker carries
       a "Please select a conference first" prompt beside it, and the two side
       by side are ~370px -- past the drawer's 320px, so the one control the
       prompt points at was the one clipped off the edge. */
    html[data-nav-js] .conference-selector {
        width: 100%;
        flex-direction: column;
        align-items: stretch;
    }

    html[data-nav-js] .conference-picker,
    html[data-nav-js] .conference-picker > summary {
        width: 100%;
        min-height: var(--target-size-min);
    }

    /* Pico gives a dropdown inside `<nav>` a -16px top margin: in the bar's
       row it centres the picker, in this column it pulled it up over the
       "select a conference first" prompt and onto the divider above. */
    html[data-nav-js] .conference-picker {
        margin-top: 0;
    }

    /* Dropdown panels inside the drawer are already inside a scrolling column,
       so they open in flow rather than absolutely — an absolutely-positioned
       panel would hang outside the drawer's 320px and off the screen. */
    html[data-nav-js] .conference-picker > ul,
    html[data-nav-js] .navbar .user-menu > ul {
        position: static;
        width: 100%;
        max-height: 50vh;
        overflow-y: auto;
    }

    /* Pico hangs a transparent full-screen layer on an open dropdown's
       `summary::before` (fixed, z-index 1) so a tap outside closes it. It only
       works because Pico's panel is absolute at z-index 99; a STATIC panel
       ignores its z-index, so the layer sat on top of every row and a tap on
       an edition closed the menu instead of following the link -- the phone
       stayed on whatever edition it already had. The panel opens in flow here
       and the summary itself closes it, so the layer has no job left. */
    html[data-nav-js] .conference-picker[open] > summary::before {
        display: none;
    }

    /* Every row in an open menu is a tap target, not just the tabs outside it.
       The picker's entries measured 33px -- under the 44px floor in
       docs/rules/ui.md section 7, and picking the wrong edition is exactly the
       mistake a cramped target causes. */
    html[data-nav-js] .conference-picker > ul a,
    html[data-nav-js] .navbar .user-menu > ul a {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        min-height: var(--target-size-min);
    }

    /* The account menu stays in the BAR, not in the drawer — it is the one
       control a phone user reaches for without first opening a menu, and its
       panel is short enough to anchor. */
    html[data-nav-js] .navbar .nav-right .user-menu > ul {
        position: absolute;
        width: max-content;
        max-width: 80vw;
        right: 0;
        left: auto;
    }

    /* The global search is LIVE -- `/search` and `/search/dropdown` are real
       routes and this box is wired to them. An earlier version of this file
       hid it on a phone, on the strength of a stale comment in custom.css that
       still called it "not wired up yet". Hiding a working search is a
       parity regression, so it gets the drawer's full width instead: it is the
       first row under the drawer head, where a thumb reaches first, and it
       spans every edition so it belongs above the edition hinge.

       The results panel is `position: absolute` against the box on desktop.
       Inside a scrolling drawer that would clip, so it flows in place and
       takes its own scroll. */
    html[data-nav-js] .navbar .nav-search {
        display: flex;
        width: 100%;
        order: -1;
    }

    html[data-nav-js] .navbar .nav-search input[type="search"] {
        width: 100%;
        /* Below 16px the browser zooms the page on focus and leaves it
           zoomed -- the same floor as every other field (section 5). */
        font-size: var(--font-size-base);
    }

    html[data-nav-js] .navbar .nav-search #nav-search-results {
        position: static;
        width: 100%;
        max-height: 50vh;
        overflow-y: auto;
    }

    /* -- The notch, the rounded corners and the home indicator -------------
       `viewport-fit=cover` in base.html lets the page paint under an iPhone's
       notch and rounded corners; these rules are what stop CONTENT going under
       them. Without the meta tag every `env(safe-area-inset-*)` reports 0, so
       this whole group is inert on a device that has no cutouts -- an Android
       phone, a desktop window -- and costs nothing there.

       Three surfaces need it, and only three: anything the page paints edge to
       edge. The bar is pinned to the top, so it takes the top inset as padding
       and grows by exactly the notch. The drawer runs the full height, so it
       takes top and bottom. And the page's own scrolling content takes the
       bottom inset, so the last row of a list clears the home indicator
       instead of sitting under it.

       `max()` and not a bare inset: the inset is 0 in portrait on a phone with
       no cutout, and a bare `env()` there would wipe the padding the layout
       already had. */
    .navbar {
        padding-top: max(var(--space-2), env(safe-area-inset-top));
        padding-left: max(var(--space-3), env(safe-area-inset-left));
        padding-right: max(var(--space-3), env(safe-area-inset-right));
    }

    /* The bar's own box grew by the inset, so the height every sticky offset
       derives from has to grow with it or the whole stack sits under the
       notch. One property, as everywhere else. */
    :root {
        --navbar-height: calc(var(--navbar-height-mobile) + env(safe-area-inset-top, 0px)); /* css-tokens: allow -- 0px is env()'s fallback, not a layout value */
    }

    html[data-nav-js] .navbar .nav-links {
        padding-top: max(var(--space-3), env(safe-area-inset-top));
        padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
        padding-left: max(var(--space-3), env(safe-area-inset-left));
    }

    /* A toast is fixed to the top, under the bar, so it clears the notch by
       clearing the bar -- which now includes the inset. Nothing extra needed
       here; noted so the next reader does not add a second correction. */
}

@media (max-width: 768px) { /* css-tokens: allow — breakpoint, not a value; custom props are invalid in media queries */

    /* The page's own scrolling content clears the home indicator (see the
       notch notes in section 1). */
    main.container {
        padding-bottom: calc(var(--space-8) + env(safe-area-inset-bottom, 0px)); /* css-tokens: allow -- 0px is env()'s fallback, not a layout value */
    }

    /* ====================================================================
       2. THE PAGE SHELL
       ==================================================================== */

    /* Vertical only. Pico's own container ALREADY gives 12px of horizontal
       padding at phone widths and 0 at ~700px, where the container is inset by
       its max-width instead -- so a blanket override here does nothing useful
       on a phone and adds 12px that nothing else knows about just under the
       breakpoint. That is not hypothetical: it put the detail pages' footer
       action row 12px short of the container edge it is measured against
       (tests/e2e/test_detail_page_action_alignment.py, at a 700px viewport). */
    main.container {
        padding-top: var(--space-4);
        padding-bottom: var(--space-8);
    }

    /* ONE ROW FOR WHERE YOU ARE AND WHAT YOU CAN DO, ONE FOR THE SEARCH
       (docs/rules/ui.md "Spend every row", Gael 2026-09-30).

       Row 1 is the page icon, the title and the actions; row 2 is the search,
       its reset and the view toggle. The phone header this replaced gave the
       icon, the title, the scope, the search and the actions a row EACH --
       five rows before the page started, on a screen that shows about fifteen.

       `order` regroups without touching a template: the actions follow the
       search in the DOM (desktop reads title, search, actions left to right),
       so here they are pulled forward onto the title's row. The title takes
       what the actions leave (`flex: 1 1 0`) but never less than 10rem: when
       the icons do not fit beside that, they move down a row rather than
       squeeze the title. A 30ch search cannot join a title and its icons on
       a 412px line, so it opens the next row.

       The labels give way on their own: page-header-fit.js measures that the
       labelled actions would cost a row here and drops them (custom.css,
       "LABELS GIVE WAY BEFORE THE SEARCH AND THE TITLE"). No rule below
       hides them -- a phone is just the narrowest case of the same rule. */
    .page-header {
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .page-header__icon,
    .page-header__icon :is(i, svg) {
        width: var(--space-6);
        height: var(--space-6);
    }

    /* Every variant the title rules in custom.css use: each carries a `:not()`
       or a `:has()` scoring (0,2,1), and a bare `.page-header h1` loses to all
       of them -- the detail variant's `min-width: 0` won, and a comm title
       broke one syllable per line beside its five icons.

       The floor is what makes the title outrank the icons: 10rem, or the
       whole row on a phone narrower than that. When the icons cannot fit
       beside a title that wide, THEY wrap to the next row; the title never
       gives up its words for them. */
    .page-header h1,
    .page-header:not(.page-header--list) h1,
    .page-header--list:has(.page-header__search) h1,
    .page-header--list:not(:has(.page-header__search)) h1 {
        font-size: var(--font-size-lg);
        line-height: var(--line-height-heading);
        /* Basis 0 with `min-width: auto`: the title takes what the action
           icons leave and never shrinks below its longest WORD. The icons stay
           on its line (Gael, 2026-10-02); a bare 10rem floor let a word wider
           than that break mid-letters -- "communicati / on items" on
           /comms/all. Only when the longest word and the icons cannot share
           the line do the icons wrap. */
        flex: 1 1 0;
        min-width: auto;
        max-width: 100%;
        overflow-wrap: normal;
        word-break: normal;
        white-space: normal;
        overflow: visible;
    }

    .page-header__actions {
        order: 1;
        margin-left: auto;
        flex-wrap: wrap;
        justify-content: flex-end;
        gap: var(--space-2);
    }

    .page-header__search,
    .page-header__reset,
    .page-header__toggle {
        order: 2;
    }

    /* The header's dropdown menus open from their RIGHT edge on a phone. With
       the actions at the end of the title's line, a menu anchored on its left
       hung 180px past the screen even closed (Pico keeps it laid out) and the
       whole page scrolled sideways (/comms/all, 2026-10-02). */
    .page-header__actions details.dropdown > summary + ul {
        left: auto;
        right: 0;
    }

    /* 30ch is the search's desktop cap, i.e. the width it is designed to be
       read at; here it is the basis instead, so the search fills its row and
       page-header-fit.js counts anything narrower as squeezed -- which is what
       makes the view toggle drop its words before the search loses its own. */
    .page-header .page-header__search {
        flex: 1 1 30ch;
    }

    /* The scope ("- Green IO Munich 2026") rides under the title inside the
       height the 44px icons already give the row, so it costs nothing -- as
       long as it takes one line. It ellipsises rather than wraps; the phone's
       own bar names the edition in full directly above. `contain:
       inline-size` keeps its width out of the title's min-content, so a long
       scope cannot push the actions off the row. */
    .page-scope {
        display: block;
        contain: inline-size;
        font-size: var(--font-size-sm);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .page-footer__actions,
    .action-row {
        margin-left: 0;
        flex-wrap: wrap;
        width: 100%;
        justify-content: flex-start;
    }

    .table-actions .action-row__label {
        display: none;
    }

    /* The breadcrumb costs a row, and sometimes two once it wraps, to say what
       the page's own Back button already says -- and the browser's back
       gesture says it a third time. On a phone that row is the most expensive
       one on the screen (Gael, 2026-09-29: "why keeping the breadcrumbs?").
       It stays in the DOM for assistive tech and for anyone who lands here
       with a wide window; it just stops taking vertical space. */
    nav[aria-label="Breadcrumb"],
    .breadcrumb {
        display: none;
    }

    /* The context bar's clones are icons at every width (custom.css); here
       they tighten to leave the record's name the rest of the line. */
    .context-bar__actions {
        display: flex;
        flex-shrink: 0;
        gap: var(--space-1);
    }

    .context-bar__title {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* Corner toasts become full-width banners: a 400px card on a 412px screen
       is a full-width banner wearing a margin. */
    .toast-container {
        left: var(--space-3);
        right: var(--space-3);
        top: calc(var(--navbar-height) + var(--space-2));
    }

    .toast-container .toast {
        max-width: none;
    }

    /* ====================================================================
       3. LISTS — the table becomes a stack of cards
       ==================================================================== */

    /* WITHOUT the script: the table stays a table and scrolls inside its own
       box rather than pushing the page sideways. This is the floor, and it is
       what every table gets if mobile-table.js fails to run. */
    main.container table,
    .table-scroll {
        display: block;
        overflow-x: auto;
    }

    /* WITH the script: each row becomes a card, each cell a label/value pair.
       `td::before` reads the `data-label` mobile-table.js copied down from the
       matching `<th>` — CSS cannot read one element's text into another's
       `content`, which is the whole reason that script exists.

       EVERY table in the page body, not just the ones a `data_table` macro
       drew. The script labels them all, and scoping the CSS to two container
       classes left the others -- the dashboard's vouchers table among them --
       as real tables full of labelled cells, overflowing the phone by 47px.
       One rule for every table is also the honest one: a reader does not know
       which macro drew which list. */
    html[data-nav-js] main.container table {
        display: block;
        overflow-x: visible;
        border: 0;
    }

    /* `display: none` and not a visually-hidden clip: the column menus inside
       <thead> hold this table's filter inputs, and a `display: none` form
       control is still submitted with the form. So a filter set on a desktop
       session survives into a phone one — it just cannot be CHANGED here. */
    html[data-nav-js] main.container table thead {
        display: none;
    }

    html[data-nav-js] main.container table tbody {
        display: block;
    }

    /* EVERY SELECTOR BELOW STARTS WITH THE SAME PREFIX, and that is the point.
       The general cell rule is `html[data-nav-js] main.container table tbody
       td` (0,2,5); a refinement written without `main.container` scores
       (0,2,4) and loses to it however late it sits. Four refinements shipped
       that way on 2026-09-29 and none of them ever applied: empty fields still
       took a row, the checkbox sat indented in the value column, and the
       "short fields pair up" layout never rendered. A refinement that needs
       to win adds to this prefix; it never drops part of it.

       THE CARD (docs/rules/ui.md "Spend every row"). Line one is the record:
       its checkbox, its name, and its action icons, side by side -- the
       controls act on the record, so they sit on the line that names it. Every
       other field follows, one per line, label BESIDE value. mobile-table.js
       marks the three line-one cells; a table it cannot find a name in keeps
       its controls as full-width rows instead. */
    html[data-nav-js] main.container table tbody tr {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: 0 var(--space-3);
        align-content: start;
        background: var(--color-surface);
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        padding: var(--space-3);
        margin-bottom: var(--space-3);
    }

    html[data-nav-js] main.container table tbody td {
        grid-column: 1 / -1;
        display: grid;
        /* Label column sized in rem so it tracks the type scale rather than a
           guessed pixel width, and capped so a long label ("Onboarding
           status") wraps instead of squeezing its own value to nothing. */
        grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr);
        gap: var(--space-1) var(--space-3);
        align-items: baseline;
        width: auto;
        max-width: none;
        padding: var(--space-1) 0;
        border: 0;
        /* The table-wide one-line-and-ellipsis cap is a column-width policy.
           A card has no columns, so the value wraps instead of truncating —
           truncation on a phone hides the one thing the card exists to show. */
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
        /* A centred or right-aligned column is a table alignment: numbers line
           up down a column so they can be compared. A card has no column to
           line up with, so both the label and its value read from the start
           edge like every other line on the page. */
        text-align: start;
    }

    /* `text-align` on the label and value boxes themselves, not only on the
       cell: a column rule can be an id selector (`#comm-items-tbody
       .comm-mechanism` centres the comms Type column) and out-rank the cell
       rule above, and the two boxes would inherit its centring. An inherited
       value loses to any declaration made on the element itself. */
    html[data-nav-js] main.container table tbody td::before {
        content: attr(data-label);
        color: var(--color-text-tertiary);
        font-size: var(--font-size-sm);
        font-weight: 500;
        text-align: start;
    }

    /* A field with no value costs a row and tells the reader nothing. Marked
       by mobile-table.js, which only ever counts a cell empty when it holds
       no text beyond a placeholder dash AND nothing interactive. */
    html[data-nav-js] main.container table tbody td[data-mobile-empty] {
        display: none;
    }

    /* Everything on the value side lives in ONE box that mobile-table.js
       builds, so a status badge and the "?" that explains it stay on the same
       line instead of the "?" dropping to a row of its own. Inside it, content
       flows inline exactly as the template wrote it. */
    html[data-nav-js] main.container table tbody td > .mobile-cell__value {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;
        text-align: start;
        align-items: center;
        gap: var(--space-1) var(--space-2);
        min-width: 0;
    }

    /* A pill longer than the value column ("Onboarding in progress") wraps
       rather than widening the document past the phone. */
    html[data-nav-js] main.container table tbody td .badge {
        white-space: normal;
    }

    /* A cell whose column has no header text gets no label and lays its
       contents out in a row. */
    html[data-nav-js] main.container table tbody td[data-mobile-cell] {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
    }

    html[data-nav-js] main.container table tbody td[data-mobile-cell]::before {
        content: none;
    }

    /* -- Line one: checkbox | name | action icons ------------------------ */
    html[data-nav-js] main.container table tbody tr:has(> td[data-mobile-primary]) > td[data-mobile-cell="select"] {
        grid-column: 1;
        grid-row: 1;
        align-self: center;
        padding: 0;
    }

    html[data-nav-js] main.container table tbody tr:has(> td[data-mobile-primary]) > td[data-mobile-cell="actions"] {
        grid-column: 3;
        grid-row: 1;
        align-self: center;
        flex-wrap: nowrap;
        padding: 0;
    }

    /* The name is the card's title, so it drops the label that would say
       "Name" and takes the weight instead. It spans whichever of the two
       outer columns has no control in it. */
    html[data-nav-js] main.container table tbody td[data-mobile-primary] {
        grid-column: 1 / -1;
        grid-row: 1;
        display: block;
        align-self: center;
        font-weight: 600;
    }

    html[data-nav-js] main.container table tbody tr:has(> td[data-mobile-cell="select"]) > td[data-mobile-primary] {
        grid-column-start: 2;
    }

    html[data-nav-js] main.container table tbody tr:has(> td[data-mobile-cell="actions"]) > td[data-mobile-primary] {
        grid-column-end: 3;
    }

    html[data-nav-js] main.container table tbody td[data-mobile-primary]::before {
        content: none;
    }

    /* -- The fields FLOW, they do not stack (2026-10-02) ------------------ */
    /* One field per line made every card in every list four to seven lines
       of "Country | USA", "Pronouns | she" -- short facts each spending a
       row (Gael, 2026-10-02: "make sure to stack information on the same row
       when possible"). The card is now a wrapping flex row: line one is still
       the record (checkbox, name, icons), then a zero-height full-width
       break, then every other field as a caption over its value, side by
       side, as many to a line as fit. A long value (a session title) takes
       the line alone because it is wider than what is left. "Spend every
       row" allows a caption over a value exactly here: one line carries two
       or more facts. Desktop is untouched -- all of this is in the 768px
       block. */
    html[data-nav-js] main.container table tbody tr {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        /* No row gap: the zero-height break is a flex line of its own, and a
           gap on each side of it doubled the space under the name. The
           fields carry their own top margin instead. */
        gap: 0 var(--space-4);
    }

    html[data-nav-js] main.container table tbody tr::before {
        content: "";
        order: 1;
        flex-basis: 100%;
        height: 0;
    }

    html[data-nav-js] main.container table tbody td {
        order: 2;
        display: flex;
        flex-direction: column;
        flex: 0 1 auto;
        gap: 0;
        min-width: 0;
        max-width: 100%;
        padding: 0;
        margin-top: var(--space-2);
    }

    /* The name stretches to the line's height and centres its text in it,
       so the 44px icons beside it share its line rather than overhanging it. */
    html[data-nav-js] main.container table tbody td[data-mobile-primary] {
        order: 0;
        margin-top: 0;
        flex: 1 1 0;
        display: flex;
        align-items: center;
        align-self: stretch;
    }

    html[data-nav-js] main.container table tbody td[data-mobile-cell="select"],
    html[data-nav-js] main.container table tbody td[data-mobile-cell="actions"] {
        order: 0;
        flex: 0 0 auto;
        flex-direction: row;
        align-self: center;
        margin-top: 0;
    }

    /* -- The onboarding step list is not a record list ------------------- */
    /* It is a <table> for its desktop columns (state dot | step | control),
       but it has no <thead>, so mobile-table.js finds no label to copy down
       and the card rules above turned every step into a card with an empty
       label column beside three stacked lines -- the dot, the name, the tick
       (Gael, 2026-10-02, with a screenshot of half the screen blank). A step
       is ONE line on a phone too: dot, name, control. The capture form a step
       may carry takes the full width under it. The extra `.onboarding-step-
       table` and `.step-row` out-rank the card rules, including the
       empty-cell one that would otherwise hide the text-less state dot. */
    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        gap: 0 var(--space-2);
        background: transparent;
        border: 0;
        border-radius: 0;
        padding: var(--space-1) 0;
        margin: 0;
    }

    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row > td {
        grid-column: auto;
        display: block;
        padding: 0;
    }

    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row > td::before {
        content: none;
    }

    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row::before {
        content: none;
    }

    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row > td > .mobile-cell__value {
        display: inline;
    }

    html[data-nav-js] main.container table.onboarding-step-table tbody tr.step-row--inline-form > td {
        grid-column: 1 / -1;
        padding: 0 0 var(--space-2);
    }

    /* Every row action keeps the 44px target it needs on a touch screen. */
    html[data-nav-js] .table-actions {
        justify-content: flex-start;
        flex-wrap: nowrap;
        gap: var(--space-1);
    }

    html[data-nav-js] .table-action-btn {
        min-width: var(--target-size-min);
        min-height: var(--target-size-min);
    }

    /* The search + reset toolbar stacks; the search box is the widest thing on
       a list page and earns the full row. */
    .data-table-toolbar,
    .data-table-toolbar-right,
    .data-table-toolbar-extra {
        flex-direction: column;
        align-items: stretch;
        white-space: normal;
    }

    .list-search,
    .list-search input[type="search"],
    .page-header__search {
        width: 100%;
        max-width: none;
    }

    /* ====================================================================
       4. DETAIL PAGES
       ==================================================================== */

    /* The rail is already gone at this width (block further up). What is left
       is the content column, its cards, and the label/value lists inside them
       — all of which are still laid out for a 1200px column. */
    /* LABEL BESIDE VALUE, never above it (docs/rules/ui.md "Spend every
       row", Gael 2026-09-30: "how many fields have the label on one row and
       the content on the other"). The pairs/triples/quads variants step down
       to ONE pair per line -- not to one FIELD per line: a `dt` stacked over
       its `dd` spends two rows on one fact, and on the person page that was
       nineteen facts in thirty-eight rows. The label column fits the list's
       longest label up to 9.5rem ("Unsubscribed on FOST" is ~9rem at 14px):
       a list shares one grid, so every row still aligns, and a label that
       wraps would spend the row this rule exists to save. (A list CARD cannot
       do the same -- each cell is its own grid there -- so it keeps 7.5rem.) */
    .detail-list,
    .detail-list--pairs,
    .detail-list--triples,
    .detail-list--quads {
        grid-template-columns: fit-content(9.5rem) minmax(0, 1fr);
        gap: 0 var(--space-3);
    }

    /* A long value takes the value column, not a row of its own under its
       label. The desktop `span 2` would do exactly that with two tracks. */
    .detail-list dd.detail-list__dd--wide {
        grid-column: 2 / -1;
    }

    /* The voucher line FLOWS as text on a phone. As a wrapping flex row a
       long code (a 40-letter name-based one) could not shrink, so it took a
       line of its own and its "Not on FOST" badge a third. Inline, the code
       breaks where it must and the badge follows it. */
    .card .role-voucher {
        display: block;
    }

    /* Pico draws `code` as an inline-block, which cannot break across lines
       however `overflow-wrap` is set -- so a long code still took a line. */
    .card .role-voucher code {
        display: inline;
    }

    /* A card lid's small buttons keep their icon and lose their words on a
       phone: three labelled buttons beside "Drive folders" stacked into a lid
       three rows tall. Each keeps its name in `aria-label` and `title`. */
    .card-header .btn-mini__label {
        display: none;
    }

    /* A status stepper's labels take one line each on a phone. The 80px
       column floor broke "CFP Submitted" onto two, a row of half-words under
       every dot; each column is now at least as wide as its label, and the
       stepper already scrolls inside its own box when they do not all fit. */
    .state-machine-stepper {
        grid-template-columns: repeat(var(--steps, 7), minmax(max-content, 1fr));
    }

    .state-machine-step__label {
        white-space: nowrap;
    }

    /* Desktop-only blocks (Gael, 2026-10-02). The rule in docs/rules/ui.md
       "Responsive" is that nothing available on a desktop is hidden on a
       phone; these are its named exceptions, listed there. Each is a
       back-office act or reference nobody needs from a conference floor. */
    main.container .desktop-only {
        display: none;
    }

    /* Through the token, not a bare padding: `.card-header` bleeds back out of
       the card with `-1 * --panel-padding-sm`, so a card padded 12px around a
       lid pulled 16px stuck out 4px on every side and left a 4px band under a
       folded card's lid -- the "Submission information" bug (Gael,
       2026-10-02). */
    .card {
        --panel-padding-sm: var(--space-3);
    }

    .card,
    .card-padded {
        padding: var(--space-3);
    }

    /* Padding does not stack. A `.card-padded` body inside a `.card`, and a
       role's indented `.role-sub` inside that, took 48px off each side of the
       person page's contact lists -- a fifth of a 412px screen -- so every
       two-word label wrapped onto a second row. The outer card's padding is
       the one gutter; the nesting line (`.role-sub`'s border) stays. */
    .card .card-padded {
        padding-left: 0;
        padding-right: 0;
    }

    /* `.card` in front because record_card.css loads AFTER this file on the
       pages that use it, and would win a tie on source order. */
    .card .role-sub {
        margin-left: 0;
    }

    /* Under a card's lid, the lid's own bottom margin is the gap. The body's
       top padding and its first sub-block's top margin stacked two more on
       it: 36px of nothing above "Reachable at" (2026-10-02). */
    .card > .card-header + .card-padded {
        padding-top: 0;
    }

    .card .card-padded > .role-sub:first-child {
        margin-top: 0;
    }

    /* The photo sits beside the headline and the glyphs, not on three rows
       of its own above them: 64px, not 96px, and the text takes the rest of
       the line (2026-10-02). */
    .card .profile-top {
        flex-wrap: nowrap;
        gap: var(--space-3);
    }

    .card .profile-top__text {
        flex: 1 1 0;
    }

    .card .profile-photo .image-slot__frame {
        width: var(--space-16);
        height: var(--space-16);
    }

    /* A HELP "?" STAYS ON THE LINE IT EXPLAINS. A heading that carries a
       verdict badge and its "?" ("Contractual deliverables -- Differs from
       the Champion package ?"), and the state-off-stepper line ("Wrapped is
       where this edition is now ... ?"), were wrapping flex rows: a flex row
       breaks before any item that does not fit whole, so the long badge or
       sentence took a line and the 24px "?" was pushed onto one of its own --
       by 2px, one flex gap, on the contract heading. As inline text the
       pieces flow like a sentence, the gap is a word space, and the "?"
       follows what it explains. */
    .card .role-sub__title,
    .state-machine-overlay {
        display: block;
    }

    .card .role-sub__title > *,
    .state-machine-overlay > * {
        vertical-align: middle;
    }

    .state-machine-overlay > * + * {
        margin-left: var(--space-1);
    }

    /* The badge breaks between its words like the rest of the line, so a
       long verdict ("Differs from the Champion package") wraps instead of
       taking a line whole -- and the "?" follows its last word. `clone`
       repeats the pill's padding and rounding on each line it spans. */
    .card .role-sub__title > .badge,
    .state-machine-overlay > .badge {
        display: inline;
        white-space: normal;
        line-height: 2;
        -webkit-box-decoration-break: clone;
        box-decoration-break: clone;
    }

    /* Chart.js writes an explicit `width` attribute on its canvas, so the
       element ignores its container and the dashboard's pie chart pushed the
       page to 333px at a 320px viewport. Capping it lets the chart scale
       instead; `height: auto` keeps its aspect ratio while it does. */
    canvas {
        max-width: 100%;
        height: auto;
    }

    /* Wide content scrolls inside its own box rather than giving the page a
       horizontal scrollbar — the same rule the state-machine stepper already
       follows at this width. */
    .state-machine-intent__bar,
    .agenda-track,
    .agrid-layout,
    pre,
    .card-code {
        overflow-x: auto;
    }

    /* The card grids collapse: they hold cards and charts, and at 412px two
       of those side by side means two unreadable columns. */
    .grid-2,
    .grid-3,
    .grid-4 {
        grid-template-columns: minmax(0, 1fr);
    }

    /* The dashboard's number tiles are words and numbers, not cards: they
       pack as many to a row as fit at 8rem (two at 412px) instead of one per
       row, which spent three rows on three zeros. */
    .dashboard-stats {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
    }

    /* Form rows do NOT collapse to one field per line: they exist to pack
       SHORT fields (docs/rules/ui.md "Pack short fields into one row"), and
       two dates or two selects fit side by side at 412px. Each field gets at
       least 10rem; the row takes as many as fit -- two on a phone, one at the
       320px floor. */
    .form-row,
    .form-row-3,
    .form-row-4 {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
    }

    /* ====================================================================
       5. FORMS — reachable, not optimised
       ==================================================================== */

    .form-container,
    .form-container-wide,
    .form-mixed-width {
        max-width: none;
    }

    /* The desktop rule is "input width matches content" — an 8ch postal code
       field is 8ch wide. That inverts on a phone: a narrow field inside a
       narrow column reads as broken, and there is no second column for it to
       share a line with. */
    input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]),
    select,
    textarea {
        width: 100%;
        max-width: none;
        /* 16px is the floor at which Android Chrome and iOS Safari stop
           zooming the viewport on focus. Below it, tapping any field silently
           rescales the whole page and leaves it rescaled. */
        font-size: var(--font-size-base);
    }

    .form-inline {
        flex-direction: column;
        align-items: stretch;
    }

    /* Form footers: full-width stacked buttons, each an easy thumb target.
       A Save and a Cancel are the end of a task and deserve the room.

       Deliberately NOT every `.button-group`. The comms page header wraps its
       controls in one, and stacking those put four rows of full-width buttons
       above the page before it started (Gael, 2026-09-29: "the buttons could
       be grouped on 2 rows instead of 4"). A header toolbar goes icon-only when
       crowded (page-header-fit.js); only a group inside a form or a card footer
       stacks. */
    .form-container .button-group,
    .form-container .button-group-left,
    .form-container .button-group-right,
    .form-container .button-group--center,
    form > .button-group,
    form > .button-group-right,
    .page-footer__actions .button-group {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }

    .form-container .button-group > *,
    .form-container .button-group-right > *,
    form > .button-group > *,
    form > .button-group-right > * {
        width: 100%;
        min-height: var(--target-size-min);
    }

    /* A button group that is NOT a form footer -- a header toolbar -- wraps
       into as few rows as its icons need. */
    .page-header__actions .button-group {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        width: auto;
    }

    /* ====================================================================
       6. THE AGENDA GRID  (/agenda)
       ====================================================================
       This is the hardest screen in the app on a phone and the one that
       matters most during a conference. It is a spreadsheet: one COLUMN per
       track, one flex ROW per time segment, with the track names in a single
       sticky header above all of them. At 412px two tracks side by side give
       each 190px, which is narrower than the time range in its own gutter.

       WHAT THIS DOES INSTEAD, AND THE ONE THING IT GIVES UP
       Each segment's columns stack: you read the 09:00-10:40 block as Main
       Stage's talks then Workshop Room's talks, then the anchored break bar
       that closes the block, then the next block. Chronology by BLOCK is
       preserved, which is the granularity a conference day is actually run
       at; chronology ACROSS tracks inside one block is not, and cannot be —
       the rows are grouped by track in the DOM, and no stylesheet can
       interleave them.

       The alternative was one track per screen with a horizontal swipe. It
       does not work here: the track columns live in a separate flex row per
       segment, so swiping would scroll one block's tracks and leave every
       other block behind.

       The track name comes back on each stacked column from `data-track-name`
       (set in agenda/overview.html), because the header row that used to
       carry it is no longer above the column it names. */

    .agrid-layout {
        flex-direction: column;
        /* Stretch, not the desktop's flex-start: stacked, the grid must take
           the screen's width. Since each track column became a size container
           (custom.css, 2026-10-02), its content no longer widens it, and with a
           short programme the whole grid shrank to 280px of a 380px screen --
           enough to push every slot's length stepper off its time line. */
        align-items: stretch;
    }

    /* The unplaced-talks tray is a drag-and-drop surface — a desktop gesture
       this layout does not try to reproduce. It drops below the grid, stops
       being sticky and stops competing for the width. */
    .agrid-tray {
        width: 100%;
        position: static;
        max-height: 60vh;
    }

    /* Kept, not hidden: room, capacity and the MC are what you look up from
       the floor. But not STICKY — stacked, this row is several hundred pixels
       tall, and pinning it would leave the agenda itself a letterbox. */
    .agrid-headrow {
        position: static;
        flex-direction: column;
        gap: var(--space-2);
    }

    .agrid-headcell {
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        padding: var(--space-2);
    }

    .agrid-segrow {
        flex-direction: column;
        gap: var(--space-2);
    }

    .agrid-segcol {
        /* 17.5rem is a column minimum for a side-by-side grid; stacked, it is
           just a horizontal scrollbar waiting to happen on a 320px phone. */
        min-width: 0;
        border-top: 1px solid var(--color-border);
        border-radius: var(--radius-md);
    }

    .agrid-segcol::before {
        content: attr(data-track-name);
        display: block;
        margin-bottom: var(--space-1);
        color: var(--color-text-tertiary);
        font-size: var(--font-size-sm);
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.04em; /* css-tokens: allow — typographic tracking, not a spacing value */
    }

    /* A slot row is one line of 12px text on desktop because the grid's value
       is rows-per-screen. On a phone the value is READING one row, so it wraps
       into three bands and comes up to the 14px label floor:

         1. time and format, with the length stepper and the row's icons at
            the end of the same line -- they act on the slot, and the time is
            the slot (docs/rules/ui.md "Spend every row");
         2. the title;
         3. the speakers, with their status badges on the same line.

       It used to be five: the stepper and the badges each took a band of
       their own (Gael, 2026-10-02: "the session in the agenda could gain 2
       rows"). `order` moves the controls up without touching the desktop
       DOM order the grid's drag-and-drop reads. */
    .agrid-row {
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-1) var(--space-2);
        padding: var(--space-2);
        font-size: var(--font-size-sm);
    }

    .agrid-gut {
        flex: 1 1 auto;
        flex-wrap: wrap;
        white-space: normal;
        order: 0;
    }

    .agrid-row > .agrid-stepper {
        order: 1;
    }

    .agrid-row > .agrid-rowactions {
        order: 2;
    }

    .agrid-row > .agrid-bd {
        order: 3;
    }

    /* The row body is `flex-wrap: nowrap; overflow: hidden` on desktop, which
       is what makes a long title truncate rather than push the grid wider. At
       412px that same rule is what squeezed a four-speaker panel's title into
       a 60px ribbon of one word per line. Wrapping is the whole fix: title,
       speakers and status badges each take their own band. */
    .agrid-bd {
        flex: 1 1 100%;
        min-width: 0;
        flex-wrap: wrap;
        align-items: flex-start;
        overflow: visible;
    }

    .agrid-ti {
        flex: 1 1 100%;
        font-size: var(--font-size-base);
        /* The desktop two-line clamp exists to keep a 36px row height. There
           is no row height to protect here, and a clipped talk title is the
           one thing this screen must never do. */
        -webkit-line-clamp: none;
        line-clamp: none;
        display: block;
        overflow: visible;
    }

    .agrid-spk {
        /* 12px against a 14px row was a deliberate de-emphasis in a dense
           grid; at arm's length on a phone it is just small. The label floor
           in docs/rules/ui.md is 14px and this is a label. Sized to its names,
           not the whole line, so the status badges follow on the same line. */
        flex: 0 1 auto;
        font-size: var(--font-size-sm);
    }

    .agrid-rowactions {
        flex-wrap: wrap;
        margin-left: 0;
    }

    /* The anchored-break bar spans the day, so it spans the column. Its name
       field is `flex: 1; min-width: 0`, which on a 412px row collapsed
       "Morning coffee" to "Mo…" to make room for the time stepper beside it.
       Given its own band it keeps its name and the controls sit under it. */
    .agrid-anchorbar {
        margin: var(--space-3) 0;
        padding: var(--space-2) var(--space-3);
    }

    /* `.agrid-anchor-rename` is the break's NAME field (the anchor bar's own
       inline rename form); `.agrid-break-rename` is the same idea on a break
       ROW. Both are `flex: 1; min-width: 0` on desktop, and the section-5
       rule above gives every input `width: 100%` — so inside a form the flex
       row had already squeezed to nothing, the field rendered at 100% of
       nothing and the break lost its name entirely. Each gets its own band. */
    .agrid-anchor-rename,
    .agrid-break-rename {
        flex: 1 1 100%;
        min-width: 0;
    }

    .agrid-anchorbar .tm,
    .agrid-anchorbar .agrid-stepper,
    .agrid-anchorbar .agrid-stepper-sm {
        flex-shrink: 0;
    }

    .agrid-anchorbar .all {
        margin-left: 0;
    }

    /* The break's remove "x" sits on its name line, like a card's delete
       icon on its title line -- not alone on the bar's last line. */
    .agrid-anchorbar > * {
        order: 2;
    }

    /* The name takes the line, less the "x" beside it -- a basis of 0 let
       the time and steppers squeeze it to nothing on the same line. */
    .agrid-anchorbar > .agrid-anchor-rename,
    .agrid-anchorbar > .agrid-anchor-name {
        flex: 1 1 calc(100% - var(--target-size-min) - var(--space-2));
        order: 0;
    }

    .agrid-anchorbar > form.table-action-form {
        order: 1;
    }

    .agrid-anchor-host {
        display: flex;
        width: 100%;
    }

    /* A filter chip row (the `chip_row` macro) is ONE line on a phone that
       scrolls inside its own box: wrapped, the production board's eight
       filters took three rows above the board (2026-10-02). Scrolling sideways
       inside a strip is allowed (docs/rules/ui.md "Responsive": wide content
       scrolls in its own box, never the page). */
    main.container nav.chip-row {
        display: flex;
        max-width: 100%;
        /* A flex item will not shrink below its content without this, and in a
           wrapping control row (the comms list's lens chips) the strip then
           widened the page instead of scrolling. */
        min-width: 0;
        flex: 1 1 100%;
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
    }

    main.container nav.chip-row > * {
        flex: 0 0 auto;
    }

    /* The comms list wraps its chip rows in a plain div inside a flex row;
       that div needs the same permission to shrink. */
    .comm-list-controls > * {
        min-width: 0;
        max-width: 100%;
    }

    /* The programme template folds behind its title on a phone (the toggle
       and its state live in agenda/overview.html). Folded, it is one line;
       opened, it is the toolbar below. */
    .agrid-toolbar .agrid-toolbar__toggle {
        display: inline-flex;
    }

    .agrid-toolbar__head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-2);
    }

    .agrid-toolbar:not(.is-open) > .agrid-toolgrp {
        display: none;
    }

    .agrid-toolbar.is-open .agrid-toolbar__toggle :is(i, svg) {
        transform: rotate(180deg);
    }

    /* One track SHOWN: its name is already on the track card above, so the
       label each stacked block repeats ("MAIN STAGE") is a row of nothing.
       With several tracks it is what says which column you are reading. The
       track filter hides a column with `.hidden`, not by removing it, so
       "one shown" is "no two columns without `.hidden`". */
    .agrid-segrow:not(:has(> .agrid-segcol:not(.hidden) ~ .agrid-segcol:not(.hidden))) > .agrid-segcol::before {
        content: none;
    }

    /* The edition's dates go to the next line rather than off the screen. */
    .card .edition-bar {
        flex-wrap: wrap;
    }

    /* The archived-edition picker sizes itself to its longest option ("Green
       IO London 2025 -- 18 Sep 2025 - 19 Sep 2025"), wider than a phone's
       card: it scrolled an attendee's whole page sideways (production probe,
       2026-10-05). On a phone it takes its own row, and never more. */
    .card .edition-jump {
        flex: 1 1 100%;
        min-width: 0;
    }

    .card .edition-jump select {
        min-width: 0;
        width: 100%;
    }

    /* The day tabs keep the date, which is what a phone reader looks for,
       and drop "Day 1": with it the tabs and the track filter took two rows. */
    .agrid-daytab__label {
        display: none;
    }

    /* The grid's own toolbar (day tabs, track filter, template controls).
       The TOOLBAR stacks its groups; a GROUP does not stack its label over its
       control (docs/rules/ui.md "Spend every row"). "Day starts" is two words
       and its stepper is 120px -- one line, with the help sentence wrapping
       to the next only because it is a sentence. */
    .agrid-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3);
    }

    .agrid-toolgrp {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
    }

    /* The new-break form's fields join the group's row, so the label shares a
       line with the name field and the time + Add follow as far as they fit.
       `display: contents` drops only the form's BOX: it is still the form its
       fields and button submit, and htmx still hears its submit event. */
    .agrid-toolgrp .agrid-anchor-add {
        display: contents;
    }

    /* (0,3,1) to out-rank the phone's full-width input rule in section 5. */
    .agrid-toolgrp .agrid-anchor-add input[type="text"] {
        flex: 1 1 6rem;
        width: auto;
        min-width: 0;
    }

    .agrid-toolgrp .agrid-anchor-add input[type="time"] {
        flex: 0 1 8rem;
        width: auto;
        min-width: 0;
    }

    .agrid-toolgrp .agrid-anchor-add button {
        white-space: nowrap;
    }

    /* A flex or grid child defaults to `min-width: auto`, which means it
       cannot shrink below its own content -- so a toolbar stretched to the
       page width still grew past it, carrying the day tabs' full text with it.
       Every direct child gets the `0` floor, which is what lets the
       `overflow-x` below actually take effect instead of being unreachable. */
    .agrid-toolbar > *,
    .agrid-toolgrp > * {
        min-width: 0;
    }

    /* Two or three day tabs stay side by side — they are the page's primary
       switch, and stacked they read as a menu rather than a segment. Long
       labels ("Day 2 Fri 12 Jun") scroll inside the control. */
    .agrid-daytabs {
        flex-direction: row;
        overflow-x: auto;
        min-width: 0;
        max-width: 100%;
    }

    /* The sync-status footer is a row of "<light> <name> — <explanation>"
       items, and the explanation is a 193px run of text that does not wrap on
       a desktop because it never has to. Wrap the row AND the item. */
    .agenda-sync-lights {
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .agenda-sync-lights > * {
        min-width: 0;
        max-width: 100%;
    }

    .sync-light-detail {
        white-space: normal;
        min-width: 0;
    }

    .agrid-daytabs a {
        white-space: nowrap;
        min-height: var(--target-size-min);
        display: inline-flex;
        align-items: center;
        /* The template writes `{{ d.label }} <span>{{ date }}</span>` and relies
           on that literal space. A flex container DROPS a whitespace-only text
           node between two children, so making this anchor inline-flex -- which
           is what buys the 44px target -- rendered "Day 1Thu 11 Jun". The gap
           puts the space back as layout rather than as content. */
        gap: var(--space-1);
    }

    /* The Tracks opener beside them is the one way to change track on a
       phone, and it kept the desktop's 36px (place-talk journey, 2026-10-01). */
    .agrid-trackdd > button,
    /* ...and "Drop a talk here · or click to pick" is the only way to place a
       talk on a phone (the drag is a desktop gesture): it was 26px tall, and so
       are the ✓ and the select of the picker it opens. */
    .agrid-pick-btn,
    .agrid-pickform select,
    .agrid-pickform button {
        min-height: var(--target-size-min);
    }

    .agrid-pickform button {
        min-width: var(--target-size-min);
    }

    /* ====================================================================
       7. DIALOGS
       ==================================================================== */

    .confirm-dialog article,
    dialog article {
        width: 100%;
        max-width: none;
        margin: 0;
        border-radius: var(--radius-md);
    }

    .confirm-dialog footer.button-group-right {
        flex-direction: column-reverse;
    }

    /* The @-link picker's suggestions are tapped, not clicked: each keeps the
       44px target a finger needs. */
    .mention-picker__option {
        min-height: var(--target-size-min);
        align-items: center;
    }
}
