/* Class names mirror PHP functions: ui_button() -> .ui-button, layout_page_start() -> .layout-*
   Color/type/spacing tokens ported from ~/Develop/Python ui/1_theme.py (Material You, seed #63A002)
   — keep both in sync if the seed or scale changes there.

   M3 window size classes (breakpoints) — no differing content yet, all sizes render the same;
   this is the single source of truth to use once a screen actually needs to differ:
     compact      < 600px   — phone portrait
     medium     600–839px   — tablet/foldable portrait
     expanded   840–1199px  — phone/tablet/foldable landscape
     large     1200–1599px  — desktop
     extra-large 1600px+    — desktop, ultra-wide
   CSS custom properties can't be used inside @media conditions, so these thresholds are documented
   here and must be hardcoded again in any @media (min-width: ...) rule that uses them. */

:root {
    /* Material You color roles — light scheme (default) */
    --md-primary: #4C662B;
    --md-on-primary: #FFFFFF;
    --md-primary-container: #CDEDA3;
    --md-on-primary-container: #354E16;
    --md-secondary: #586249;
    --md-on-secondary: #FFFFFF;
    --md-secondary-container: #DCE7C8;
    --md-on-secondary-container: #404A33;
    --md-tertiary: #386663;
    --md-on-tertiary: #FFFFFF;
    --md-tertiary-container: #BCECE7;
    --md-on-tertiary-container: #1F4E4B;
    --md-error: #BA1A1A;
    --md-on-error: #FFFFFF;
    --md-error-container: #FFDAD6;
    --md-on-error-container: #93000A;
    --md-background: #F9FAEF;
    --md-on-background: #1A1C16;
    --md-surface: #F9FAEF;
    --md-on-surface: #1A1C16;
    --md-surface-variant: #E1E4D5;
    --md-on-surface-variant: #44483D;
    --md-surface-container-lowest: #FFFFFF;
    --md-surface-container-low: #F3F4E9;
    --md-surface-container: #EEEFE3;
    --md-surface-container-high: #E8E9DE;
    --md-surface-container-highest: #E2E3D8;
    --md-outline: #75796C;
    --md-outline-variant: #C5C8BA;
    --md-inverse-surface: #2F312A;
    --md-inverse-on-surface: #F1F2E6;
    --md-inverse-primary: #B1D18A;

    /* Extended colors (Material Theme Builder), named by color: `node bin/theme/from-seed.mjs --extended '#63A002'`; an app's theme block has its own. */
    --md-red: #B42800;
    --md-on-red: #FFFFFF;
    --md-red-container: #FFDAD2;
    --md-on-red-container: #8A1D00;
    --md-yellow: #795900;
    --md-on-yellow: #FFFFFF;
    --md-yellow-container: #FFDFA0;
    --md-on-yellow-container: #5C4300;
    --md-green: #286C07;
    --md-on-green: #FFFFFF;
    --md-green-container: #AAF685;
    --md-on-green-container: #1A5200;

    /* M3 typescale (line heights: M3 spec, not in ~/Develop/Python ui/1_theme.py) */
    --md-display-large-size: 57px;   --md-display-large-weight: 400; --md-display-large-line-height: 64px;
    --md-display-medium-size: 45px;  --md-display-medium-weight: 400; --md-display-medium-line-height: 52px;
    --md-display-small-size: 36px;   --md-display-small-weight: 400; --md-display-small-line-height: 44px;
    --md-headline-large-size: 32px;  --md-headline-large-weight: 400; --md-headline-large-line-height: 40px;
    --md-headline-medium-size: 28px; --md-headline-medium-weight: 400; --md-headline-medium-line-height: 36px;
    --md-headline-small-size: 24px;  --md-headline-small-weight: 400; --md-headline-small-line-height: 32px;
    --md-title-large-size: 22px;     --md-title-large-weight: 400; --md-title-large-line-height: 28px;
    --md-title-medium-size: 16px;    --md-title-medium-weight: 500; --md-title-medium-line-height: 24px;
    --md-title-small-size: 14px;     --md-title-small-weight: 500; --md-title-small-line-height: 20px;
    --md-body-large-size: 16px;      --md-body-large-weight: 400; --md-body-large-line-height: 24px;
    --md-body-medium-size: 14px;     --md-body-medium-weight: 400; --md-body-medium-line-height: 20px;
    --md-body-small-size: 12px;      --md-body-small-weight: 400; --md-body-small-line-height: 16px;
    --md-label-large-size: 14px;     --md-label-large-weight: 500; --md-label-large-line-height: 20px;
    --md-label-medium-size: 12px;    --md-label-medium-weight: 500; --md-label-medium-line-height: 16px;
    --md-label-small-size: 11px;     --md-label-small-weight: 500; --md-label-small-line-height: 16px;
    /* Something new (an unread letter): text and icon 100 above their normal weight, text 400 → 500, icon 200 → 300 (user decision, 2026-11-01) */
    --md-emphasis-weight: 500;
    --md-icon-emphasis-weight: 300;

    /* Spacing scale */
    --spacing-xxxsmall: 2px;
    --spacing-xxsmall: 4px;
    --spacing-xsmall: 8px;
    --spacing-small: 12px;
    --spacing-medium: 16px;
    --spacing-large: 24px;
    --spacing-xlarge: 32px;
    --spacing-xxlarge: 40px;
    --spacing-xxxlarge: 48px;

    /* Content pane width — two separate tokens (not one shared value) so an app can override just its centered
       column or just its grid without the other following along. Same default today: 840px, Medium/Expanded's
       own M3 boundary (above) — the point where a card grid and a column of body text both still read fine. */
    --layout-centered-max-width: 840px;
    --layout-grid-max-width: 840px;
    /* A fixed pane's starting width (.claude/rules/navigation-sizes.md "Panes"): 360 − 2 × 16px padding = 328px = two tracker buttons (2 × 160 + 8); M3 uses 360dp too. */
    --layout-pane-fixed-360: 360px;
    /* The nav rail's column (from 600px); the panes, the corner FAB and the content cap's breakpoint follow it. */
    --layout-nav-rail-width: 96px;

    --radius: 6px;
    --font: system-ui, sans-serif;
}

@media (prefers-color-scheme: dark) {
    :root {
        --md-primary: #B1D18A;
        --md-on-primary: #1F3701;
        --md-primary-container: #354E16;
        --md-on-primary-container: #CDEDA3;
        --md-secondary: #BFCBAD;
        --md-on-secondary: #2A331E;
        --md-secondary-container: #404A33;
        --md-on-secondary-container: #DCE7C8;
        --md-tertiary: #A0D0CB;
        --md-on-tertiary: #003735;
        --md-tertiary-container: #1F4E4B;
        --md-on-tertiary-container: #BCECE7;
        --md-error: #FFB4AB;
        --md-on-error: #690005;
        --md-error-container: #93000A;
        --md-on-error-container: #FFDAD6;
        --md-background: #12140E;
        --md-on-background: #E2E3D8;
        --md-surface: #12140E;
        --md-on-surface: #E2E3D8;
        --md-surface-variant: #44483D;
        --md-on-surface-variant: #C5C8BA;
        --md-surface-container-lowest: #0C0F09;
        --md-surface-container-low: #1A1C16;
        --md-surface-container: #1E201A;
        --md-surface-container-high: #282B24;
        --md-surface-container-highest: #33362E;
        --md-outline: #8F9285;
        --md-outline-variant: #44483D;
        --md-inverse-surface: #E2E3D8;
        --md-inverse-on-surface: #2F312A;
        --md-inverse-primary: #4C662B;
        --md-red: #FFB4A2;
        --md-on-red: #621100;
        --md-red-container: #8A1D00;
        --md-on-red-container: #FFDAD2;
        --md-yellow: #F8BD2A;
        --md-on-yellow: #402D00;
        --md-yellow-container: #5C4300;
        --md-on-yellow-container: #FFDFA0;
        --md-green: #8FD96C;
        --md-on-green: #0F3900;
        --md-green-container: #1A5200;
        --md-on-green-container: #AAF685;
    }
}

* { box-sizing: border-box; }

/* Mobile-native feel: no iOS auto-zoom on inputs, no rubber-band past page edges, no gray tap flash / 300ms tap delay */
/* No page bounce when content fits: only a [data-pane-scroll] box that really scrolls rubber-bands, like a native scroll view. */
html { -webkit-text-size-adjust: 100%; overscroll-behavior: none; }
/* The part of the screen behind the browser's own bars (iOS 26 Safari's glass tab bar, 0 on the home screen): the compact pane reaches under it
   like a native scroll view, and its content keeps clear of it (.claude/rules/scroll-elevation.md). The pane is taller than the page then, so the page itself must not scroll. */
:root { --layout-behind-bars: calc(100lvh - 100svh); overflow: hidden; }
/* With the keyboard open the pane reaches under it too, and Safari's pill above it (Bottom tabs) no longer leaves an empty band. */
:root[data-keyboard-open] { --layout-behind-bars: var(--layout-keyboard-inset, 0px); }
@media (min-width: 600px) { :root, :root[data-keyboard-open] { --layout-behind-bars: 0px; } }
body {
    margin: 0;
    font-family: var(--font);
    color: var(--md-on-background);
    background: var(--md-background);
}
a, button {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

/* Screen transitions (cross-document view transitions; Safari 18.2+, Chrome 126+, instant elsewhere). app.js sets the kind
   on <html data-transition> from the page's layout_nav_stack() outlets, which renames the first pane — see .claude/rules/transitions.md:
   app-forward/app-back = shared axis slide, app-state = same state (e.g. an answer picked). Only the pane moves; the rest
   (root: background, nav rail) stands still, so root never animates — it would also block taps until it ended. */
@view-transition {
    navigation: auto;
}
[data-transition="forward"] .layout-panes { view-transition-name: app-forward; }
[data-transition="back"] .layout-panes { view-transition-name: app-back; }
[data-transition="state"] .layout-panes { view-transition-name: app-state; }
/* A screen's script presents a pane that started hidden (app.js, same-document): it slides in from its side, and the first pane eases into its new size. */
[data-transition^="present"] [data-pane="first"] { view-transition-name: app-pane-first; }
[data-transition^="present"] [data-pane="supporting"] { view-transition-name: app-pane-supporting; }
::view-transition-group(app-pane-first), ::view-transition-group(app-pane-supporting) { animation-duration: 0.3s; animation-timing-function: ease-out; }
::view-transition-old(app-pane-first), ::view-transition-new(app-pane-first) { height: 100%; object-fit: cover; object-position: left top; animation-duration: 0.3s; }
[data-transition="present-beside"]::view-transition-new(app-pane-supporting) { animation: app-slide-in-right 0.3s ease-out; }
[data-transition="present-below"]::view-transition-new(app-pane-supporting) { animation: app-slide-in-up 0.3s ease-out; }
/* A pane presented on top of joined panes slides down from under the first pane's bar: the bar stands still, the pane's group clips its slide. */
[data-transition="present-above"] [data-pane="first"] > .app-bar { view-transition-name: app-pane-bar; }
::view-transition-group(app-pane-bar), ::view-transition-old(app-pane-bar), ::view-transition-new(app-pane-bar) { animation: none; }
[data-transition="present-above"]::view-transition-group(app-pane-supporting) { overflow: clip; }
[data-transition="present-above"]::view-transition-new(app-pane-supporting) { animation: app-slide-in-down 0.3s ease-out; }
::view-transition-group(root), ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
::view-transition-old(app-forward), ::view-transition-old(app-back),
::view-transition-new(app-forward), ::view-transition-new(app-back) {
    animation-duration: 0.3s;
    animation-timing-function: ease-out;
}
::view-transition-old(app-forward) { animation-name: app-slide-out-left; }
::view-transition-new(app-forward) { animation-name: app-slide-in-right; }
::view-transition-old(app-back) { animation-name: app-slide-out-right; }
::view-transition-new(app-back) { animation-name: app-slide-in-left; }
/* Swap instantly — a whole-page animation here read as lag. Give these an animation-name to animate state changes. */
::view-transition-group(app-state), ::view-transition-old(app-state), ::view-transition-new(app-state) { animation: none; }
@keyframes app-slide-out-left { to { transform: translateX(-100%); } }
@keyframes app-slide-in-right { from { transform: translateX(100%); } }
@keyframes app-slide-out-right { to { transform: translateX(100%); } }
@keyframes app-slide-in-left { from { transform: translateX(-100%); } }
@keyframes app-slide-in-up { from { transform: translateY(100%); } }
@keyframes app-slide-in-down { from { transform: translateY(-100%); } }

/* layout_app_bar() — M3 Small Top App Bar, ported from ~/Develop/Python ui/1_app_bar.py; menu icon opens layout_nav_drawer(), title always centered.
   Pinned to the top of its pane (like a native nav bar); on surface at rest, tinted by app.js once content scrolls under it (--scrolled).
   Left/right padding add safe-area-inset so the icon/title never sit under a landscape notch or rounded corner. */
/* Fixed itself (same spot: the compact pane fills the viewport): iOS 26 Safari tints its status-bar band only from a fixed edge element's own background (.claude/rules/scroll-elevation.md). */
.app-bar {
    position: fixed;
    top: var(--layout-keyboard-top, 0px);
    left: 0;
    right: 0;
    /* --layout-bar-strip: the height of a bar's strip under its title row (layout_app_bar($stripHtml)), measured by app.js; 0 without one. */
    height: calc(64px + env(safe-area-inset-top) + var(--layout-bar-strip, 0px));
    padding: env(safe-area-inset-top) calc(12px + env(safe-area-inset-right)) 0 env(safe-area-inset-left);
    display: flex;
    align-items: center;
    gap: 0;
    box-sizing: border-box;
    background: var(--md-surface);
    transition: background-color 0.2s ease-out;
    z-index: 10;
}
/* A later pane's own bar (beside the first pane) sits at its pane's top, never across the viewport. */
.layout-pane + .layout-pane > .app-bar { position: absolute; }
/* Scroll elevation, ported from ~/Develop/Python ui/1_layout.py attach_scroll_elevation() (.bar-elevated there). */
.app-bar--scrolled { background: var(--md-surface-container-low); }
/* layout_app_bar($stripHtml): a strip under the title row, part of the bar (sticky with it, tinted with it). The bar's bottom padding is the strip's measured height,
   so the title row keeps its 64px content box and stays centered; the strip stands at the bar's bottom, 16px from its sides (2026-10-29). */
.app-bar--with-strip { padding-bottom: var(--layout-bar-strip, 0px); }
.app-bar__strip:has(> [hidden]:only-child) { display: none; }
.app-bar__strip { position: absolute; left: 0; right: 0; bottom: 0; padding: 0 calc(var(--spacing-medium) + env(safe-area-inset-right)) var(--spacing-small) calc(var(--spacing-medium) + env(safe-area-inset-left)); }
.app-bar__icon-btn {
    width: 48px;
    height: 48px;
    flex-shrink: 0;
    margin-left: var(--spacing-xsmall);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: var(--md-on-surface-variant);
    z-index: 1;
}
/* Trailing account icon (opens layout_profile_drawer()) — pushed to the right edge; the bar's own padding-right already holds the 12px + safe-area. */
.app-bar__icon-btn--end {
    margin-left: auto;
    width: auto;
    min-width: 48px;
}
/* 56px inset ported as-is from the source's title_left constant — an 8px gap after the 48px icon, same in Python; extended by the safe-area on each side. */
.app-bar__title {
    position: absolute;
    left: 0;
    right: 0;
    margin: 0;
    padding: 0 calc(56px + env(safe-area-inset-right)) 0 calc(56px + env(safe-area-inset-left));
    text-align: center;
    font-size: var(--md-title-large-size);
    font-weight: var(--md-title-large-weight);
    color: var(--md-on-surface);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* layout_app_bar(actionCount:): 2 or 3 trailing 40px icon buttons 4px apart; the title keeps the same clearance on both sides, so it stays centered. */
:is(.app-bar--actions-2, .app-bar--actions-3) .app-bar__icon-btn--end { gap: var(--spacing-xxsmall); }
.app-bar--actions-2 .app-bar__title { padding-inline: calc(12px + 2 * 40px + var(--spacing-xxsmall) + max(env(safe-area-inset-left), env(safe-area-inset-right))); }
.app-bar--actions-3 .app-bar__title { padding-inline: calc(12px + 3 * 40px + 2 * var(--spacing-xxsmall) + max(env(safe-area-inset-left), env(safe-area-inset-right))); }
/* Offline (public/_assets/offline.js sets html[data-offline]): the bar turns tertiary container, its text and icons on-tertiary-container; the scroll tint pauses. */
html[data-offline] .app-bar, html[data-offline] .app-bar--scrolled { background: var(--md-tertiary-container); }
html[data-offline] .app-bar :is(.app-bar__icon-btn, .app-bar__title, .app-bar__subtitle, .ui-icon-button--standard) { color: var(--md-on-tertiary-container); }
/* layout_offline_html(): the dialog's "Last connection" line under its text. */
.ui-dialog__body [data-offline-last] { margin-top: var(--spacing-xsmall); }
/* layout_app_bar_offline_button(): only while offline.js marks the page offline; the title then clears two leading slots (8 + 48 + 8 + 48), unless three trailing icons need more. */
.app-bar__offline { display: none; }
html[data-offline] .app-bar__offline { display: flex; }
html[data-offline] .app-bar:not(.app-bar--actions-3) .app-bar__title { padding-inline: calc(2 * var(--spacing-xsmall) + 2 * 48px + max(env(safe-area-inset-left), env(safe-area-inset-right))); }
/* layout_app_bar($titleSuffix): only the title text shrinks with an ellipsis, the suffix ("(3/20)") always stays whole. */
.app-bar__title--suffixed {
    display: flex;
    justify-content: center;
    column-gap: var(--spacing-xxsmall);
}
.app-bar__title-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.app-bar__title-suffix { flex: none; }
/* layout_app_bar($subtitle): title line (28px) + subtitle line (16px) = 44px, centered in the 64px bar like M3's small app bar with a subtitle. */
.app-bar__title--subtitled { line-height: var(--md-title-large-line-height); }
.app-bar__title-line, .app-bar__subtitle {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
}
.app-bar__title-line.app-bar__title--suffixed { display: flex; }
.app-bar__subtitle {
    font-size: var(--md-label-medium-size);
    font-weight: var(--md-label-medium-weight);
    line-height: var(--md-label-medium-line-height);
    color: var(--md-on-surface-variant);
}
/* layout_app_bar_late() icons (app.js marks their slot): instant on a fast page, fade in with the content after a loading screen (.claude/rules/navigation.md). */
.layout-was-loading .app-bar [data-app-bar-late] > * { animation: layout-appear 0.2s ease-out both; }

/* layout_nav_drawer() — M3 modal nav drawer: scrim + a left-edge panel, both driven by the drawer's own aria-hidden
   (app.js only flips the attribute, same pattern as ui_fab()'s aria-disabled). No Python source to port from —
   the Python sidebar (ui/1_nav.py) is a permanently visible desktop rail, not a mobile modal overlay. */
.ui-nav-drawer {
    position: fixed;
    inset: 0;
    z-index: 20;
    visibility: hidden;
    /* visibility itself can't fade/slide, but a transitioned discrete property only flips at the END of the
       transition when animating toward "hidden" (and immediately when animating toward "visible", CSS spec) —
       so closing keeps the scrim/panel visible for their own 200/250ms transitions instead of vanishing at once. */
    transition: visibility 250ms;
}
.ui-nav-drawer[aria-hidden="false"] { visibility: visible; }
.ui-nav-drawer__scrim {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    opacity: 0;
    transition: opacity 200ms ease;
}
.ui-nav-drawer[aria-hidden="false"] .ui-nav-drawer__scrim { opacity: 1; }
.ui-nav-drawer__panel {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(320px, 85vw);
    box-sizing: border-box;
    background: var(--md-surface-container-low);
    /* M3 modal drawer shape: flush against the screen's left/top/bottom edges, rounded only on the trailing edge. */
    border-radius: 0 16px 16px 0;
    padding: calc(var(--spacing-small) + env(safe-area-inset-top)) var(--spacing-small) calc(var(--spacing-small) + env(safe-area-inset-bottom)) calc(var(--spacing-small) + env(safe-area-inset-left));
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 250ms cubic-bezier(0.2, 0, 0, 1);
}
.ui-nav-drawer[aria-hidden="false"] .ui-nav-drawer__panel { transform: translateX(0); }
/* layout_profile_drawer() — the same drawer mirrored onto the right edge: rounded on its leading edge, right safe-area inset. */
.ui-nav-drawer--end .ui-nav-drawer__panel {
    left: auto;
    right: 0;
    border-radius: 16px 0 0 16px;
    padding-left: var(--spacing-small);
    padding-right: calc(var(--spacing-small) + env(safe-area-inset-right));
    transform: translateX(100%);
}
/* layout_end_drawer(): its content below the header; a ui_list_group() fills the panel's width. */
.ui-nav-drawer__body { display: flex; flex-direction: column; gap: var(--spacing-small); }
/* Loading/empty/error start at the same height as a page's own: app bar (64px, .layout-main has no top padding below it), minus the panel's own. */
.ui-nav-drawer__state:not([hidden]) { padding-top: calc(64px - var(--spacing-small)); }
.ui-nav-drawer__header {
    display: flex;
    align-items: center;
    height: 56px;
    flex-shrink: 0;
    margin: 0;
    padding: 0 var(--spacing-medium);
    color: var(--md-on-surface-variant);
    font-size: var(--md-title-small-size);
    font-weight: var(--md-title-small-weight);
}
.ui-nav-drawer__divider {
    flex-shrink: 0;
    margin: var(--spacing-small) var(--spacing-medium);
    border: none;
    border-top: 1px solid var(--md-outline-variant);
}
.ui-nav-drawer__item {
    display: flex;
    align-items: center;
    gap: var(--spacing-medium);
    height: 56px;
    flex-shrink: 0;
    padding: 0 var(--spacing-medium);
    border-radius: 28px;
    color: var(--md-on-surface-variant);
    text-decoration: none;
    font-size: var(--md-label-large-size);
    font-weight: var(--md-label-large-weight);
}
@media (hover: hover) { .ui-nav-drawer__item:hover { background: color-mix(in srgb, var(--md-on-surface) 8%, transparent); } }
.ui-nav-drawer__item--active {
    background: var(--md-secondary-container);
    color: var(--md-on-secondary-container);
}
@media (hover: hover) { .ui-nav-drawer__item--active:hover { background: color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container)); } }
/* M3 state layers, the content color over the container: hover 8%, pressed 10% (a tap: no hover). With a mouse, pressed sits on top
   of the hover layer, as in Figma (8% + 10%). iOS applies :active because app.js listens to touchstart. */
.ui-nav-drawer__item:active { background: color-mix(in srgb, var(--md-on-surface) 10%, transparent); }
@media (hover: hover) { .ui-nav-drawer__item:active { background: color-mix(in srgb, var(--md-on-surface) 10%, color-mix(in srgb, var(--md-on-surface) 8%, transparent)); } }
.ui-nav-drawer__item--active:active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, var(--md-secondary-container)); }
@media (hover: hover) { .ui-nav-drawer__item--active:active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
/* layout_page_start() — the panes' area (.layout-panes) and the first pane (.claude/rules/navigation.md): app bar + scroll region, the one element a screen transition moves.
   Compact: the whole viewport. Absolute, not fixed (body never scrolls, same spot): a full-width fixed element at the bottom edge
   makes iOS 26 Safari paint a solid band under its toolbar (.claude/rules/scroll-elevation.md). The generated panes block arranges the panes in it. */
.layout-panes {
    position: absolute;
    inset: 0;
    /* Set by app.js while the on-screen keyboard is open (.claude/rules/keyboard.md). */
    top: var(--layout-keyboard-top, 0px);
    bottom: calc(var(--layout-keyboard-inset, 0px) - var(--layout-behind-bars));
    /* The first pane's bar: the room a slot on top of joined panes leaves above itself (generated panes block). */
    --layout-app-bar-height: calc(64px + env(safe-area-inset-top) + var(--layout-bar-strip, 0px));
}
/* Each pane's own bar strip; app.js sets the measured height on the pane that has one (the first pane's also on the panes, for --layout-app-bar-height). */
.layout-pane { position: relative; --layout-bar-strip: 0px; }
.layout-pane[hidden] { display: none; }
/* Scrollable content region below the pane's app bar; this is what actually scrolls, not body. */
.layout-scroll {
    position: absolute;
    /* A variable, because a hugging pane (the generated panes block) puts the box in flow and keeps the bar's room as its margin. */
    --layout-scroll-top: calc(64px + env(safe-area-inset-top) + var(--layout-bar-strip, 0px));
    top: var(--layout-scroll-top);
    left: 0;
    right: 0;
    bottom: 0;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    background: var(--md-surface);
    transition: background-color 0.2s ease-out;
    padding-bottom: env(safe-area-inset-bottom);
}
/* The keyboard covers the home indicator, so its inset would only be a gap above the keyboard (app.js sets the attribute, .claude/rules/keyboard.md). */
:root[data-keyboard-open] .layout-scroll { padding-bottom: 0; }
/* Every box that scrolls a screen's content (layout_page_start()/layout_page_end() mark it; .claude/rules/pane-layout.md) reaches under the browser's bars:
   the spacer at its end lets the last content scroll clear of them, and a focused field stops above them. A spacer, not padding, because a flex scroll box may drop its end padding. */
[data-pane-scroll] { scroll-padding-bottom: var(--layout-behind-bars); }
[data-pane-scroll]::after { content: ''; display: block; flex: 0 0 auto; height: var(--layout-behind-bars); }
.ui-sheet-open [data-pane-scroll] { overflow: hidden; }
/* Desktop scrollbar (.claude/rules/design.md): a mouse/trackpad gets app.js's overlay thumb instead of the native bars; touch keeps its own.
   Overrides the popover UA box (inset, margin, border, padding, canvas background). Place and size come from app.js as custom properties. */
.layout-scrollbar { display: none; }
@media (hover: hover) and (pointer: fine) {
    * { scrollbar-width: none; }
    *::-webkit-scrollbar { display: none; }
    .layout-scrollbar {
        display: block;
        position: fixed;
        inset: var(--layout-scrollbar-top, 0px) var(--layout-scrollbar-right, 0px) auto auto;
        z-index: 30;
        width: 6px;
        height: var(--layout-scrollbar-height, 44px);
        margin: 0;
        padding: 0;
        border: none;
        border-radius: 100px;
        overflow: visible;
        background: var(--md-outline-variant);
        opacity: 0;
        pointer-events: none;
        transition: opacity 150ms, background-color 150ms;
    }
    .layout-scrollbar--visible { opacity: 1; pointer-events: auto; }
    .layout-scrollbar:hover, .layout-scrollbar--dragging { background: var(--md-outline); }
    .layout-scrollbar--dragging { transition: none; }
}
/* No app bar on this page (layout_page_start() called without one) — scroll region then starts below the notch itself instead. */
.layout-scroll--no-app-bar { --layout-scroll-top: env(safe-area-inset-top); }
/* A full-height box sits absolute in the scroll box, so it keeps the 16px gap that .layout-main's padding gives a scrolling one. */
.layout-scroll--no-app-bar > .layout-main > .layout-content--fills-pane { top: var(--spacing-medium); }
/* Joined panes (generated panes block): a scroll in a lower pane tints the bar and the whole pane above it, so that pane reads as part of the bar (app.js). */
.layout-pane[data-joined-scrolled] > .layout-scroll { background: var(--md-surface-container-low); }
/* No top gap under an app bar: bar and content share --md-surface, the scroll tint separates them (.claude/rules/scroll-elevation.md). */
.layout-scroll--no-app-bar > .layout-main { padding-top: var(--spacing-medium); }

.layout-main {
    padding-top: 0;
    padding-bottom: var(--spacing-medium);
    /* Never less than the 16px baseline; grows only if a landscape notch/rounded corner actually needs more. */
    padding-left: max(var(--spacing-medium), env(safe-area-inset-left));
    padding-right: max(var(--spacing-medium), env(safe-area-inset-right));
}

/* layout_nav_rail() — M3 navigation rail, ported from ~/Develop/Python ui/1_nav.py + 1_navigation_item.py: 96px column,
   pill indicator + icon + label per item. Medium+ only (.claude/rules/navigation.md); below 600px the drawers take over.
   Its own view-transition-name + z-index above the pane: it stands still and a sliding pane passes underneath it. */
.layout-nav-rail { display: none; }
.layout-nav-rail__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex-shrink: 0;
    width: 100%;
    padding: var(--spacing-xxsmall) 0;
    color: var(--md-on-surface-variant);
    text-decoration: none;
}
.layout-nav-rail__indicator {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 32px;
    border-radius: 16px;
}
@media (hover: hover) { .layout-nav-rail__item:hover .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-surface) 8%, transparent); } }
.layout-nav-rail__item--active .layout-nav-rail__indicator {
    background: var(--md-secondary-container);
    color: var(--md-on-secondary-container);
}
@media (hover: hover) { .layout-nav-rail__item--active:hover .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container)); } }
.layout-nav-rail__item:active .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-surface) 10%, transparent); }
@media (hover: hover) { .layout-nav-rail__item:active .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-surface) 10%, color-mix(in srgb, var(--md-on-surface) 8%, transparent)); } }
.layout-nav-rail__item--active:active .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, var(--md-secondary-container)); }
@media (hover: hover) { .layout-nav-rail__item--active:active .layout-nav-rail__indicator { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
.layout-nav-rail__label {
    max-width: calc(96px - 2 * var(--spacing-xxsmall));
    margin-top: var(--spacing-xxsmall);
    font-size: var(--md-label-medium-size);
    font-weight: var(--md-label-medium-weight);
    line-height: var(--md-label-medium-line-height);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.layout-nav-rail__item--active .layout-nav-rail__label { color: var(--md-on-surface); }::view-transition-group(app-rail), ::view-transition-old(app-rail), ::view-transition-new(app-rail) { animation: none; }

/* Medium+ with a rail (layout_page_start() sets .layout-has-rail whenever it renders one): ported from ~/Develop/Python
   ui/1_layout.py — rail + a rounded surface card inset 16px on surface-container, the app bar inside the card.
   The bar drops the compact container items (menu, account — the rail holds them), early or late; a back arrow stays. */
@media (min-width: 600px) {
    .layout-has-rail { background: var(--md-surface-container); }
    .layout-has-rail .layout-nav-rail {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 1;
        width: calc(var(--layout-nav-rail-width) + env(safe-area-inset-left));
        padding: calc(var(--spacing-medium) + env(safe-area-inset-top)) 0 calc(var(--spacing-medium) + env(safe-area-inset-bottom)) env(safe-area-inset-left);
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--spacing-xxsmall);
        overflow-y: auto;
        background: var(--md-surface-container);
        view-transition-name: app-rail;
    }
    /* layout_nav_rail_expanded(): the drawer's rows on the window background, 16px to the edge and to the pane card; the app name level with the card's 64px bar. */
    .layout-has-rail .layout-nav-rail--expanded {
        display: none;
        align-items: stretch;
        gap: 0;
        padding-left: calc(var(--spacing-medium) + env(safe-area-inset-left));
        padding-right: var(--spacing-medium);
    }
    .layout-nav-rail--expanded .ui-nav-drawer__header:first-child { height: 64px; }
    .layout-has-rail .layout-panes {
        position: fixed;
        top: calc(var(--spacing-medium) + env(safe-area-inset-top) + var(--layout-keyboard-top, 0px));
        right: calc(var(--spacing-medium) + env(safe-area-inset-right));
        bottom: calc(var(--spacing-medium) + max(env(safe-area-inset-bottom), var(--layout-keyboard-inset, 0px)));
        left: calc(var(--layout-nav-rail-width) + env(safe-area-inset-left));
        --layout-app-bar-height: calc(64px + var(--layout-bar-strip, 0px));
    }
    .layout-has-rail .layout-pane {
        border-radius: 16px;
        overflow: hidden;
        background: var(--md-surface);
    }
    .layout-has-rail .app-bar {
        position: absolute;
        top: 0;
        height: calc(64px + var(--layout-bar-strip, 0px));
        padding: 0 12px 0 0;
    }
    .layout-has-rail .app-bar__title { padding: 0 56px; }
    .layout-has-rail .app-bar--with-strip { padding-bottom: var(--layout-bar-strip, 0px); }
    .layout-has-rail .app-bar__strip { padding: 0 var(--spacing-medium) var(--spacing-small); }
    /* The clearance for two or three trailing icons holds in the card too, so a narrow pane (the tracker's court beside its buttons) cuts the title, never overlaps it. */
    .layout-has-rail .app-bar--actions-2 .app-bar__title { padding-inline: calc(12px + 2 * 40px + var(--spacing-xxsmall)); }
    .layout-has-rail .app-bar--actions-3 .app-bar__title { padding-inline: calc(12px + 3 * 40px + 2 * var(--spacing-xxsmall)); }
    .layout-has-rail .app-bar [data-drawer-toggle] { display: none; }
    /* A root bar's leading slot holds only the hidden menu from 600px, so the offline button takes its place; a pushed bar keeps both slots. */
    .layout-has-rail .app-bar--root [data-app-bar-slot="leading"] { display: none; }
    html[data-offline] .layout-has-rail .app-bar--root:not(.app-bar--actions-2):not(.app-bar--actions-3) .app-bar__title { padding-inline: 56px; }
    html[data-offline] .layout-has-rail .app-bar:not(.app-bar--root):not(.app-bar--actions-3) .app-bar__title { padding-inline: calc(2 * var(--spacing-xsmall) + 2 * 48px); }
    .layout-has-rail .layout-scroll {
        --layout-scroll-top: calc(64px + var(--layout-bar-strip, 0px));
        padding-bottom: 0;
    }
    /* A pane without a bar beside the first one (a supporting pane) starts at its own top edge: the pane's margin already clears the safe area. */
    .layout-has-rail .layout-scroll--no-app-bar { --layout-scroll-top: 0px; }
    .layout-has-rail .layout-main { padding-inline: var(--spacing-medium); }
    /* The pane card's 16px gaps above and below also count toward the 320px viewport floor. */
    .layout-has-rail .layout-content--fills-pane { bottom: var(--spacing-medium); left: var(--spacing-medium); right: var(--spacing-medium); min-height: calc(320px - 64px - 3 * var(--spacing-medium)); }
    .layout-has-rail .layout-content--fill { bottom: 0; left: 0; right: 0; padding-bottom: var(--spacing-medium); padding-inline: var(--spacing-medium); }
    /* Flush with the centered column's (and its rows') right edge: pane margin + .layout-main padding, plus half the pane width the column cap leaves free
       (100vw − rail − pane margin − 2 × padding − cap). Below the cap the extra is 0, so it sits in the pane corner, flush with the rows, as before. */
    .layout-has-rail .ui-fab {
        right: calc(2 * var(--spacing-medium) + env(safe-area-inset-right)
            + max(0px, (100vw - var(--layout-nav-rail-width) - env(safe-area-inset-left) - env(safe-area-inset-right) - 3 * var(--spacing-medium) - var(--layout-centered-max-width)) / 2));
        bottom: calc(2 * var(--spacing-medium) + env(safe-area-inset-bottom));
    }
}
/* The expanded rail (.claude/rules/navigation-sizes.md): 312px = 96 + the 216px a single pane's margins have at 1200px (1200 − 984, where its content
   reaches the 840px cap). A screen with a pane beside the first waits for 1600px, where its first pane's content still has the full 840px. */
@media (min-width: 1200px) {
    .layout-has-rail[data-nav-rail-expanded-from="large"] { --layout-nav-rail-width: 312px; }
    .layout-has-rail[data-nav-rail-expanded-from="large"] .layout-nav-rail { display: none; }
    .layout-has-rail[data-nav-rail-expanded-from="large"] .layout-nav-rail--expanded { display: flex; }
}
@media (min-width: 1600px) {
    .layout-has-rail { --layout-nav-rail-width: 312px; }
    .layout-has-rail .layout-nav-rail { display: none; }
    .layout-has-rail .layout-nav-rail--expanded { display: flex; }
}
/* panes:start — generated by bin/build-pane-css.py from bin/storyboard_model.py: never edit it (.claude/rules/pane-layout.md). */
.layout-panes { display: flex; flex-direction: column; }
.layout-pane { min-width: 0; min-height: 0; }
/* Beside each other from 840px or on a low screen (any phone in landscape); from 600px each pane is its own rounded area. */
@media (min-width: 840px), (max-height: 479.98px) { .layout-panes { flex-direction: row; gap: var(--spacing-medium); } }
/* Otherwise the panes join into one pane, one below the other: from 600px the panes' area is the rounded area, and a lower pane starts 16px below the one above (user decision, 2026-10-29). */
@media (max-width: 839.98px) and (min-height: 480px) {
    .layout-has-rail .layout-panes { border-radius: 16px; overflow: hidden; background: var(--md-surface); }
    .layout-has-rail .layout-pane { border-radius: 0; }
    /* Joined, a lower pane's own bar hides: the first pane's bar is the one bar (layout_page_end() $paneAppBars). */
    .layout-pane + .layout-pane > .app-bar { display: none; }
    .layout-pane + .layout-pane > .layout-scroll { --layout-scroll-top: 0px; }
    .layout-pane + .layout-pane > .layout-scroll > .layout-main { padding-top: var(--spacing-medium); }
    .layout-pane + .layout-pane > .layout-scroll > .layout-main > .layout-content--fills-pane { top: var(--spacing-medium); }
}
/* While a slot waits hidden (isHidden, until the screen's script presents it), the first slot fills the area alone. */
.layout-panes[data-pane-hidden] > [data-pane="first"] { flex: 1 1 0; }
@media (max-width: 599.98px) and (max-height: 479.98px) {
    .layout-pane[data-sizes~="compact-compact:hidden"] { display: none; }
    .layout-pane[data-sizes~="compact-compact:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="compact-compact:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="compact-compact:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-compact:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-compact:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-compact:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-compact:hug"] [data-pane-scroll]::after { display: none; }
}
@media (max-width: 599.98px) and (min-height: 480px) and (max-height: 899.98px) {
    .layout-pane[data-sizes~="compact-medium:hidden"] { display: none; }
    .layout-pane[data-sizes~="compact-medium:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="compact-medium:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="compact-medium:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-medium:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-medium:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-medium:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-medium:hug"] [data-pane-scroll]::after { display: none; }
    .layout-panes[data-on-top~="compact-medium:supporting"] > [data-pane="supporting"] { order: -1; }
    .layout-panes[data-on-top~="compact-medium:supporting"]:not([data-pane-hidden]) > [data-pane="supporting"] > .layout-scroll { --layout-scroll-top: var(--layout-app-bar-height); }
    .layout-panes[data-on-top~="compact-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] { position: static; display: flex; flex-direction: column; }
    .layout-panes[data-on-top~="compact-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { position: relative; inset: auto; flex: 1 1 0; min-height: 0; }
    .layout-panes[data-on-top~="compact-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main { padding-top: var(--spacing-small); }
    .layout-panes[data-on-top~="compact-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main > .layout-content--fills-pane { top: var(--spacing-small); }
    .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) { overflow-y: auto; overscroll-behavior-y: contain; }
    .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) > .layout-pane { flex: 0 0 auto; }
    .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) > .layout-pane > .layout-scroll { position: relative; top: auto; bottom: auto; overflow: visible; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) > [data-pane="first"] { display: contents; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) > [data-pane="first"] > .app-bar { position: sticky; top: 0; flex: 0 0 auto; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-medium"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { margin-top: 0; }
}
@media (max-width: 599.98px) and (min-height: 900px) {
    .layout-pane[data-sizes~="compact-expanded:hidden"] { display: none; }
    .layout-pane[data-sizes~="compact-expanded:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="compact-expanded:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="compact-expanded:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-expanded:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-expanded:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-expanded:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="compact-expanded:hug"] [data-pane-scroll]::after { display: none; }
    .layout-panes[data-on-top~="compact-expanded:supporting"] > [data-pane="supporting"] { order: -1; }
    .layout-panes[data-on-top~="compact-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="supporting"] > .layout-scroll { --layout-scroll-top: var(--layout-app-bar-height); }
    .layout-panes[data-on-top~="compact-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] { position: static; display: flex; flex-direction: column; }
    .layout-panes[data-on-top~="compact-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { position: relative; inset: auto; flex: 1 1 0; min-height: 0; }
    .layout-panes[data-on-top~="compact-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main { padding-top: var(--spacing-small); }
    .layout-panes[data-on-top~="compact-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main > .layout-content--fills-pane { top: var(--spacing-small); }
    .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) { overflow-y: auto; overscroll-behavior-y: contain; }
    .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) > .layout-pane { flex: 0 0 auto; }
    .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) > .layout-pane > .layout-scroll { position: relative; top: auto; bottom: auto; overflow: visible; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) > [data-pane="first"] { display: contents; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) > [data-pane="first"] > .app-bar { position: sticky; top: 0; flex: 0 0 auto; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="compact-expanded"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { margin-top: 0; }
}
@media (min-width: 600px) and (max-width: 839.98px) and (max-height: 479.98px) {
    .layout-pane[data-sizes~="medium-compact:hidden"] { display: none; }
    .layout-pane[data-sizes~="medium-compact:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="medium-compact:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="medium-compact:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-compact:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-compact:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-compact:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-compact:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 600px) and (max-width: 839.98px) and (min-height: 480px) and (max-height: 899.98px) {
    .layout-pane[data-sizes~="medium-medium:hidden"] { display: none; }
    .layout-pane[data-sizes~="medium-medium:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="medium-medium:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="medium-medium:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-medium:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-medium:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-medium:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-medium:hug"] [data-pane-scroll]::after { display: none; }
    .layout-panes[data-on-top~="medium-medium:supporting"] > [data-pane="supporting"] { order: -1; }
    .layout-panes[data-on-top~="medium-medium:supporting"]:not([data-pane-hidden]) > [data-pane="supporting"] > .layout-scroll { --layout-scroll-top: var(--layout-app-bar-height); }
    .layout-panes[data-on-top~="medium-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] { position: static; display: flex; flex-direction: column; }
    .layout-panes[data-on-top~="medium-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { position: relative; inset: auto; flex: 1 1 0; min-height: 0; }
    .layout-panes[data-on-top~="medium-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main { padding-top: var(--spacing-small); }
    .layout-panes[data-on-top~="medium-medium:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main > .layout-content--fills-pane { top: var(--spacing-small); }
    .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) { overflow-y: auto; overscroll-behavior-y: contain; }
    .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) > .layout-pane { flex: 0 0 auto; }
    .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) > .layout-pane > .layout-scroll { position: relative; top: auto; bottom: auto; overflow: visible; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) > [data-pane="first"] { display: contents; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) > [data-pane="first"] > .app-bar { position: sticky; top: 0; flex: 0 0 auto; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-medium"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { margin-top: 0; }
}
@media (min-width: 600px) and (max-width: 839.98px) and (min-height: 900px) {
    .layout-pane[data-sizes~="medium-expanded:hidden"] { display: none; }
    .layout-pane[data-sizes~="medium-expanded:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="medium-expanded:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="medium-expanded:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-expanded:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-expanded:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-expanded:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="medium-expanded:hug"] [data-pane-scroll]::after { display: none; }
    .layout-panes[data-on-top~="medium-expanded:supporting"] > [data-pane="supporting"] { order: -1; }
    .layout-panes[data-on-top~="medium-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="supporting"] > .layout-scroll { --layout-scroll-top: var(--layout-app-bar-height); }
    .layout-panes[data-on-top~="medium-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] { position: static; display: flex; flex-direction: column; }
    .layout-panes[data-on-top~="medium-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { position: relative; inset: auto; flex: 1 1 0; min-height: 0; }
    .layout-panes[data-on-top~="medium-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main { padding-top: var(--spacing-small); }
    .layout-panes[data-on-top~="medium-expanded:supporting"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll > .layout-main > .layout-content--fills-pane { top: var(--spacing-small); }
    .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) { overflow-y: auto; overscroll-behavior-y: contain; }
    .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) > .layout-pane { flex: 0 0 auto; }
    .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) > .layout-pane > .layout-scroll { position: relative; top: auto; bottom: auto; overflow: visible; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) > [data-pane="first"] { display: contents; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) > [data-pane="first"] > .app-bar { position: sticky; top: 0; flex: 0 0 auto; }
    .layout-has-rail .layout-panes[data-scrolls-as-one~="medium-expanded"]:not([data-pane-hidden]) > [data-pane="first"] > .layout-scroll { margin-top: 0; }
}
@media (min-width: 840px) and (max-width: 1199.98px) and (max-height: 479.98px) {
    .layout-pane[data-sizes~="expanded-compact:hidden"] { display: none; }
    .layout-pane[data-sizes~="expanded-compact:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="expanded-compact:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="expanded-compact:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-compact:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-compact:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-compact:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-compact:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 840px) and (max-width: 1199.98px) and (min-height: 480px) and (max-height: 899.98px) {
    .layout-pane[data-sizes~="expanded-medium:hidden"] { display: none; }
    .layout-pane[data-sizes~="expanded-medium:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="expanded-medium:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="expanded-medium:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-medium:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-medium:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-medium:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-medium:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 840px) and (max-width: 1199.98px) and (min-height: 900px) {
    .layout-pane[data-sizes~="expanded-expanded:hidden"] { display: none; }
    .layout-pane[data-sizes~="expanded-expanded:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="expanded-expanded:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="expanded-expanded:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-expanded:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-expanded:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-expanded:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="expanded-expanded:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1200px) and (max-width: 1599.98px) and (max-height: 479.98px) {
    .layout-pane[data-sizes~="large-compact:hidden"] { display: none; }
    .layout-pane[data-sizes~="large-compact:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="large-compact:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="large-compact:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-compact:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-compact:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-compact:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-compact:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1200px) and (max-width: 1599.98px) and (min-height: 480px) and (max-height: 899.98px) {
    .layout-pane[data-sizes~="large-medium:hidden"] { display: none; }
    .layout-pane[data-sizes~="large-medium:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="large-medium:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="large-medium:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-medium:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-medium:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-medium:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-medium:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1200px) and (max-width: 1599.98px) and (min-height: 900px) {
    .layout-pane[data-sizes~="large-expanded:hidden"] { display: none; }
    .layout-pane[data-sizes~="large-expanded:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="large-expanded:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="large-expanded:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-expanded:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-expanded:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-expanded:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="large-expanded:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1600px) and (max-height: 479.98px) {
    .layout-pane[data-sizes~="extraLarge-compact:hidden"] { display: none; }
    .layout-pane[data-sizes~="extraLarge-compact:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="extraLarge-compact:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="extraLarge-compact:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-compact:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-compact:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-compact:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-compact:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1600px) and (min-height: 480px) and (max-height: 899.98px) {
    .layout-pane[data-sizes~="extraLarge-medium:hidden"] { display: none; }
    .layout-pane[data-sizes~="extraLarge-medium:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="extraLarge-medium:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="extraLarge-medium:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-medium:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-medium:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-medium:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-medium:hug"] [data-pane-scroll]::after { display: none; }
}
@media (min-width: 1600px) and (min-height: 900px) {
    .layout-pane[data-sizes~="extraLarge-expanded:hidden"] { display: none; }
    .layout-pane[data-sizes~="extraLarge-expanded:fixed-360"] { flex: 0 0 var(--layout-pane-fixed-360); }
    .layout-pane[data-sizes~="extraLarge-expanded:flexible"] { flex: 1 1 0; }
    .layout-pane[data-sizes~="extraLarge-expanded:hug"] { flex: 0 0 auto; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-expanded:hug"] > .layout-scroll { position: relative; top: auto; bottom: auto; margin-top: var(--layout-scroll-top); overflow: visible; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-expanded:hug"] > .layout-scroll > .layout-main { padding: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-expanded:hug"] .layout-content--fills-pane { position: relative; inset: auto; min-height: 0; overflow: visible; padding-bottom: 0; }
    .layout-panes:not([data-pane-hidden]) > .layout-pane[data-sizes~="extraLarge-expanded:hug"] [data-pane-scroll]::after { display: none; }
}
/* panes:end */
/* Streamed page (layout_page_start/end): loading screen appears only after 150ms, disappears once .layout-content exists (:has) or parsing ends (.layout-loaded, app.js — Safari's :has goes stale while streaming).
   Content fades in only if the loading screen was actually visible (app.js sets .layout-was-loading), so fast pages don't flicker. */
.layout-paint-trigger { position: absolute; visibility: hidden; }
.layout-loading { animation: layout-appear 0.2s ease-out 0.15s both; }
.layout-main:has(> .layout-content) > .layout-loading, .layout-loaded .layout-loading { display: none; }
.layout-was-loading .layout-main > .layout-content { animation: layout-appear 0.2s ease-out both; }
/* The real page after a placeholder page (.claude/rules/placeholders.md) shows its own placeholder at once, then its content fades in
   over it while it fades out: a crossfade inside the page. Chrome's page-to-page crossfade often began from a white picture of the
   placeholder page (user reports, 2026-10-18). Absolute in .layout-scroll with the main box's padding, the fading placeholder keeps the content's place.
   Never a positioned .layout-main: a full-height box measures from .layout-scroll, and in a contentless main it shrank to its minimum (map quiz, 2026-10-19). */
[data-placeholder-arrival] .layout-loading { animation: none; }
[data-placeholder-arrival] .layout-main:has(> .layout-content) > .layout-loading,
[data-placeholder-arrival].layout-loaded .layout-loading {
    display: block;
    position: absolute;
    inset: 0;
    /* The whole scroll box like .layout-main: its own 840px cap would hold the padding too, and its placeholder sat 16px in on a wide screen. */
    width: auto;
    max-width: none;
    margin: 0;
    padding: inherit;
    box-sizing: border-box;
    pointer-events: none;
    animation: layout-placeholder-out 0.25s ease-out forwards;
}
[data-placeholder-arrival] .layout-main > .layout-content, [data-placeholder-arrival] .app-bar__title { animation: layout-appear 0.25s ease-out both; }
@keyframes layout-placeholder-out { to { opacity: 0; visibility: hidden; } }
@media (prefers-reduced-motion: reduce) {
    [data-placeholder-arrival] .layout-main:has(> .layout-content) > .layout-loading, [data-placeholder-arrival].layout-loaded .layout-loading { display: none; }
    [data-placeholder-arrival] .layout-main > .layout-content, [data-placeholder-arrival] .app-bar__title { animation: none; }
}
@keyframes layout-appear { from { opacity: 0; } }

/* A screen's placeholder (.claude/rules/placeholders.md): its layout with static blocks for text, icons and media, like iOS's
   .redacted(reason: .placeholder) (user decision, 2026-10-18: no shimmer). As there, each block is its own text color, faint:
   dark text gives a light gray block, primary text a light primary one. The text itself is en spaces, so it draws nothing. */
.layout-placeholder { max-width: none; }
.ui-placeholder__text {
    background: color-mix(in srgb, currentColor 14%, transparent);
    border-radius: 4px;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
}
.ui-placeholder__icon { display: inline-block; width: 1em; height: 1em; border-radius: 50%; background: color-mix(in srgb, currentColor 14%, transparent); }
.ui-placeholder__media { background: color-mix(in srgb, var(--md-on-surface) 8%, transparent); }
/* A filled control has no fill in a placeholder, only its label as a faint primary block, like an iOS button's label (user report, 2026-10-18:
   the dark "Start App" box). Pseudo-element text (the dots between facts) draws nothing. */
:is(.ui-placeholder, [data-placeholder-pane] .app-bar) :is(.ui-button--filled, .ui-button--tonal, .ui-icon-button--filled, .ui-icon-button--tonal, .ui-fab) {
    background: transparent; box-shadow: none; color: var(--md-primary);
}
/* A selected segment looks like the others, so the control reads as one light shape. */
.ui-placeholder .ui-segmented-button__input:checked + .ui-segmented-button__pill { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }
.ui-placeholder ::before, .ui-placeholder ::after { color: transparent; }

/* The content pane (.layout-content, or the standalone .layout-loading screen before it exists): defaults to a
   centered single column, capped at --layout-centered-max-width (Medium/Expanded's own boundary — content stops
   growing where it already looks intentional, rather than stretching edge to edge on a wide screen). Floored so
   the viewport itself never needs to go below 320px (Compact's own boundary, also iPad Split View's narrowest
   pane) — the pane's own min-width is 320px minus .layout-main's 16px side padding, not 320px itself, since that
   padding sits outside this element. Below 320px viewport the pane overflows horizontally instead of shrinking further. */
.layout-content, .layout-loading {
    width: 100%;
    min-width: calc(320px - 2 * var(--spacing-medium));
    max-width: var(--layout-centered-max-width);
    margin-inline: auto;
}
.layout-content {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-medium);
}

/* layout_page_end(..., hasFab: true) — a fixed ui_fab() floats over the scroll region's own end, so the pane
   reserves room below its last child equal to the largest ui_fab() size (80px) + its own bottom offset (M3
   spec: that's enough clearance, no extra gap needed), even on a page using the smaller medium FAB (harmless
   extra slack, not worth a param). */
.layout-content--has-fab { padding-bottom: calc(80px + var(--spacing-medium)); }

/* layout_page_end(..., layout: 'centeredFullHeight'): the content fills exactly the visible pane's height and never scrolls (a full-height map card).
   Absolute in the fixed-size .layout-scroll with .layout-main's own padding, so no :has() is needed (Safari misses it while streaming).
   Its last child takes the remaining height; a ui_card() there gives it to its media.
   Floored like the width: a viewport under 320px tall scrolls the pane instead of squashing the map (320px − the 64px bar − its 16px bottom gap). */
.layout-content--fills-pane {
    position: absolute;
    top: 0;
    bottom: calc(var(--spacing-medium) + env(safe-area-inset-bottom));
    left: max(var(--spacing-medium), env(safe-area-inset-left));
    right: max(var(--spacing-medium), env(safe-area-inset-right));
    width: auto;
    min-height: calc(320px - 64px - var(--spacing-medium));
    /* Like a scrolling screen's .layout-scroll: the box reaches under the browser's bars, content taller than the pane scrolls behind them. */
    overflow-y: auto;
    overscroll-behavior: contain;
}
/* In the flex column the [data-pane-scroll] spacer would add the column's gap too: its negative margin cancels it, so with no bars it adds nothing. */
.layout-content--fills-pane::after { margin-top: calc(-1 * var(--spacing-medium)); }
/* The last child grows into the free height but never shrinks below its content: taller content then scrolls, and the spacer follows its real end. */
.layout-content--fills-pane > :last-child { flex: 1 1 auto; min-height: min-content; }
.layout-content--fills-pane > .ui-card > .ui-card__media { flex: 1 1 auto; height: auto; min-height: 0; }
.layout-content--fills-pane > .ui-card > .ui-card__body { flex: none; }
/* layout: 'fill': the same full height, and the whole pane's width without the column cap.
   The gaps are padding, so the box ends at the pane edge like .layout-scroll: its scrollbar thumb, and that of a child reaching into the padding, sits at that edge. */
.layout-content--fill {
    max-width: none;
    bottom: 0;
    left: 0;
    right: 0;
    /* The spacer for Safari's bars already clears the last row; the 16px fills in only where no bars stand. */
    padding-bottom: max(env(safe-area-inset-bottom), var(--spacing-medium) - var(--layout-behind-bars));
    padding-inline: max(var(--spacing-medium), env(safe-area-inset-left)) max(var(--spacing-medium), env(safe-area-inset-right));
}

/* layout_page_end(..., gridCount: N) — an M3 column grid instead of the centered column, for real content only:
   layout_page_end() never adds this class for the empty/error fallback, since one placeholder message stretched
   across grid columns would look broken, not adaptive. M3's 4/8/12-column system (compact/medium/expanded+): every
   card spans 4 columns, so it's the same width everywhere and lines up with future elements (nav rail, detail
   pane) built on the same column grid — 1 per row at compact, 2 at medium, 3 from expanded up. No 4th tier: this
   deliberately caps at 3 per row rather than adding a Large/Extra-large step. Capped at --layout-grid-max-width,
   a separate token from the centered column's (both default to the same 840px today, so a card-grid page and a
   centered-text page share one page width/margins unless an app's own style.css overrides just one of them) —
   not Large's own 1600px ceiling, so the grid just stops widening its 3 cards past that point rather than
   gaining side margin, since cards narrower than that still read fine at 3-up.
   Rejected: an auto-fit/:has()-based sparse-column fix (tried and reverted) — auto-fit's 1fr tracks stretch a
   sparse card to fill the whole cap instead of staying card-width and centered, and any :has()-based
   variant is unreliable here: Safari doesn't re-check :has() for content that arrives while the page is still
   streaming (.claude/rules/streaming.md), which is exactly how these cards arrive — headless Playwright doesn't reproduce
   that, so a passing test could still ship the wrong column count to a real phone. */
.layout-content--grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--spacing-medium);
    max-width: var(--layout-grid-max-width);
}
.layout-content--grid > * { grid-column: span 4; }
/* A grid sparse enough to fit one row (1-2 cards, PHP knows the count via layout_page_end()'s $gridCount) shares
   that row out evenly instead of leaving empty trailing columns: 1 card spans the full width (same as the centered
   layout), 2 cards split it in half. 2 cards at medium and 3+ cards anywhere already tile full rows. */
.layout-content--count-1 > * { grid-column: 1 / -1; }
@media (min-width: 600px) {
    .layout-content--grid { grid-template-columns: repeat(8, 1fr); }
}
@media (min-width: 840px) {
    .layout-content--grid { grid-template-columns: repeat(12, 1fr); }
    .layout-content--count-2 > * { grid-column: span 6; }
}
.layout-main h1 {
    font-size: var(--md-headline-medium-size);
    font-weight: var(--md-headline-medium-weight);
    margin: 0 0 var(--spacing-large);
    color: var(--md-on-background);
}
/* Plain long-form content (legal/'s imprint + privacy pages, root CLAUDE.md's "h1, p, ul") — .layout-content is a
   flex column with its own gap, so these only set type/color and reset each tag's default margin to 0 (h2 keeps
   a little extra margin-top, to read as a new section rather than just the next paragraph); the gap does the rest. */
.layout-main h2 {
    font-size: var(--md-title-large-size);
    font-weight: var(--md-title-large-weight);
    margin: var(--spacing-medium) 0 0;
    color: var(--md-on-background);
}
.layout-main h3 {
    font-size: var(--md-title-medium-size);
    font-weight: var(--md-title-medium-weight);
    margin: 0;
    color: var(--md-on-background);
}
.layout-main p { margin: 0; font-size: var(--md-body-large-size); font-weight: var(--md-body-large-weight); line-height: 1.5; }
.layout-main ul { margin: 0; padding-left: var(--spacing-large); }
.layout-main li { font-size: var(--md-body-large-size); font-weight: var(--md-body-large-weight); line-height: 1.5; }
/* A bare "a" tag selector here would beat ui_button()/ui_fab()'s own single-class color rule on specificity alone
   (a type selector + a class beats a class alone) and silently recolor every button site-wide — a real regression
   this caused once already. A plain prose link gets this class explicitly instead. */
.ui-text-link { color: var(--md-primary); }

/* Self-hosted (Apache 2.0 licensed, see google/material-design-icons) — avoids a third-party request to Google's CDN. */
/* Subset — generated by bin/build-icons.sh, see .claude/rules/design.md */
@font-face {
    font-family: 'Material Symbols Rounded';
    font-style: normal;
    font-weight: 100 700;
    font-display: block;
    src: url('/_assets/material-symbols-rounded.woff2?v=09df5497') format('woff2');
}

/* ui_icon() — Material Symbols Rounded, rendered via ligature text; icon inherits color from its parent.
   Size = the square frame (line-height 1); the font draws its glyph in 20/24 of it, e.g. small 20px → 16.67px glyph (Figma). */
.ui-icon {
    font-family: 'Material Symbols Rounded';
    font-weight: 200;
    font-style: normal;
    line-height: 1;
    -webkit-font-feature-settings: 'liga';
    font-feature-settings: 'liga';
    font-variation-settings: 'FILL' 0, 'GRAD' 0, 'opsz' 24;
}
.ui-icon--small { font-size: 20px; }
.ui-icon--medium { font-size: 24px; }
.ui-icon--large { font-size: 40px; }
.ui-icon--xlarge { font-size: 48px; }
.ui-icon--xxlarge { font-size: 96px; }

/* ui_button() — M3 pill buttons, ported from ~/Develop/Python ui/1_button.py (filled/outlined/tonal/text, medium/small) */
.ui-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-xsmall);
    box-sizing: border-box;
    text-decoration: none;
    white-space: nowrap;
    font-family: var(--font);
    font-size: var(--md-label-large-size);
    font-weight: var(--md-label-large-weight);
    cursor: pointer;
}
/* Save mode (postFields): same busy pattern as .ui-fab — locked at once, spinner after 150ms. */
/* :where() keeps this reset below the variants, so a <button> keeps the outlined border like an <a>. */
:where(button).ui-button { border: none; padding-block: 0; }
.ui-button[aria-disabled="true"] {
    background: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
    color: color-mix(in srgb, var(--md-on-surface) 38%, transparent);
    pointer-events: none;
}
/* ui_toggle_button() — both labels are rendered, aria-pressed picks the visible one. */
.ui-toggle-button[aria-pressed="false"] .ui-toggle-button__on, .ui-toggle-button[aria-pressed="true"] .ui-toggle-button__off { display: none; }
.ui-button[aria-busy="true"] { pointer-events: none; position: relative; }
.ui-button[aria-busy="true"] .ui-button__label { animation: ui-fab-busy-icon 0s linear 0.15s both; }
.ui-button[aria-busy="true"]::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: var(--spacing-large);
    height: var(--spacing-large);
    box-sizing: border-box;
    border: var(--spacing-xxsmall) solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: layout-appear 0.2s ease-out 0.15s both, ui-spin 0.8s linear infinite;
}
.ui-button--medium { height: 56px; padding: 0 24px; border-radius: 28px; }
.ui-button--small { height: 40px; padding: 0 16px; border-radius: 20px; }

/* $count: a circle 2px inside the button's top, bottom and end edge; the label takes the free width (may wrap to two lines), so the circle sits at the end, 8px after it; 12px at the start. */
.ui-button--with-count {
    justify-content: flex-start;
    gap: var(--spacing-xsmall);
    padding-inline: var(--spacing-small) 2px;
    white-space: normal;
    text-align: start;
    line-height: var(--md-label-large-line-height);
}
.ui-button--outlined.ui-button--with-count { padding-inline-end: 1px; }
.ui-button--with-count > .ui-icon { flex: none; }
.ui-button--with-count .ui-button__label { flex: 1; min-width: 0; }
.ui-button__count {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(100% - 4px);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--md-inverse-surface);
    color: var(--md-inverse-on-surface);
    font-size: var(--md-title-medium-size);
    font-weight: var(--md-title-medium-weight);
    font-variant-numeric: tabular-nums;
}
.ui-button--outlined .ui-button__count { height: calc(100% - 2px); }
.ui-button[aria-disabled="true"] .ui-button__count {
    background: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
    color: color-mix(in srgb, var(--md-on-surface) 38%, transparent);
}

.ui-button--filled { background: var(--md-primary); color: var(--md-on-primary); }
@media (hover: hover) { .ui-button--filled:hover { background: color-mix(in srgb, var(--md-on-primary) 8%, var(--md-primary)); } }

.ui-button--destructive { background: var(--md-error); color: var(--md-on-error); }
@media (hover: hover) { .ui-button--destructive:hover { background: color-mix(in srgb, var(--md-on-error) 8%, var(--md-error)); } }

/* An outlined button's outline and content colours are component properties, so a button on a coloured container can take that container's on-colour. */
.ui-button--outlined { background: transparent; color: var(--ui-button-outlined-content, var(--md-on-surface-variant)); border: 1px solid var(--ui-button-outlined-outline, var(--md-outline-variant)); }
@media (hover: hover) { .ui-button--outlined:hover { background: color-mix(in srgb, var(--ui-button-outlined-content, var(--md-on-surface-variant)) 8%, transparent); } }

.ui-button--tonal { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }

/* ui_assist_chip() — M3 assist chip: 32px, an outline, 8px corners, a primary leading icon 8px in and 8px before the label, the label 16px from the end. */
.ui-assist-chip { display: inline-flex; align-items: center; gap: var(--spacing-xsmall); height: 32px; padding: 0 var(--spacing-medium) 0 var(--spacing-xsmall); box-sizing: border-box; border: 1px solid var(--md-outline-variant); border-radius: 8px; background: transparent; color: var(--md-on-surface); font: inherit; font-size: var(--md-label-large-size); font-weight: var(--md-label-large-weight); cursor: pointer; }
.ui-assist-chip > .ui-icon { color: var(--md-primary); }
@media (hover: hover) { .ui-assist-chip:hover { background: color-mix(in srgb, var(--md-on-surface) 8%, transparent); } }
.ui-assist-chip[hidden] { display: none; }
@media (hover: hover) { .ui-button--tonal:hover { background: color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container)); } }

.ui-button--text { background: transparent; color: var(--md-primary); }
@media (hover: hover) { .ui-button--text:hover { background: color-mix(in srgb, var(--md-primary) 8%, transparent); } }
.ui-button--filled:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary) 10%, var(--md-primary)); }
@media (hover: hover) { .ui-button--filled:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary) 10%, color-mix(in srgb, var(--md-on-primary) 8%, var(--md-primary))); } }
.ui-button--destructive:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-error) 10%, var(--md-error)); }
@media (hover: hover) { .ui-button--destructive:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-error) 10%, color-mix(in srgb, var(--md-on-error) 8%, var(--md-error))); } }
.ui-button--outlined:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--ui-button-outlined-content, var(--md-on-surface-variant)) 10%, transparent); }
@media (hover: hover) { .ui-button--outlined:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--ui-button-outlined-content, var(--md-on-surface-variant)) 10%, color-mix(in srgb, var(--ui-button-outlined-content, var(--md-on-surface-variant)) 8%, transparent)); } }
.ui-button--tonal:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, var(--md-secondary-container)); }
@media (hover: hover) { .ui-button--tonal:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
.ui-button--text:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-primary) 10%, transparent); }
@media (hover: hover) { .ui-button--text:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-primary) 10%, color-mix(in srgb, var(--md-primary) 8%, transparent)); } }

/* ui_icon_button() — M3 icon button (standard/outlined/tonal), ported from ~/Develop/Python ui/1_icon_button.py.
   toggle_icon_button not ported: it's a standalone boolean, not a link or a radio-group choice — no state mechanism
   for that exists yet (.claude/rules/state.md only has state groups); port it once a real toggle feature needs one.
   Sizes xsmall..xlarge = M3's 32/40/56/96/136 scale, matching ui_fab()'s medium/large on the same scale. */
.ui-icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    border-radius: 50%;
    text-decoration: none;
    /* $opensDrawer mode renders a <button>, not an <a> — browsers give <button> a default border/background/padding
       that read as an unwanted ring around the icon; reset them the same way ui_fab()'s own <button> mode does. */
    border: none;
    background: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
}
.ui-icon-button--xsmall { width: 32px; height: 32px; }
.ui-icon-button--small { width: 40px; height: 40px; }
.ui-icon-button--medium { width: 56px; height: 56px; }
.ui-icon-button--large { width: 96px; height: 96px; }
.ui-icon-button--xlarge { width: 136px; height: 136px; }

.ui-icon-button:is([data-copy-text], [data-share-url]) .ui-icon:nth-child(2), .ui-icon-button:is([data-copy-text], [data-share-url])[data-copied] .ui-icon:nth-child(1) { display: none; }
.ui-icon-button:is([data-copy-text], [data-share-url])[data-copied] .ui-icon:nth-child(2) { display: inline; }
.ui-icon-button--standard { background: transparent; color: var(--md-on-surface-variant); }
@media (hover: hover) { .ui-icon-button--standard:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--md-on-surface-variant) 8%, transparent); } }
.ui-icon-button--standard:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface-variant) 10%, transparent); }
@media (hover: hover) { .ui-icon-button--standard:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface-variant) 10%, color-mix(in srgb, var(--md-on-surface-variant) 8%, transparent)); } }

.ui-icon-button--outlined { background: transparent; color: var(--md-on-surface-variant); border: 1px solid var(--md-outline-variant); }
@media (hover: hover) { .ui-icon-button--outlined:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--md-on-surface-variant) 8%, transparent); } }
.ui-icon-button--outlined:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface-variant) 10%, transparent); }
@media (hover: hover) { .ui-icon-button--outlined:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface-variant) 10%, color-mix(in srgb, var(--md-on-surface-variant) 8%, transparent)); } }

.ui-icon-button--tonal { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }
@media (hover: hover) { .ui-icon-button--tonal:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container)); } }
.ui-icon-button--tonal:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, var(--md-secondary-container)); }
@media (hover: hover) { .ui-icon-button--tonal:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }

.ui-icon-button--filled { background: var(--md-primary); color: var(--md-on-primary); }
@media (hover: hover) { .ui-icon-button--filled:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--md-on-primary) 8%, var(--md-primary)); } }
.ui-icon-button--filled:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary) 10%, var(--md-primary)); }
@media (hover: hover) { .ui-icon-button--filled:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary) 10%, color-mix(in srgb, var(--md-on-primary) 8%, var(--md-primary))); } }

.ui-icon-button[aria-disabled="true"] { cursor: default; pointer-events: none; }
.ui-icon-button[aria-disabled="true"] .ui-icon { opacity: 0.38; }
.ui-icon-button:is(.ui-icon-button--tonal, .ui-icon-button--filled)[aria-disabled="true"] { background: color-mix(in srgb, var(--md-on-surface) 12%, transparent); color: var(--md-on-surface); }

/* ui_progress() — M3 linear progress (Figma: Type=Flat, Thickness=8 dp): primary indicator, a 4px gap, the
   secondary-container track and a 4px stop dot at its end. A native <progress> holds the value, so no style="" is needed;
   the gap is a shadow in the colour behind the bar (--ui-progress-gap: the pane's surface, a card's container). */
.ui-progress { position: relative; height: 8px; --ui-progress-gap: var(--md-surface); }
.ui-card .ui-progress { --ui-progress-gap: var(--md-surface-container); }
.ui-progress__bar { appearance: none; display: block; width: 100%; height: 8px; border: 0; border-radius: 4px; overflow: hidden; background: var(--md-secondary-container); }
.ui-progress__bar::-webkit-progress-bar { background: var(--md-secondary-container); border-radius: 4px; }
.ui-progress__bar::-webkit-progress-value { background: var(--md-primary); border-radius: 4px; box-shadow: 4px 0 0 0 var(--ui-progress-gap); }
.ui-progress__bar::-moz-progress-bar { background: var(--md-primary); border-radius: 4px; box-shadow: 4px 0 0 0 var(--ui-progress-gap); }
.ui-progress__bar[value="0"]::-webkit-progress-value { box-shadow: none; }
.ui-progress__bar[value="0"]::-moz-progress-bar { box-shadow: none; }
.ui-progress::after { content: ''; position: absolute; right: 2px; top: 2px; width: 4px; height: 4px; border-radius: 2px; background: var(--md-primary); }

/* ui_stepper() — a label, then [−] value [+]: two tonal xsmall icon buttons (aria-disabled at a limit) around the value. */
.ui-stepper { display: flex; align-items: center; gap: var(--spacing-small); }
.ui-stepper__label { flex: 1; color: var(--md-on-surface); font-size: var(--md-body-large-size); }
.ui-stepper__value { min-width: 64px; text-align: center; color: var(--md-on-surface); font-size: var(--md-title-medium-size); font-weight: var(--md-title-medium-weight); font-variant-numeric: tabular-nums; }

/* ui_counter() / ui_counter_row() — small chips with a label and a running value, wrapping in a row. */
.ui-counter-row { display: flex; flex-wrap: wrap; gap: var(--spacing-xsmall); }
/* 32px high like an M3 chip (ui_assist_chip()), so both kinds stand level in one row (2026-10-29). */
.ui-counter { display: inline-flex; align-items: center; gap: var(--spacing-xsmall); height: 32px; padding: 0 12px; box-sizing: border-box; border-radius: 8px; background: var(--md-surface-container-highest); font-size: var(--md-label-large-size); font-weight: var(--md-label-large-weight); }
.ui-counter__label { color: var(--md-on-surface-variant); }
.ui-counter__value { color: var(--md-on-surface); font-variant-numeric: tabular-nums; }
/* ui_counter(trailingIcon:): the chip as a button with its icon at the end, centred on the text line. */
.ui-counter--action { align-items: center; border: 0; font-family: inherit; cursor: pointer; color: var(--md-on-surface-variant); }
.ui-counter--action:hover { background: color-mix(in srgb, var(--md-on-surface) 8%, var(--md-surface-container-highest)); }

/* ui_floating_toolbar() — M3 floating toolbar, ported from ~/Develop/Python ui/1_floating_toolbar.py: a 64px raised pill of icon buttons, the same shadow as ui_fab(). */
.ui-floating-toolbar {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-xxsmall);
    height: 64px;
    padding: 0 var(--spacing-small);
    box-sizing: border-box;
    border-radius: 32px;
    background: var(--md-surface-container);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 8px 3px rgba(0, 0, 0, 0.15);
}
/* Between two button groups, like ~/Develop/Python ui/1_toolbar.py's separator: 1x20px, on-surface-variant at 30%. */
.ui-floating-toolbar__divider {
    flex-shrink: 0;
    width: 1px;
    height: 20px;
    margin: 0 var(--spacing-xxsmall);
    background: color-mix(in srgb, var(--md-on-surface-variant) 30%, transparent);
}
/* ui_floating_toolbar(isVertical: true): the same pill upright, 64px wide, its buttons top to bottom; a divider turns 20x1. */
.ui-floating-toolbar--vertical { flex-direction: column; width: 64px; height: auto; padding: var(--spacing-small) 0; }
.ui-floating-toolbar--vertical > .ui-floating-toolbar__divider { width: 20px; height: 1px; margin: var(--spacing-xxsmall) 0; }
/* ui_docked_toolbar() — M3 docked toolbar: a full-width 64px bar on surface-container, no corners, no shadow; three slots, the centre one centred
   whatever the others hold (two equal outer columns), 16px from the sides, clear of the home indicator. */
.ui-docked-toolbar {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: var(--spacing-small);
    min-height: 64px;
    padding: 0 max(var(--spacing-medium), env(safe-area-inset-right)) env(safe-area-inset-bottom) max(var(--spacing-medium), env(safe-area-inset-left));
    box-sizing: border-box;
    background: var(--md-surface-container);
}
.ui-docked-toolbar__leading { display: flex; justify-content: flex-start; gap: var(--spacing-xxsmall); }
.ui-docked-toolbar__centre { display: flex; justify-content: center; gap: var(--spacing-xxsmall); }
.ui-docked-toolbar__trailing { display: flex; justify-content: flex-end; gap: var(--spacing-xxsmall); }
/* Save mode (postFields): same busy pattern as .ui-fab — locked at once, the spinner replaces the glyph after 150ms. */
.ui-icon-button[aria-busy="true"] { pointer-events: none; position: relative; }
.ui-icon-button[aria-busy="true"] .ui-icon { animation: ui-fab-busy-icon 0s linear 0.15s both; }
.ui-icon-button[aria-busy="true"]::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: var(--spacing-large);
    height: var(--spacing-large);
    box-sizing: border-box;
    border: var(--spacing-xxsmall) solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: layout-appear 0.2s ease-out 0.15s both, ui-spin 0.8s linear infinite;
}

/* ui_fab() — M3 floating action button, ported from ~/Develop/Python ui/1_fab.py; fixed bottom-right, safe-area aware.
   [aria-disabled] is a live state (.claude/rules/state.md): app.js only removes the attribute, this file owns its look, same pattern as ui_list_item()'s aria-checked. */
.ui-fab {
    position: fixed;
    right: max(var(--spacing-medium), env(safe-area-inset-right));
    bottom: max(var(--spacing-medium), env(safe-area-inset-bottom));
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    background: var(--md-primary-container);
    color: var(--md-on-primary-container);
    text-decoration: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 8px 3px rgba(0, 0, 0, 0.15);
    z-index: 10;
}
@media (hover: hover) { .ui-fab:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--md-on-primary-container) 8%, var(--md-primary-container)); } }
.ui-fab:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary-container) 10%, var(--md-primary-container)); }
@media (hover: hover) { .ui-fab:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-primary-container) 10%, color-mix(in srgb, var(--md-on-primary-container) 8%, var(--md-primary-container))); } }

.ui-fab--medium { width: 56px; height: 56px; border-radius: 16px; }
/* ui_fab(isInline: true): in the flow beside a ui_floating_toolbar() (M3's toolbar + FAB pair), not in the screen corner;
   relative, so the busy spinner (::after) centres on the FAB itself. */
/* inset: auto drops the corner offsets of .ui-fab (and of .layout-has-rail .ui-fab), which would shift a relative FAB out of the row. */
.ui-fab.ui-fab--inline { position: relative; inset: auto; flex-shrink: 0; z-index: auto; }
.ui-fab--large { width: 80px; height: 80px; border-radius: 24px; }

.ui-fab--extended { width: auto; padding: 0 20px 0 16px; gap: var(--spacing-small); }
.ui-fab__label {
    font-family: var(--font);
    font-size: var(--md-label-large-size);
    font-weight: var(--md-label-large-weight);
    white-space: nowrap;
}

.ui-fab[aria-disabled="true"] {
    background: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
    color: var(--md-on-surface);
    box-shadow: none;
    cursor: default;
    pointer-events: none;
}
.ui-fab[aria-disabled="true"] .ui-icon { opacity: 0.38; }
/* Save mode while its POST runs: locked at once, the spinner only after the same 150ms as the loading screens (no flash on a fast save). */
.ui-fab[aria-busy="true"] { pointer-events: none; }
.ui-fab[aria-busy="true"] .ui-icon { animation: ui-fab-busy-icon 0s linear 0.15s both; }
.ui-fab[aria-busy="true"]::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: var(--spacing-large);
    height: var(--spacing-large);
    box-sizing: border-box;
    border: var(--spacing-xxsmall) solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: layout-appear 0.2s ease-out 0.15s both, ui-spin 0.8s linear infinite;
}
@keyframes ui-fab-busy-icon { to { opacity: 0; } }

/* ui_list_item() / ui_list_group() — M3 two-line list item, ported from ~/Develop/Python ui/1_list_item.py */
.ui-list-group {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ui-list-item {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: 64px;
    background: var(--md-surface-container);
    padding: 10px 16px;
    box-sizing: border-box;
    border-radius: 4px;
    color: inherit;
    text-decoration: none;
}
.ui-list-item--overline { min-height: 80px; padding: 12px 16px; }
/* ui_list_field_item(): the field fills the row, 12px above and below it; an empty helper line takes no room there. */
.ui-list-item--field { padding: var(--spacing-small) var(--spacing-medium); }
.ui-list-item--field > .ui-text-field { flex: 1; min-width: 0; }
.ui-list-item--field .ui-text-field__supporting:empty { display: none; }
a.ui-list-item, button.ui-list-item { cursor: pointer; }
button.ui-list-item { border: 0; font: inherit; text-align: start; }
@media (hover: hover) { :is(a, button).ui-list-item:hover { background: color-mix(in srgb, var(--md-on-surface) 8%, var(--md-surface-container)); } }
:is(a, button).ui-list-item:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface) 10%, var(--md-surface-container)); }
@media (hover: hover) { :is(a, button).ui-list-item:not([aria-disabled="true"]):active { background: color-mix(in srgb, var(--md-on-surface) 10%, color-mix(in srgb, var(--md-on-surface) 8%, var(--md-surface-container))); } }
.ui-list-group > .ui-list-item:first-child { border-radius: 12px 12px 4px 4px; }
.ui-list-group > .ui-list-item:last-child { border-radius: 4px 4px 12px 12px; }
.ui-list-group > .ui-list-item:only-child { border-radius: 12px; }
.ui-list-group > .ui-list-item[aria-checked="true"] {
    background: var(--md-secondary-container);
    border-radius: 12px;
    cursor: default;
    pointer-events: none;
}
/* A ticked checkbox stays tappable (untick); its rounding stays the group's, so a run of ticked rows reads as one block. */
.ui-list-group > .ui-list-item[role="checkbox"][aria-checked="true"] { border-radius: 4px; }
.ui-list-group > .ui-list-item[role="checkbox"][aria-checked="true"]:not([aria-disabled="true"]) { cursor: pointer; pointer-events: auto; }
.ui-list-group > .ui-list-item[role="checkbox"][aria-checked="true"]:first-child { border-radius: 12px 12px 4px 4px; }
.ui-list-group > .ui-list-item[role="checkbox"][aria-checked="true"]:last-child { border-radius: 4px 4px 12px 12px; }
.ui-list-item[aria-checked="true"] .ui-list-item__headline,
.ui-list-item[aria-checked="true"] .ui-list-item__supporting,
.ui-list-item[aria-checked="true"] .ui-list-item__overline,
.ui-list-item[aria-checked="true"] .ui-list-item__trailing-text { color: var(--md-on-secondary-container); }
/* A frozen row (e.g. a quiz option that's no longer pickable) — same opacity 0.38 as ui_icon_button()/ui_fab()'s aria-disabled. */
.ui-list-item[aria-disabled="true"] { cursor: default; pointer-events: none; }
.ui-list-item[aria-disabled="true"] .ui-list-item__headline,
.ui-list-item[aria-disabled="true"] .ui-list-item__supporting,
.ui-list-item[aria-disabled="true"] .ui-list-item__overline,
.ui-list-item[aria-disabled="true"] .ui-list-item__trailing-text,
.ui-list-item[aria-disabled="true"] .ui-list-item__leading { opacity: 0.38; }
.ui-list-item__leading {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    margin-right: var(--spacing-medium);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--md-on-surface-variant);
}
.ui-list-item[aria-checked="true"] .ui-list-item__leading { color: var(--md-primary); }
.ui-list-item__leading--primary { color: var(--md-primary); }
.ui-list-item__leading--error { color: var(--md-error); }
.ui-list-item[aria-checked="true"] .ui-list-item__leading--radio > :last-child,
.ui-list-item[aria-checked="false"] .ui-list-item__leading--radio > :first-child { display: none; }
.ui-list-item__content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.ui-list-item__content--single-line { justify-content: center; }
.ui-list-item__overline {
    font-size: var(--md-label-small-size);
    font-weight: var(--md-label-small-weight);
    color: var(--md-on-surface-variant);
    width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ui-list-item__headline {
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    line-height: var(--md-body-large-line-height);
    color: var(--md-on-surface);
    width: 100%;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
}
.ui-list-item__supporting {
    margin: 0;
    font-size: var(--md-body-medium-size);
    font-weight: var(--md-body-medium-weight);
    line-height: var(--md-body-medium-line-height);
    color: var(--md-on-surface-variant);
    width: 100%;
    overflow: hidden;
    word-break: break-word;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
}
.ui-list-item__trailing-text {
    font-size: var(--md-label-small-size);
    font-weight: var(--md-label-small-weight);
    color: var(--md-on-surface-variant);
    flex-shrink: 0;
    margin-left: 8px;
}
.ui-list-item__trailing-button { flex-shrink: 0; margin-left: 8px; display: flex; align-items: center; }
/* ui_list_item(trailingHtml:): own controls at the row's end (a stepper, a button). */
.ui-list-item__trailing { flex-shrink: 0; margin-left: 8px; display: flex; align-items: center; }
.ui-list-item__trailing-icon {
    flex-shrink: 0;
    margin-left: 8px;
    display: flex;
    align-items: center;
    color: var(--md-on-surface-variant);
}
/* ui_list_item(badgeCount:) — an iOS-style count before the trailing icon (a group's new rows): a 24px circle in surface-container-highest that grows into a pill
   from two digits, 8px before the icon (user decision, 2026-11-01). Not the M3 error badge: it marks news, not a problem. */
.ui-list-item__badge { flex: none; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; min-width: 24px; height: 24px; margin-left: 8px; padding: 0 6px;
    border-radius: 12px; background: var(--md-surface-container-highest); color: var(--md-on-surface); font-size: var(--md-label-medium-size); font-weight: var(--md-label-medium-weight); line-height: var(--md-label-medium-line-height); }
.ui-list-item__badge[hidden] { display: none; }
/* ui_list_item(revealed:) — the check keeps its space while hidden, so revealing it never moves the headline. */
.ui-list-item__reveal { color: var(--md-primary); }
.ui-list-item:not([data-revealed]):not([data-wrong]) .ui-list-item__reveal { visibility: hidden; }
/* One slot, two icons: the check on a correct option, the red cross on a wrong pick (data-wrong), so the row never reflows. */
.ui-list-item__reveal .ui-icon + .ui-icon, .ui-list-item[data-wrong] .ui-list-item__reveal .ui-icon:first-child { display: none; }
.ui-list-item[data-wrong] .ui-list-item__reveal { color: var(--md-error); }
.ui-list-item[data-wrong] .ui-list-item__reveal .ui-icon + .ui-icon { display: inline; }

/* ui_section_header() — ported from ~/Develop/Python ui/1_detail_section.py detail_header(): 48px zone, text 8px
   above its block. `.layout-main` beats the plain `.layout-main h2` rule. Inside a --spacing-medium gap column the
   gap above counts toward the 48px and the gap below is cancelled, so header + gaps still total 48px. */
/* ui_section_footer(): 32px below the content (16px column gap + 16px padding), inset like section headers; icon above the text, both on-surface-variant. */
.layout-main .ui-section-footer { display: flex; flex-direction: column; align-items: flex-start; gap: var(--spacing-medium); padding: var(--spacing-medium) var(--spacing-medium) 0; color: var(--md-on-surface-variant); }
.ui-section-footer__text { font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }

.layout-main .ui-section-header {
    display: flex;
    align-items: flex-end;
    min-height: 48px;
    margin: 0;
    padding: 0 var(--spacing-medium) var(--spacing-xsmall);
    box-sizing: border-box;
    color: var(--md-secondary);
    font-size: var(--md-title-small-size);
    font-weight: var(--md-title-small-weight);
    line-height: var(--md-title-small-line-height);
}
.layout-main .ui-section-header--error { color: var(--md-error); }
.layout-content > .ui-section-header,
.ui-surface > .ui-section-header {
    min-height: calc(48px - var(--spacing-medium));
    margin-bottom: calc(-1 * var(--spacing-medium));
}
/* In a card grid it opens a new row and spans all columns (beats `.layout-content--grid > *`'s span 4). */
.layout-content--grid > .ui-section-header { grid-column: 1 / -1; }
/* A section's text line ("No exercises yet") spans the row like its header. */
.layout-content--grid > p { grid-column: 1 / -1; }
/* The first header of its block has nothing above it to separate from: no 48px zone, only its 8px above the block. */
.layout-main .ui-section-header:first-child { min-height: 0; }

/* ui_surface() — plain rounded surface-colored box, no Python source: holds arbitrary long-form content a
   list item/card can't (both clamp). Its own flex column + gap replaces the one .layout-content gives its
   direct children, so headings/paragraphs inside still space out the same way. */
.ui-surface {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-medium);
    background: var(--md-surface-container);
    border-radius: 16px;
    padding: var(--spacing-medium);
}

/* ui_card() — M3 card, ported from ~/Develop/Python ui/1_card.py. Background is --md-surface-container, not
   the source's --md-surface: in this app's token set --md-surface equals --md-background exactly (see :root
   above), so a card using --md-surface would exactly match the page behind it and disappear. */
.ui-card {
    display: flex;
    flex-direction: column;
    width: 100%;
    overflow: hidden;
    border-radius: 12px;
    box-sizing: border-box;
    background: var(--md-surface-container);
    color: inherit;
    text-decoration: none;
}
.ui-card--elevated { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15); }
.ui-card--outlined { border: 1px solid var(--md-outline-variant); }
.ui-card--disabled { opacity: 0.4; }
.ui-card--selected { background: var(--md-secondary-container); }
.ui-card--interactive { cursor: pointer; }
@media (hover: hover) {
    .ui-card--interactive:not(.ui-card--selected):hover {
        background: color-mix(in srgb, var(--md-on-surface) 8%, var(--md-surface));
        transition: background 150ms ease;
    }
}
/* Not while one of its own action buttons is pressed: that button shows its own pressed state. */
.ui-card--interactive:not(.ui-card--selected):active:not(:has(.ui-card__actions :active)) { background: color-mix(in srgb, var(--md-on-surface) 10%, var(--md-surface)); }
@media (hover: hover) { .ui-card--interactive:not(.ui-card--selected):active:not(:has(.ui-card__actions :active)) { background: color-mix(in srgb, var(--md-on-surface) 10%, color-mix(in srgb, var(--md-on-surface) 8%, var(--md-surface))); } }
/* The clickable region when action buttons are also present (see .claude/rules/shared.md): a flex column like the body, so its children lay out as before.
   Never display: contents: Chrome then finds no scroll box under the stretched overlay below, so a wheel or trackpad scroll over the picture did nothing
   (user report, 2026-11-02; the pixels stay the same). */
.ui-card__inner { display: flex; flex-direction: column; color: inherit; text-decoration: none; }
/* Stretched link: the media and padding sit outside the <a>, so its overlay covers the whole card (a tap on the picture used to do nothing);
   the action buttons stay above it and keep their own links. */
.ui-card--interactive { position: relative; }
.ui-card--interactive .ui-card__inner::after { content: ""; position: absolute; inset: 0; }
.ui-card--interactive .ui-card__actions { position: relative; z-index: 1; }

.ui-card__media { width: 100%; height: 188px; overflow: hidden; flex-shrink: 0; background: var(--md-surface-variant); }
.ui-card__media picture { display: contents; }
.ui-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* ui_card(mediaShape: 'whole'): the picture uncropped at its own ratio (its width/height attributes keep the room before it loads),
   at most as tall as the visible pane: the bar and 4 × 16px (the content's padding, from 600px the pane's margins), centered. */
.ui-card__media--whole { height: auto; }
.ui-card__media--whole img { height: auto; max-height: calc(100dvh - var(--layout-app-bar-height) - 4 * var(--spacing-medium)); object-fit: contain; }
/* ui_card_illustration() — icon + text sized to fit the 188px media height together. */
.ui-card__illustration {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    padding-bottom: var(--spacing-medium);
    gap: var(--spacing-xxxsmall);
}
/* $background/$foreground: one class per allowed M3 role (ui_card_illustration() rejects others) — new role = one line each here + its allowlist. */
.ui-card__illustration--bg-primary-container { background: var(--md-primary-container); }
.ui-card__illustration--bg-tertiary-container { background: var(--md-tertiary-container); }
.ui-card__illustration--fg-on-primary-container { color: var(--md-on-primary-container); }
.ui-card__illustration--fg-on-tertiary-container { color: var(--md-on-tertiary-container); }
/* ui_gallery() — one row that scrolls sideways and snaps to an item. It lines up with the content, and its track reaches
   through the 16px side margin to the pane's edge. Per row: 2 items and a fifth of the next; 3 and 4 once the row fits
   that many items of a 390px iPhone's 2-column width (176px): 3 × 176 + 2 × 8 = 544px, 4 × 176 + 3 × 8 = 728px.
   A container query, because the rail and the panes set the width. Items that all fit grow to share the row. */
.ui-gallery { container-type: inline-size; }
.layout-main .ui-gallery__track {
    --ui-gallery-columns: 2;
    display: flex;
    gap: var(--spacing-xsmall);
    margin: 0 calc(-1 * var(--spacing-medium));
    padding: 0 var(--spacing-medium);
    list-style: none;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--spacing-medium);
    scrollbar-width: none;
}
.ui-gallery__track::-webkit-scrollbar { display: none; }
@container (min-width: 544px) {
    .layout-main .ui-gallery__track { --ui-gallery-columns: 3; }
}
@container (min-width: 728px) {
    .layout-main .ui-gallery__track { --ui-gallery-columns: 4; }
}
.layout-main .ui-gallery__item {
    flex: 1 0 calc((100cqw - var(--ui-gallery-columns) * var(--spacing-xsmall)) / (var(--ui-gallery-columns) + 0.2));
    display: flex;
    min-width: 0;
    scroll-snap-align: start;
    font-size: inherit;
    line-height: inherit;
}
.ui-gallery__item > * { flex: 1; min-width: 0; }
.layout-main .ui-gallery--own-width .ui-gallery__item { flex: none; }
/* ui_gallery_arrows() in ui_section_header(trailingHtml:): 32px buttons centered on the 20px label line, glyph edge on the cards' edge. */
.layout-main .ui-section-header__label { margin: 0; font: inherit; color: inherit; }
.ui-gallery-arrows { display: flex; gap: var(--spacing-xxsmall); margin: -6px calc(-1 * var(--spacing-xsmall)) -6px auto; }
.ui-gallery-arrows[hidden] { display: none; }
.ui-card__illustration-textcol { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-xxsmall); }
.ui-card__illustration-text { font-size: var(--md-display-medium-size); font-weight: 500; line-height: 1; }
.ui-card__illustration-subtext { font-size: var(--md-body-large-size); font-weight: var(--md-body-large-weight); line-height: 1; }

/* The body fills a card that its row stretches, and the actions sit at its bottom: cards side by side keep their buttons on one line.
   Its parts have no flex gap: each spacing below is Figma's, set once where it applies (user decision, 2026-10-28). */
.ui-card__body { flex: 1; padding: var(--spacing-medium); display: flex; flex-direction: column; }
/* The head row (circle, header, subhead) sits above the media, 16px all round, as in Figma; a body right below it needs no top padding of its own. */
.ui-card__head { display: flex; align-items: center; gap: var(--spacing-medium); padding: var(--spacing-medium); box-sizing: border-box; width: 100%; color: inherit; text-decoration: none; }
.ui-card__head + .ui-card__body { padding-top: 0; }

/* ui_list_item(hasLeadingCircle:) shares the card's circle, so both look alike. */
.ui-card__circle, .ui-list-item__leading--circle {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--md-surface-variant);
    color: var(--md-on-surface-variant);
    display: flex;
    align-items: center;
    justify-content: center;
}
.ui-card__circle--high { background: var(--md-error-container); color: var(--md-on-error-container); }
.ui-card__circle--medium { background: var(--md-yellow-container); color: var(--md-on-yellow-container); }
.ui-card__circle--low { background: var(--md-secondary-container); color: var(--md-on-secondary-container); }
.ui-card__circle-text { font-size: var(--md-title-medium-size); font-weight: var(--md-title-medium-weight); line-height: 1; text-align: center; }
/* Priority circles show a whole word ("medium"): the smallest type-scale token, still fits the 40px circle. */
:is(.ui-card__circle--high, .ui-card__circle--medium, .ui-card__circle--low) .ui-card__circle-text { font-size: var(--md-label-small-size); font-weight: var(--md-label-small-weight); }

.ui-card__headcol { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.ui-card__header, .ui-card__title { color: var(--md-on-surface); }
.ui-card__subhead, .ui-card__supporting, .ui-card__trailing-text { color: var(--md-on-surface-variant); }
/* Two lines, not one: a quiz question ("Which countries belong to Central Europe?") must stay readable on a phone. */
.ui-card__header {
    font-size: var(--md-title-medium-size);
    font-weight: var(--md-title-medium-weight);
    line-height: var(--md-title-medium-line-height);
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.ui-card__subhead {
    font-size: var(--md-body-medium-size);
    font-weight: var(--md-body-medium-weight);
    line-height: var(--md-body-medium-line-height);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ui-card__badge {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--md-primary);
    display: flex;
    align-items: center;
    justify-content: center;
}
.ui-card__badge-count { font-size: 13px; font-weight: 700; color: var(--md-on-primary); line-height: 1; }
.ui-card__trailing-text { font-size: var(--md-label-small-size); font-weight: var(--md-label-small-weight); flex-shrink: 0; }

.ui-card__title {
    margin: 0;
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    line-height: var(--md-body-large-line-height);
    width: 100%;
    white-space: normal;
    word-break: break-word;
}
/* $subtitle (ui_card()): own column, right below $title. Title body-large 16/24, subtitle and supporting text body-medium 14/20, as in Figma. */
.ui-card__titlecol { display: flex; flex-direction: column; }
.ui-card__subtitle {
    margin: 0;
    font-size: var(--md-body-medium-size);
    font-weight: var(--md-body-medium-weight);
    line-height: var(--md-body-medium-line-height);
    color: var(--md-on-surface-variant);
    width: 100%;
    white-space: normal;
    word-break: break-word;
}
.ui-card__supporting {
    margin: 0;
    font-size: var(--md-body-medium-size);
    font-weight: var(--md-body-medium-weight);
    line-height: var(--md-body-medium-line-height);
    width: 100%;
    white-space: normal;
    word-break: break-word;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
}
/* 32px from the title (or its subtitle) to the supporting text, as in Figma. */
:is(.ui-card__title, .ui-card__titlecol) + .ui-card__supporting { margin-top: var(--spacing-xlarge); }
.ui-card__actions { display: flex; justify-content: flex-end; gap: var(--spacing-xsmall); margin-top: auto; }
/* Above the buttons: 32px after the supporting text (Figma), 16px after a title, subtitle or controls (user decision, 2026-10-28),
   and nothing more than the body's 16px padding when nothing stands before them. */
.ui-card__actions { padding-top: var(--spacing-medium); }
.ui-card__inner:has(> .ui-card__supporting:last-child) + .ui-card__actions { padding-top: var(--spacing-xlarge); }
.ui-card__inner:empty + .ui-card__actions { padding-top: 0; }
/* ui_card(bodyHtml:): live controls (a progress bar, steppers) below the text, 16px after it, 12px between them; Figma's card has none. */
.ui-card__controls { display: flex; flex-direction: column; gap: var(--spacing-small); }
.ui-card__controls:not(:first-child) { margin-top: var(--spacing-medium); }

.ui-card--selected .ui-card__header,
.ui-card--selected .ui-card__title,
.ui-card--selected .ui-card__subhead,
.ui-card--selected .ui-card__subtitle,
.ui-card--selected .ui-card__supporting,
.ui-card--selected .ui-card__trailing-text { color: var(--md-on-secondary-container); }

/* ui_loading_state() / ui_empty_state() / ui_error_state() — ported from ~/Develop/Python ui/1_page_state.py */
.ui-loading-state, .ui-empty-state, .ui-error-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-small);
    padding: calc(var(--spacing-xlarge) * 2) 0;
    text-align: center;
}
.ui-empty-state .ui-icon { color: var(--md-on-surface-variant); opacity: 0.4; }
.ui-error-state .ui-icon { color: var(--md-error); }
.ui-loading-state__label, .ui-empty-state__label, .ui-error-state__label {
    margin: 0;
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    line-height: 1.5;
    color: var(--md-on-surface-variant);
}
.ui-empty-state__label { opacity: 0.6; }
/* layout_error_page(): the state alone, centered in the whole pane instead of near the top. */
.layout-main:has(.layout-error-page) { display: flex; min-height: 100%; }
.layout-main:has(.layout-error-page) > .layout-content { justify-content: center; }
.layout-error-page .ui-empty-state__label { text-align: center; }
/* ui_progress_circular() — one spinner for the loading screen (large) and a list row's leading slot (small, the icon's glyph size): a primary arc. */
.ui-progress-circular {
    display: block;
    flex-shrink: 0;
    border: var(--spacing-xxsmall) solid transparent;
    border-top-color: var(--md-primary);
    border-radius: 50%;
    animation: ui-spin 0.8s linear infinite;
}
.ui-progress-circular--large { width: var(--spacing-xxlarge); height: var(--spacing-xxlarge); }
.ui-progress-circular--small { width: var(--spacing-medium); height: var(--spacing-medium); border-width: var(--spacing-xxxsmall); }
@keyframes ui-spin { to { transform: rotate(360deg); } }

/* ui_post_on_load() replaces the page's own (still-invisible-at-this-point) loading screen, often within its 150ms
   delay (its only real caller has no DB work before it) — appear instantly by default, or there's a blank gap where
   neither screen has faded in yet. Only fade (no further delay, .layout-content's own rule) when the page's loading
   screen was already visible for a while (.layout-was-loading) — an instant swap would be a jarring hard cut there. */
.ui-post-on-load__loading { animation: none; }
.layout-was-loading .ui-post-on-load__loading { animation: layout-appear 0.2s ease-out both; }
.ui-post-on-load [hidden] { display: none; }

/* An offline screen's outbox config (app_view_<container>_outbox_html(), .claude/rules/offline-sync.md): only a data carrier, so it never changes the layout. */
[data-offline-outbox] { display: contents; }
/* layout_profile_drawer()'s loading state: same 150ms grace as a page's loading screen — a fast panel reply never flashes a spinner. */
.ui-nav-drawer__state[data-drawer-state="loading"] .ui-loading-state { animation: layout-appear 0.2s ease-out 150ms both; }

/* ui_text_field() — M3 outlined text field, ported from ~/Develop/Python ui/1_text_field.py (outlined_text_field()).
   The label sits inside the box like a placeholder and floats onto the top border once focused or filled (:placeholder-shown,
   via the input's blank placeholder); its background cuts the border line, so it must match the page background. */
.ui-text-field { display: flex; flex-direction: column; }
.ui-text-field__box {
    position: relative;
    margin-top: var(--spacing-xsmall);
    border: 1px solid var(--md-outline);
    border-radius: 4px;
    transition: border-color 0.1s, box-shadow 0.1s;
}
.ui-text-field__input {
    width: 100%;
    height: 56px;
    box-sizing: border-box;
    padding: 0 var(--spacing-medium);
    border: none;
    outline: none;
    background: transparent;
    font: inherit;
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    color: var(--md-on-surface);
}
.ui-text-field__input::placeholder { color: transparent; }
/* iOS draws a date field with its native control, which ignored width: 100% and made the page scroll sideways (user's iPhone, 2026-10-12). */
/* Without its native look iOS puts the date at the top: the line height centers it in the 56px box. */
.ui-text-field__input[type="date"] { display: block; min-width: 0; line-height: 56px; -webkit-appearance: none; appearance: none; }
.ui-text-field__input[type="date"]::-webkit-date-and-time-value { margin: 0; line-height: 56px; text-align: start; }
.ui-text-field__label {
    position: absolute;
    top: 50%;
    left: var(--spacing-medium);
    transform: translateY(-50%);
    padding: 0;
    background: transparent;
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    color: var(--md-on-surface-variant);
    white-space: nowrap;
    pointer-events: none;
    transition: top 0.15s ease, left 0.15s ease, font-size 0.15s ease, padding 0.15s ease, color 0.1s ease;
}
.ui-text-field__box:focus-within .ui-text-field__label,
.ui-text-field__input:not(:placeholder-shown) + .ui-text-field__label {
    top: 0;
    left: var(--spacing-small);
    padding: 0 var(--spacing-xxsmall);
    background: var(--md-background);
    font-size: var(--md-body-small-size);
    font-weight: var(--md-body-small-weight);
}
@media (hover: hover) { .ui-text-field__box:hover { border-color: var(--md-on-surface); } }
.ui-text-field__box:focus-within { border-color: var(--md-primary); box-shadow: 0 0 0 1px var(--md-primary); }
.ui-text-field__box:focus-within .ui-text-field__label { color: var(--md-primary); }
.ui-text-field__supporting {
    margin: 0;
    padding: var(--spacing-xxsmall) var(--spacing-medium);
    font-size: var(--md-body-small-size);
    font-weight: var(--md-body-small-weight);
    line-height: var(--md-body-small-line-height);
    color: var(--md-on-surface-variant);
}
/* Error: app.js sets aria-invalid on the input after a rejected save and swaps the helper text for the server's reason. */
.ui-text-field:has([aria-invalid="true"]) .ui-text-field__box { border-color: var(--md-error); box-shadow: 0 0 0 1px var(--md-error); }
.ui-text-field:has([aria-invalid="true"]) .ui-text-field__label,
.ui-text-field:has([aria-invalid="true"]) .ui-text-field__supporting { color: var(--md-error); }

/* ui_text_field_search_menu() — a ui_text_field() with a filtered menu (combobox), ported from ~/Develop/Python ui/1_text_field_search_menu.py,
   the menu in 1_context_menu.py's panel look. app.js sets [data-open], [data-menu-above] (no room below) and [data-new] (a name without a match). */
.ui-text-field-search-menu__input { padding-inline: 52px 48px; }
.ui-text-field-search-menu .ui-text-field__label { left: 52px; }
.ui-text-field-search-menu__icon,
.ui-text-field-search-menu__arrow {
    position: absolute;
    top: 50%;
    display: flex;
    color: var(--md-on-surface-variant);
    pointer-events: none;
    transform: translateY(-50%);
}
.ui-text-field-search-menu__icon { left: var(--spacing-small); }
.ui-text-field-search-menu__arrow { right: var(--spacing-small); transition: transform 0.15s ease; }
.ui-text-field-search-menu[data-open] .ui-text-field-search-menu__arrow { transform: translateY(-50%) rotate(180deg); }
.ui-text-field-search-menu__icon--new,
.ui-text-field-search-menu[data-new] .ui-text-field-search-menu__icon { display: none; }
.ui-text-field-search-menu[data-new] .ui-text-field-search-menu__icon--new { display: flex; }
.ui-text-field-search-menu__menu {
    position: absolute;
    z-index: 3;
    top: calc(100% + var(--spacing-xxsmall));
    left: 0;
    right: 0;
    max-height: var(--ui-search-menu-max-height, 240px);
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: var(--spacing-xsmall) 0;
    list-style: none;
    border-radius: 16px;
    background: var(--md-surface-container);
    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.15);
    animation: ui-search-menu-in 0.12s ease;
}
/* Outranks .layout-main ul's list indent, which left a gap before the rows. */
.layout-main .ui-text-field-search-menu__menu { padding: var(--spacing-xsmall) 0; }
.ui-text-field-search-menu[data-menu-above] .ui-text-field-search-menu__menu { top: auto; bottom: calc(100% + var(--spacing-xxsmall)); }
.ui-text-field-search-menu__menu[hidden] { display: none; }
@keyframes ui-search-menu-in { from { opacity: 0; } }
.ui-text-field-search-menu__option {
    display: flex;
    align-items: center;
    gap: var(--spacing-small);
    height: 48px;
    margin: 0 var(--spacing-xxsmall);
    padding: 0 var(--spacing-small);
    border-radius: 12px;
    color: var(--md-on-surface);
    font-size: var(--md-body-large-size);
    font-weight: var(--md-body-large-weight);
    cursor: pointer;
    user-select: none;
}
.ui-text-field-search-menu__option[hidden] { display: none; }
.ui-text-field-search-menu__option-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-text-field-search-menu__option .ui-icon { flex-shrink: 0; visibility: hidden; }
.ui-text-field-search-menu__option[aria-selected="true"] { color: var(--md-on-secondary-container); background: var(--md-secondary-container); }
.ui-text-field-search-menu__option[aria-selected="true"] .ui-icon,
.ui-text-field-search-menu__option--new .ui-icon { visibility: visible; }
.ui-text-field-search-menu__option[data-active] { background: color-mix(in srgb, transparent, var(--md-on-surface) 8%); }
.ui-text-field-search-menu__option[aria-selected="true"][data-active] { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 8%); }
@media (hover: hover) {
    .ui-text-field-search-menu__option:hover { background: color-mix(in srgb, transparent, var(--md-on-surface) 8%); }
    .ui-text-field-search-menu__option[aria-selected="true"]:hover { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 8%); }
}
.ui-text-field-search-menu__option:active { background: color-mix(in srgb, var(--md-on-surface) 10%, transparent); }
@media (hover: hover) { .ui-text-field-search-menu__option:active { background: color-mix(in srgb, var(--md-on-surface) 10%, color-mix(in srgb, var(--md-on-surface) 8%, transparent)); } }
.ui-text-field-search-menu__option[aria-selected="true"]:active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, var(--md-secondary-container)); }
@media (hover: hover) { .ui-text-field-search-menu__option[aria-selected="true"]:active { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
@media (prefers-reduced-motion: reduce) {
    .ui-text-field-search-menu__menu { animation: none; }
    .ui-text-field-search-menu__arrow { transition: none; }
}

/* The shared profile and restore screens (shared/pages/, .claude/rules/profile.md): every app shows them, so they live here, not in an app's style.css. */
/* Profile home intro: same side inset as .ui-section-header so it lines up with the "Account" header. */
.layout-main .profile-intro { padding: 0 var(--spacing-medium); }

/* Delete step: fills the centered content column; the warning text is inset 32px, the field and button pull out to 16px from the edge. */
.layout-main .profile-delete { padding: 0 var(--spacing-medium); display: flex; flex-direction: column; gap: var(--spacing-large); }
.profile-delete .ui-text-field, .profile-delete .ui-button { margin-inline: calc(-1 * var(--spacing-medium)); }
.profile-delete .ui-button { justify-content: center; }

/* Backup step: same full-column layout as the delete step; the link box and button pull out to 16px from the edge (text stays at 32); the link breaks anywhere and is easy to select. */
.layout-main .profile-backup { padding: 0 var(--spacing-medium); display: flex; flex-direction: column; gap: var(--spacing-large); }
.profile-backup .ui-button { margin-inline: calc(-1 * var(--spacing-medium)); justify-content: center; }
.profile-restore { margin-inline: calc(-1 * var(--spacing-medium)); display: flex; align-items: center; gap: var(--spacing-small); padding: var(--spacing-medium); border-radius: var(--radius); background: var(--md-tertiary-container); color: var(--md-on-tertiary-container); }
.profile-restore .ui-icon-button { flex: none; color: var(--md-on-tertiary-container); }
.profile-restore-link { flex: 1; min-width: 0; margin: 0; overflow-wrap: anywhere; user-select: all; font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); }

/* QR code of the restore link: a white tile (the SVG carries its own quiet zone) with the hint below, centered. */
.profile-qr { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-small); text-align: center; }
.qr { display: block; width: min(100%, 280px); height: auto; border-radius: var(--radius); }

/* ui_app_header() — App Store style product header: icon, name, subtitle and the open button beside it. */
.ui-app-header { display: flex; align-items: center; gap: var(--spacing-medium); }
/* Apple's app icon corner is about 22.4% of its width; the generated icons are plain squares. */
.ui-app-header__icon { flex: none; width: 104px; height: 104px; border-radius: 22.4%; }
/* Stretched to the text column and scrolling sideways, so long button labels never push the header wider than a narrow phone. */
.ui-app-header__actions { display: flex; align-items: center; gap: var(--spacing-small); align-self: stretch; overflow-x: auto; scrollbar-width: none; }
.ui-app-header__actions::-webkit-scrollbar { display: none; }
.ui-app-header__actions > * { flex: none; }
.ui-app-header__text { display: flex; flex-direction: column; align-items: flex-start; gap: var(--spacing-xxsmall); min-width: 0; }
.layout-main .ui-app-header__name { margin: 0; color: var(--md-on-surface); font-size: var(--md-title-large-size); font-weight: var(--md-title-large-weight); line-height: var(--md-title-large-line-height); }
.layout-main .ui-app-header__subtitle { margin: 0 0 var(--spacing-xsmall); color: var(--md-on-surface-variant); font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }

/* ui_facts_row() — short facts with a dot between them. */
.layout-main .ui-facts-row { display: flex; flex-wrap: wrap; gap: var(--spacing-xxsmall) var(--spacing-small); margin: 0; padding: 0 var(--spacing-medium); list-style: none; color: var(--md-on-surface-variant); font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }
.ui-facts-row li + li::before { content: "·"; margin-right: var(--spacing-small); }

/* ui_screenshot_strip() — store screenshots in a ui_gallery() with their own width; each has its picture's own ratio,
   taken from the capture sizes in bin/browser/store-screenshots.mjs (narrow 390×844, wide 1280×800), so nothing is cropped. */
.ui-screenshot-strip__item { width: 200px; }
.ui-screenshot-strip__item img { display: block; width: 100%; height: auto; aspect-ratio: 390 / 844; border-radius: 16px; border: 1px solid var(--md-outline-variant); box-sizing: border-box; }
.layout-main .ui-screenshot-strip__title { margin: var(--spacing-xsmall) 0 0; padding: 0 var(--spacing-medium); color: var(--md-on-surface-variant); font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }
@media (min-width: 600px) {
    .ui-screenshot-strip__item { width: 360px; }
    .ui-screenshot-strip__item img { aspect-ratio: 1280 / 800; }
}

/* ui_sheet() — a modal <dialog> (.claude/rules/onboarding.md). Compact: an almost full-screen bottom sheet sliding up from the bottom edge.
   From 600px: the floating pane of ~/Develop/Python ui/1_floating_pane.py — centered, 24px corners, surface-container, 40% scrim,
   rises 80px while fading in (400ms), fades out (300ms). app.js adds --closing and closes the dialog once that animation ends. */
.ui-sheet {
    box-sizing: border-box;
    flex-direction: column;
    width: 100%;
    max-width: none;
    height: calc(100% - env(safe-area-inset-top) - var(--spacing-large));
    max-height: none;
    margin: auto 0 0;
    padding: 0;
    border: none;
    border-radius: 28px 28px 0 0;
    overflow: hidden;
    color: var(--md-on-surface);
    /* M3's sheet colour, a step lighter than a list item, so ui_list_group() rows show in a sheet as on a page. */
    background: var(--md-surface-container-low);
}
/* The dialog itself takes the focus on open (autofocus + app.js), so the button shows no focus ring; Tab still reaches it. */
.ui-sheet:focus { outline: none; }
.ui-sheet[open] { display: flex; animation: ui-sheet-slide-in 400ms cubic-bezier(0.2, 0, 0, 1) both; }
.ui-sheet[open].ui-sheet--closing { animation: ui-sheet-slide-out 300ms cubic-bezier(0.4, 0, 1, 1) both; }
.ui-sheet::backdrop { background: rgba(0, 0, 0, 0.4); }
.ui-sheet[open]::backdrop { animation: ui-sheet-fade-in 400ms ease both; }
.ui-sheet[open].ui-sheet--closing::backdrop { animation: ui-sheet-fade-out 300ms ease both; }
/* The page behind stays still while the sheet is open (it is inert, but a wheel or a drag could still scroll it). */

.ui-sheet__scroll {
    flex: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--spacing-xxlarge) max(var(--spacing-large), env(safe-area-inset-right)) var(--spacing-large) max(var(--spacing-large), env(safe-area-inset-left));
}
.layout-main .ui-sheet__title {
    margin: 0 0 var(--spacing-xlarge);
    text-align: center;
    color: var(--md-on-surface);
    font-size: var(--md-headline-medium-size);
    font-weight: var(--md-title-medium-weight);
    line-height: var(--md-headline-medium-line-height);
}
.ui-sheet__actions {
    flex-shrink: 0;
    display: flex;
    justify-content: center;
    padding: var(--spacing-medium) var(--spacing-large) calc(var(--spacing-large) + env(safe-area-inset-bottom));
}
.ui-sheet__actions .ui-button { width: 100%; max-width: 360px; }
@media (min-width: 600px) {
    /* The iPad form sheet's 540×620 size, shrinking only to keep 32px on every side (user decision, 2026-10-09).
       A fixed height, never fit-content: the flex: 1 scroll area collapsed to nothing in Safari. */
    .ui-sheet { width: min(100% - 2 * var(--spacing-xlarge), 540px); height: min(100dvh - 2 * var(--spacing-xlarge), 620px); margin: auto; border-radius: 24px; }
    .ui-sheet[open] { animation-name: ui-sheet-rise; }
    .ui-sheet[open].ui-sheet--closing { animation-name: ui-sheet-fade-out; }
    .ui-sheet__actions { padding-bottom: var(--spacing-large); }
}
@media (prefers-reduced-motion: reduce) {
    .ui-sheet[open] { animation-name: ui-sheet-fade-in; }
    .ui-sheet[open].ui-sheet--closing { animation-name: ui-sheet-fade-out; }
}
/* A dismissable sheet (ui_sheet(id:)): the close icon sits top-right; a form around body and actions keeps the dialog one flex column. */
.ui-sheet__form { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ui-sheet__close { position: absolute; z-index: 1; top: var(--spacing-small); inset-inline-end: max(var(--spacing-small), env(safe-area-inset-right)); }
.ui-sheet__scroll > .ui-segmented-button + .ui-segmented-button { margin-top: var(--spacing-xlarge); }
/* ui_sheet(appBar: true), a tool sheet (the developer mode): our app bar (layout_pane_app_bar(), its close icon at the end) at the sheet's top instead of the window's,
   on the sheet's own colour; it stays while the content scrolls. The content keeps 16px from its sides and bottom (the bar is its top), and an empty actions row takes no room (user decision, 2026-10-31). */
.ui-sheet--app-bar > .app-bar { position: relative; top: auto; flex: none; height: 64px; padding-top: 0; background: transparent; z-index: auto; }
/* The sheet sits inside .layout-main, whose h2 rule (16px top margin, on-background) would push the bar's title down; the page's own bar sits outside it. */
.ui-sheet--app-bar > .app-bar > .app-bar__title { margin: 0; color: var(--md-on-surface); }
/* While the content is scrolled the bar takes its tint, one step above the sheet's own colour, like a page's bar (app.js). */
.ui-sheet--app-bar > .app-bar.app-bar--scrolled { background: var(--md-surface-container); }
.ui-sheet--app-bar .ui-sheet__scroll { padding: 0 max(var(--spacing-medium), env(safe-area-inset-right)) max(var(--spacing-medium), env(safe-area-inset-bottom)) max(var(--spacing-medium), env(safe-area-inset-left)); }
.ui-sheet--app-bar .ui-sheet__actions:empty { display: none; }
/* A docked toolbar in the actions row: edge to edge at the sheet's bottom, the row's own padding gone; its buttons keep their own width. */
.ui-sheet__actions:has(> .ui-docked-toolbar) { padding: 0; display: block; }
.ui-sheet__actions .ui-docked-toolbar .ui-button { width: auto; max-width: none; }
/* Like the sheet's app bar, it rests on the sheet's own colour and takes surface-container while content lies under it (app.js, user decision, 2026-11-02). */
.ui-sheet__actions > .ui-docked-toolbar { background: transparent; }
.ui-sheet__actions > .ui-docked-toolbar.ui-docked-toolbar--scrolled { background: var(--md-surface-container); }
/* On a page the same 32px between two choices: the margin tops up .layout-content's gap. */
.layout-content > .ui-segmented-button + .ui-segmented-button { margin-top: calc(var(--spacing-xlarge) - var(--spacing-medium)); }

/* ui_segmented_button() — M3 connected segmented button, ported from ~/Develop/Python ui/1_segmented_button.py: 40px pills joined by a 2px gap,
   the selected one fully rounded in --md-secondary with a check. The radios are visually hidden but focusable. */
.ui-segmented-button { margin: 0; padding: 0; border: none; min-width: 0; }
.ui-segmented-button__legend {
    padding: 0 var(--spacing-medium) var(--spacing-xsmall);
    color: var(--md-secondary);
    font-size: var(--md-title-small-size);
    font-weight: var(--md-title-small-weight);
    line-height: var(--md-title-small-line-height);
}
/* Read by screen readers, not shown: the section title above already names the choice. */
.ui-segmented-button__legend--hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* The segments share the row equally (basis 0, so the selected one's check does not widen it), each at least its content's width;
   a row too long for it scrolls by itself, never the page. */
.ui-segmented-button__row { display: flex; gap: 2px; height: 40px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none; }
.ui-segmented-button__option { position: relative; flex: 1 0 0; display: flex; cursor: pointer; }
.ui-segmented-button__input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.ui-segmented-button__pill {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-xsmall);
    padding: 0 var(--spacing-medium);
    border-radius: 8px;
    white-space: nowrap;
    color: var(--md-on-secondary-container);
    background: var(--md-secondary-container);
    font-size: var(--md-label-large-size);
    font-weight: var(--md-label-large-weight);
    transition: background 150ms ease;
}
.ui-segmented-button__option:first-child .ui-segmented-button__pill { border-radius: 20px 8px 8px 20px; }
.ui-segmented-button__option:last-child .ui-segmented-button__pill { border-radius: 8px 20px 20px 8px; }
.ui-segmented-button__option:only-child .ui-segmented-button__pill { border-radius: 20px; }
@media (hover: hover) { .ui-segmented-button__option:hover .ui-segmented-button__pill { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 8%); } }
.ui-segmented-button__option:active .ui-segmented-button__pill { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 10%); }
@media (hover: hover) { .ui-segmented-button__option:active .ui-segmented-button__pill { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
.ui-segmented-button__input:checked + .ui-segmented-button__pill { border-radius: 20px; color: var(--md-on-secondary); background: var(--md-secondary); }
.ui-segmented-button__input:focus-visible + .ui-segmented-button__pill { outline: 2px solid var(--md-primary); outline-offset: 2px; }
/* A disabled segment (set by the page's script) looks like a disabled ui_button(). */
.ui-segmented-button__input:disabled { cursor: default; }
.ui-segmented-button__input:disabled + .ui-segmented-button__pill {
    background: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
    color: color-mix(in srgb, var(--md-on-surface) 38%, transparent);
}
.ui-segmented-button__pill .ui-icon { display: none; }
.ui-segmented-button__input:checked + .ui-segmented-button__pill .ui-icon { display: inline-block; }

/* ui_filter_chips() — M3 filter chips: 32px outlined pills that wrap; a ticked one fills with --md-secondary-container and shows a check. The checkboxes are visually hidden but focusable. */
.ui-filter-chips { margin: 0; padding: 0; border: none; min-width: 0; }
.ui-filter-chips__legend {
    padding: 0 var(--spacing-medium) var(--spacing-xsmall);
    color: var(--md-secondary);
    font-size: var(--md-title-small-size);
    font-weight: var(--md-title-small-weight);
    line-height: var(--md-title-small-line-height);
}
.ui-filter-chips__row { display: flex; flex-wrap: wrap; gap: var(--spacing-small); }
.ui-filter-chips__chip { position: relative; display: flex; cursor: pointer; }
.ui-filter-chips__input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.ui-filter-chips__pill {
    display: flex;
    align-items: center;
    gap: var(--spacing-xsmall);
    height: 32px;
    padding: 0 var(--spacing-medium);
    border: 1px solid var(--md-outline-variant);
    border-radius: 8px;
    white-space: nowrap;
    color: var(--md-on-surface-variant);
    font-size: var(--md-label-large-size);
    font-weight: var(--md-label-large-weight);
    transition: background 150ms ease;
}
.ui-filter-chips__input:checked + .ui-filter-chips__pill { padding-inline-start: var(--spacing-small); border-color: transparent; color: var(--md-on-secondary-container); background: var(--md-secondary-container); }
.ui-filter-chips__input:focus-visible + .ui-filter-chips__pill { outline: 2px solid var(--md-primary); outline-offset: 2px; }
.ui-filter-chips__pill .ui-icon { display: none; }
.ui-filter-chips__input:checked + .ui-filter-chips__pill .ui-icon { display: inline-block; }
@media (hover: hover) {
    .ui-filter-chips__chip:hover .ui-filter-chips__pill { background: color-mix(in srgb, transparent, var(--md-on-surface-variant) 8%); }
    .ui-filter-chips__chip:hover .ui-filter-chips__input:checked + .ui-filter-chips__pill { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 8%); }
}
.ui-filter-chips__chip:active .ui-filter-chips__pill { background: color-mix(in srgb, transparent, var(--md-on-surface-variant) 10%); }
@media (hover: hover) { .ui-filter-chips__chip:active .ui-filter-chips__pill { background: color-mix(in srgb, var(--md-on-surface-variant) 10%, color-mix(in srgb, var(--md-on-surface-variant) 8%, transparent)); } }
.ui-filter-chips__chip:active .ui-filter-chips__input:checked + .ui-filter-chips__pill { background: color-mix(in srgb, var(--md-secondary-container), var(--md-on-secondary-container) 10%); }
@media (hover: hover) { .ui-filter-chips__chip:active .ui-filter-chips__input:checked + .ui-filter-chips__pill { background: color-mix(in srgb, var(--md-on-secondary-container) 10%, color-mix(in srgb, var(--md-on-secondary-container) 8%, var(--md-secondary-container))); } }
/* ui_filter_chips(requiredHint:): shown while no chip is ticked, styled like a text field's error helper. */
.ui-filter-chips__hint {
    margin: var(--spacing-xsmall) 0 0;
    padding: 0 var(--spacing-medium);
    color: var(--md-error);
    font-size: var(--md-body-small-size);
    font-weight: var(--md-body-small-weight);
    line-height: var(--md-body-small-line-height);
}
.ui-filter-chips__hint[hidden] { display: none; }
@keyframes ui-sheet-slide-in { from { transform: translateY(100%); } }
@keyframes ui-sheet-slide-out { to { transform: translateY(100%); } }
@keyframes ui-sheet-rise { from { opacity: 0; transform: translateY(80px); } }
@keyframes ui-sheet-fade-in { from { opacity: 0; } }
@keyframes ui-sheet-fade-out { to { opacity: 0; } }

/* ui_dialog() — M3 basic dialog, ported from ~/Develop/Python ui/1_basic_dialog.py: centered card, 28px corners, 32% scrim,
   scales in from 0.92 (200ms) and out (150ms). app.js adds --closing and closes the dialog once that animation ends.
   The padding sits on __surface, so a click whose target is the <dialog> itself is a tap on the scrim.
   It centers in the area above the keyboard and scrolls itself when that is too low (.claude/rules/keyboard.md). */
.ui-dialog {
    box-sizing: border-box;
    inset-block: calc(var(--layout-keyboard-top, 0px) + env(safe-area-inset-top)) max(var(--layout-keyboard-inset, 0px), env(safe-area-inset-bottom));
    width: min(312px, 100% - 2 * var(--spacing-medium));
    max-width: none;
    max-height: calc(100% - var(--layout-keyboard-top, 0px) - env(safe-area-inset-top) - max(var(--layout-keyboard-inset, 0px), env(safe-area-inset-bottom)) - 2 * var(--spacing-xsmall));
    margin: auto;
    padding: 0;
    border: none;
    border-radius: 28px;
    overflow: hidden auto;
    overscroll-behavior: contain;
    color: var(--md-on-surface);
    background: var(--md-surface-container-high);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.ui-dialog:focus { outline: none; }
.ui-dialog[open] { animation: ui-dialog-in 200ms cubic-bezier(0.2, 0, 0, 1) both; }
.ui-dialog[open].ui-dialog--closing { animation: ui-dialog-out 150ms cubic-bezier(0.4, 0, 1, 1) both; }
.ui-dialog::backdrop { background: rgba(0, 0, 0, 0.32); }
.ui-dialog[open]::backdrop { animation: ui-sheet-fade-in 200ms ease both; }
.ui-dialog[open].ui-dialog--closing::backdrop { animation: ui-sheet-fade-out 150ms ease both; }
.ui-dialog__surface { padding: var(--spacing-large) var(--spacing-large) 0; }
.layout-main .ui-dialog__title {
    margin: 0 0 var(--spacing-medium);
    color: var(--md-on-surface);
    font-size: var(--md-headline-small-size);
    font-weight: var(--md-headline-small-weight);
    line-height: var(--md-headline-small-line-height);
}
/* A text field in a dialog ($bodyHtml): its floating label cuts the border in the dialog's color, not the page's. */
.ui-dialog .ui-text-field__box:focus-within .ui-text-field__label,
.ui-dialog .ui-text-field__input:not(:placeholder-shown) + .ui-text-field__label { background: var(--md-surface-container-high); }
.ui-dialog__body { color: var(--md-on-surface-variant); font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }
/* Side by side, never wrapped (Python: 16px to the card's edge, so the German labels still fit in 312px). */
.ui-dialog__actions { display: flex; justify-content: flex-end; gap: var(--spacing-xsmall); margin-inline: calc(-1 * var(--spacing-xsmall)); padding: var(--spacing-large) 0; }
@media (prefers-reduced-motion: reduce) {
    .ui-dialog[open] { animation-name: ui-sheet-fade-in; }
    .ui-dialog[open].ui-dialog--closing { animation-name: ui-sheet-fade-out; }
}
@keyframes ui-dialog-in { from { opacity: 0; transform: scale(0.92); } }
@keyframes ui-dialog-out { to { opacity: 0; transform: scale(0.92); } }

/* ui_feature_list() — the points of an iOS welcome sheet: a big primary icon beside a bold title and a short description. */
.layout-main .ui-feature-list { display: flex; flex-direction: column; gap: var(--spacing-large); max-width: 480px; margin: 0 auto; padding: 0; list-style: none; }
.layout-main .ui-feature-list__item { display: flex; align-items: flex-start; gap: var(--spacing-medium); }
.ui-feature-list__item > .ui-icon { flex-shrink: 0; color: var(--md-primary); }
.ui-feature-list__text { display: flex; flex-direction: column; gap: var(--spacing-xxsmall); min-width: 0; }
.layout-main .ui-feature-list__title { margin: 0; color: var(--md-on-surface); font-size: var(--md-title-medium-size); font-weight: var(--md-title-medium-weight); line-height: var(--md-title-medium-line-height); }
.ui-feature-list__description { color: var(--md-on-surface-variant); font-size: var(--md-body-medium-size); font-weight: var(--md-body-medium-weight); line-height: var(--md-body-medium-line-height); }
