/* every flex ancestor of a .scroll-list needs min-height: 0 */

.app-chrome {
    flex: 0 0 auto;
    height: var(--app-chrome-height);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--space-5);
    background: var(--color-chrome);
    border-bottom: 1px solid var(--color-chrome-edge);
    /* chrome band is app furniture, not selectable content */
    -webkit-user-select: none;
    user-select: none;
}

.app-chrome__home {
    /* POC shortcut: the wordmark is the only global nav affordance.
       Sprint 3+ may grow it into a dropdown menu or expandable nav. */
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-md);
    font-size: var(--text-md);
    line-height: var(--leading-md);
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-tight);
    color: var(--color-chrome-text);
    background: transparent;
    text-decoration: none;
    transition: background-color 120ms ease, color 120ms ease;
}

/* built from a bordered box - no image asset needed */
.app-chrome__home::before {
    content: "";
    width: 14px;
    height: 14px;
    border-radius: var(--radius-sm);
    border: 4px solid var(--color-chrome-mark);
    flex: 0 0 auto;
}

.app-chrome__home:hover {
    background: var(--color-chrome-hover);
    color: var(--color-chrome-text);
}

.app-chrome__home:active {
    background: var(--color-chrome-active);
}

.app-chrome__settings,
.app-chrome__theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-chrome-muted);
    line-height: 1;
    transition: background-color 120ms ease, color 120ms ease;
}

.app-chrome__settings {
    margin-left: auto;
    text-decoration: none;
}

.app-chrome__settings svg,
.app-chrome__theme-toggle svg {
    width: 18px;
    height: 18px;
}

.app-chrome__settings:hover,
.app-chrome__theme-toggle:hover {
    background: var(--color-chrome-hover);
    color: var(--color-chrome-text);
}

.app-chrome__settings:active,
.app-chrome__theme-toggle:active {
    background: var(--color-chrome-active);
}

.app-chrome__settings:focus-visible,
.app-chrome__theme-toggle:focus-visible {
    outline: 2px solid var(--color-chrome-mark);
    outline-offset: 2px;
}

.app-chrome__settings:focus:not(:focus-visible),
.app-chrome__theme-toggle:focus:not(:focus-visible) {
    outline: none;
}

/* divides the nav gear and theme toggle */
.app-chrome__sep {
    flex: 0 0 auto;
    align-self: center;
    width: 1px;
    height: var(--space-5);
    margin: 0 var(--space-1);
    background: var(--color-chrome-edge);
}

/* hierarchy context label (CLIENT / SITE / PROJECT) above a title */
.page-eyebrow {
    display: block;
    /* edited here, not the --text-xs token, to avoid affecting other 12px uses */
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--color-text-subtle);
}

/* ---- Floating-shell metrics (Phase 5B, HIG-REBUILD-PLAN §3.1.3) -------------------------
   POC shortcut: these belong in tokens.css beside the other theme-independent scales. They
   are authored here because Phase 5B splits tokens.css and components.css across concurrent
   agents; fold them into tokens.css's :root when the phase closes. Values only, no colours -
   the tokens-only rule is unaffected.

   --page-header-h is the NOMINAL FALLBACK ONLY, in the same spirit as --vh's `1vh`: the header
   band is content-sized and ranges from ~63px (a settings title) to ~210px (a browse title over
   a wrapped BrowseToolbar at 375px), so no static value is right anywhere but by accident. The
   real value is measured by theme.module.js (a ResizeObserver on .page__header, re-seeded on the
   page-enter animation because the Router replaces the header on every navigation) and published
   onto the .page element as an inline custom property, exactly as it publishes --vh/--vw. This
   declaration is what keeps the page laid out for the frames before that lands, and on a host
   with no ResizeObserver - so every consumer reads the bare var(), never a hardcoded 5rem.

   --tab-bar-height is the other half of the contract MainLayout.razor.css asks for by name: its
   measured natural height at default metrics (44px tab floor + 2x --space-1 pill padding +
   2x --space-2 bar padding). It must never be set below that, or the reservation under-reserves
   and the last row hides behind the bar. Its z rung - and the floating header's - are NOT here:
   --z-tab-bar and --z-page-chrome live in tokens.css's layer ladder with the rest of the --z-*
   scale, so a reader auditing the ladder finds every rung in one place.

   --tab-bar-inset is 0 unless the phone shell is up, so desktop and tablet reserve nothing; the
   two Hidden-mode canvas roots are excluded further down instead, because bar visibility is
   route-derived in MainLayout and reaches CSS only as those two page classes. */
:root {
    --page-header-h: 5rem;
    --tab-bar-height: 4.25rem;
    --tab-bar-inset: 0px;
}

/* Keyed on the phone shell class rather than a new bar-specific one: MainLayout renders the bar
   under exactly `PlatformInfo.IsPhone && _barVisible`, and the only routes that clear
   _barVisible are BottomBarMode.Hidden - the canvas editor and registration - which are already
   named as page classes below. env() resolves UNMAGNIFIED and is then blown up by the page zoom,
   so it goes through the /var(--ui-scale, 1) divisor - the contract at tokens.css:136-141, and
   the trap that already bit two reads (§3.6.2). Matches .tab-bar's own bottom offset exactly. */
.app-shell--phone {
    --tab-bar-inset: calc(var(--tab-bar-height) + env(safe-area-inset-bottom, 0px) / var(--ui-scale, 1));
}

/* Phase 5B repeals CLAUDE.md rule 3 for this chain: .page and .page__body give up
   `overflow: hidden` so the scroller runs full-bleed under the floating header and tab bar.
   .scroll-list is now the ONLY clipping boundary inside a page.

   What the removed clamp was actually doing, verified before removal:
     - It was NOT containing .page-enter. That keyframe scales 0.985 -> 1 and never overshoots,
       so it never had anything to clip (the note beside the animation rule says as much); the
       clamp only ever constrained which easing that animation was allowed to use.
     - It was NOT what keeps a modal on screen. .modal-backdrop is position:fixed and .page is
       not a fixed containing block, so overflow here never clipped it; .modal's own max-height
       is what makes a tall modal reachable, and that is untouched.
     - It WAS a backstop against in-flow content spilling out of the page box. That backstop
       survives one level up: .app-shell__body keeps `overflow: hidden`, so nothing can escape
       the window - a spill can now overlap page chrome, but it can never become unreachable.

   The min-height:0 chain is unchanged by the repeal and still holds, because `min-height: 0`
   -- not `overflow` -- is what stops a flex item growing to its content:
     .app-shell (flex column, min-height:0)
       -> .app-shell__body (flex:1 1 auto, min-height:0, definite height)
         -> .page (height:100% against it, flex column)
           -> .page__body (flex:1 1 auto, min-height:0)
             -> .scroll-list (flex:1 1 auto, min-height:0, overflow-y:auto)
   Floating the header REMOVES a flex row from that chain rather than adding one, so
   .page__body now takes the whole of .page instead of the remainder. Nothing in the chain
   depended on the header's row.

   position: relative is deliberate and is the header's containing block. It is NOT a
   containing block for position:fixed descendants - only transform, perspective, filter,
   backdrop-filter, contain: layout/paint/strict/content, content-visibility and will-change of
   those create one, and none of them is set here. So the five DesignEditor modal backdrops,
   .context-menu, .toast and .debug-dock still position against the viewport, unmoved (§3.1.4).
   It does move the containing block for position:ABSOLUTE descendants from .app-shell__body to
   .page -- which is a geometric no-op, since .page fills .app-shell__body's padding box exactly
   (height:100%, no margins, no padding on either). It is also already the state every absolute
   descendant sees for the 200ms the page-enter transform is running, so nothing can have been
   relying on .app-shell__body specifically. z-index stays auto: .page must not become a
   stacking context, or an open modal would be trapped inside the page it was opened from. */
.page {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    position: relative;
}

/* Floats over .page__body instead of claiming a flex row, so content scrolls UNDER it
   (§3.1.3). Absolute, not fixed: the containing block must be the page, not the viewport --
   fixed would also survive into the tablet/desktop shell where the page is not the whole
   window. Opaque by ruling: floating is a layout change, glass is a material one, and the
   three-surface glass scope did not move (§3.1). Pages that already paint their own header
   background override this harmlessly. */
.page__header {
    position: absolute;
    inset: 0 0 auto 0;
    z-index: var(--z-page-chrome);
    background: var(--color-bg);
}

/* The scroll-edge effect. theme.module.js publishes --scroll-edge-top as a unitless 0..1 ramp on
   <html>; this is the look it drives - a separation edge that fades in as content starts passing
   under the band, and nothing at all at rest. A separate strip rather than a box-shadow on the
   header itself, so only its opacity animates and the header's own background/border stay put.
   Opaque header, so this is a shadow cue and not a blur (§3.1). Always keep the `, 0` fallback:
   the custom property does not exist until the module runs, and an unresolved var() drops the
   whole declaration. */
.page__header::after {
    content: "";
    position: absolute;
    inset: auto 0 -1px 0;
    height: 1px;
    background: var(--color-border);
    box-shadow: var(--shadow-1);
    opacity: var(--scroll-edge-top, 0);
    transition: opacity var(--motion-base) var(--ease-out);
    pointer-events: none;
}

/* No floating band on the static-header roots, so no edge cue either. */
.page--static-header > .page__header::after,
.page.design-editor > .page__header::after,
.page.register-page > .page__header::after {
    content: none;
}

/* The two full-bleed canvas roots keep an IN-FLOW header. Both publish BottomBarMode.Hidden
   and are never overlaid by the floating bar (§3.1.3, "scope of the repeal"), and neither has
   a scroller to pass under the band: their bodies are a fixed-height canvas grid, so floating
   the header would slide the canvas up under it and move the coordinate origin Phase 5 has
   just settled. .page--static-header is the general escape hatch; the two roots are named
   directly so neither designs.css nor register.css has to be edited for this. Background is
   left alone -- both header classes paint their own, and resetting it here would beat them. */
.page--static-header > .page__header,
.page.design-editor > .page__header,
.page.register-page > .page__header {
    position: static;
    z-index: auto;
    flex: 0 0 auto;
}

/* App bar: back arrow | title stack | trailing action. Page sheets own the band itself. */
.app-bar__back,
.app-bar__action {
    flex: 0 0 auto;
    color: var(--color-text-muted);
}

/* Phone app bar: both buttons are 44px, so centring the stack centres the title. */
@media (max-width: 599px) {
    .app-bar__title-block {
        align-items: center;
        text-align: center;
    }
}

/* overflow:hidden dropped with .page's - see the block above. min-height:0 is what holds the
   chain together and stays. */
.page__body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.scroll-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* overflow-x:hidden - overflow-y alone let iPhone pan sideways */
    overflow-x: hidden;
    /* keeps list scroll from chaining to the document (Chromium) */
    overscroll-behavior: contain;
}

/* HEADER RESERVATION - on the first IN-FLOW child of .page, not on .scroll-list.
   Phase 5B's first cut put a ::before spacer inside .scroll-list, which reserves for exactly one
   element and silently leaves every OTHER child of .page__body starting at y:0 underneath the
   opaque floating header. Measured in Chromium against these sheets: SiteDetail's .site-detail-
   page__tabs strip 100% hidden at every width (the page's primary navigation), its toolbar
   partly; Photos' .photos-rail covered for its top 133px at 375-768 (the whole chip strip);
   conditional InlineAlerts likewise. So reservation moves OUT of the scroller and onto the first
   in-flow sibling after the header, which is the ancestor of all of them.

   `.page__header + *` rather than `.page__body` on purpose, and it is what makes this general:
     - on most pages that IS .page__body, and every child of it - tabs, rails, alerts, and the
       ones nobody has written yet - clears the band for free;
     - on the SOW/CO editors the totals band is a THIRD .page child, a sibling of both header and
       body, so .page__body would have reserved a second time under a band that had already
       cleared the header by hand (measured: 210px of dead space at every width). The adjacent
       combinator lands on whichever of the two is actually first;
     - that band renders conditionally (@if _doc is not null). When it is absent the rule falls
       through to .page__body by itself. No per-page override in either direction.

   Specificity is deliberately heavy (0,5,0). It has to outrank the `padding` shorthands feature
   sheets set on these same elements (.register-page__body, .sites-page__scroll, ...), which is
   the exact objection that sent the first cut to spacers. The cost is real and accepted: where a
   scroller declared its own top gutter, this REPLACES it rather than adding, so a first row now
   sits flush under the band instead of a gutter below it.

   The three static-header roots are excluded by :not() rather than by a `padding-top: 0` reset,
   because a reset would also wipe the top gutter those pages set for themselves (.register-page
   __body's --space-3). Chained single-arg :not() instead of the one-selector-list form - no
   :not(a, b, c) or :where() appears anywhere in these sheets yet, and this is not the rule to
   introduce a Chrome-88 floor on. */
.page:not(.page--static-header):not(.design-editor):not(.register-page) > .page__header + * {
    padding-top: var(--page-header-h);
}

/* ...and the scroller pulls straight back up under the band. Net displacement of its CONTENT is
   zero (-H margin + H padding), so the first row still starts exactly where the reservation put
   it - but the scroll BOX now spans the whole body again, which is the entire point of a true
   float: content passes BENEATH the header rather than being clipped at its lower edge, and the
   .page__header::after scroll-edge cue has something to fade in over. It also keeps the scrollbar
   track and the scroll extent full-height, as the ::before spacer did.
   SCOPED TO :first-child, and that scoping is the whole correctness argument. The pull-back is
   only sound when the scroller is what carries the reservation padding. The first cut used a
   descendant selector and an UNCONDITIONAL subtraction, and the claim in this comment used to be
   that "the negative margin only cancels the padding it is paired with, so anything above the
   scroller keeps its own place." That is false. Where anything sits between the header and the
   scroller, the scroller's box is pulled up THROUGH it, and - being a later sibling with no
   stacking context - its content paints on top. Measured in Chromium at scrollTop 400: SOW/CO's
   totals band overlapped 79-99px (a scrolled BOM card overwrote the last digit of the client
   total), SiteDetail's tab+search band 46-87px (the Projects/Photos segmented sliced in half),
   Photos' filter rail 62-69px (and that rail is background:transparent under 900px, so there was
   not even a surface to hide behind). Every viewport, desktop included.

   So: pull back only when the scroller is the first in-flow child of .page__body. Pages with a
   fixed band between header and scroller (SiteDetail, Photos, SOW/CO, and any page rendering a
   conditional InlineAlert or SignInBanner) reserve correctly and simply do not scroll under the
   header - which is the safe half of the trade, because those bands are fixed chrome by intent
   (sites.css: "Fixed band - search/sort never scroll away with the results").

   FOLLOW-UP to restore true float on those pages: move the fixed bands INTO .page__header so the
   measured header height covers them and .page__body holds only the scroller. That is where fixed
   chrome belongs and it is the HIG-correct structure - content then passes under the whole
   floating region rather than under a fragment of it. Doing it here would have meant restructuring
   four pages' markup in a phase that already cannot be verified on a device.

   The direct-child combinator also makes the modal-scroller exception below redundant (a modal's
   scroller is never a direct child of .page__body); it is left in place as a harmless guard. */
.page:not(.page--static-header):not(.design-editor):not(.register-page) > .page__body > .scroll-list:first-child {
    margin-top: calc(-1 * var(--page-header-h));
    padding-top: var(--page-header-h);
}

/* A sheet-modal's scroller is not a page scroller. Tripled .scroll-list to MATCH the rule above
   at 0,6,0 - source order then decides, which is why this sits after it. Lossless: no modal
   scroller in the app declares a padding-top of its own (.co-pull__list, .quote-modal__scroll,
   .tier-setup-modal__scroll, .photo-selection-modal__grid - the padding is on .modal__body). */
.page > .page__body .modal .scroll-list.scroll-list.scroll-list {
    margin-top: 0;
    padding-top: 0;
}

/* Only exists on the phone shell, where the floating bar does. The safe-area term is already
   divided by --ui-scale inside --tab-bar-inset. */
.app-shell--phone .page > .page__body .scroll-list::after {
    content: "";
    display: block;
    flex: 0 0 auto;
    height: var(--tab-bar-inset);
}

/* Same exclusion for a modal scroller that happens to render inside .page__body. */
.modal .scroll-list.scroll-list.scroll-list::after {
    content: none;
}

/* ...and the static-header roots reserve no bar band either: §3.1.3 defines them as never
   overlaid by the floating bar, so the reservation must not appear even if the shell still
   has the bar class up. Doubled .scroll-list to match the rule above's specificity; source
   order then decides, which is why this sits after it and not up with the header reservation. */
.page--static-header > .page__body .scroll-list.scroll-list::after,
.page.design-editor > .page__body .scroll-list.scroll-list::after,
.page.register-page > .page__body .scroll-list.scroll-list::after {
    content: none;
}

.scroll-x-row {
    /* POC shortcut: horizontal overflow row used for tab strips.
       Production may want pagination or a dropdown when N is large. */
    display: flex;
    align-items: stretch;
    gap: var(--space-2);
    overflow-x: auto;
    overflow-y: hidden;
    flex-wrap: nowrap;
    /* bar hidden only on touch - see .app-shell--touch below */
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
}

/* like .scroll-x-row + proximity snap; children opt in via scroll-snap-align */
.swipe-strip {
    display: flex;
    align-items: stretch;
    gap: var(--space-2);
    overflow-x: auto;
    overflow-y: hidden;
    flex-wrap: nowrap;
    /* min-width:0 - a flex child's default min-width:auto blocks the scroll */
    min-width: 0;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
}

/* touch only - desktop keeps the themed scrollbar (base.css) */
.app-shell--touch .scroll-x-row,
.app-shell--touch .swipe-strip,
.app-shell--touch .scroll-list {
    scrollbar-width: none;              /* Firefox / standards */
}
.app-shell--touch .scroll-x-row::-webkit-scrollbar,
.app-shell--touch .swipe-strip::-webkit-scrollbar,
.app-shell--touch .scroll-list::-webkit-scrollbar {
    display: none;                      /* WebKit (WebView2 + Android hosts) */
}

/* modals/toasts use position:fixed so .app-shell__body's overflow:hidden doesn't clip them */
/* Minus the safe-area band: index.html's .status-bar-safe-area sits in flow ABOVE #app and, on
   iOS, is env(safe-area-inset-top) tall - a rigid 100vh here then overhangs the WebView by that
   much and pushes the bottom of every page out of view. Resolves to 0 wherever the host already
   keeps the WebView clear of the status bar (the phone build, Android, Windows). */
.app-shell {
    display: flex;
    flex-direction: column;
    /* --vh is already in the zoomed subtree's space; the env() inset is real CSS px, so only
       it needs the --ui-scale divisor. */
    height: calc(var(--vh, 1vh) * 100 - env(safe-area-inset-top, 0px) / var(--ui-scale, 1));
    min-height: 0;
    overflow: hidden;
}

.app-shell__body {
    /* provides the definite height .page's height:100% resolves against */
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    position: relative;
}

/* Only while the standing photo-index alert is up. That strip is an in-flow sibling of .page, and
   .page takes height:100% of this box -- so the two together overflow it, and .app-shell's
   overflow:hidden clips the bottom of the page UNREACHABLY (there is no scroll to recover it).
   Switching to a flex column makes the strip take its own height and .page take the remainder.
   height:auto is required: height:100% would resolve against the full container again and put the
   overflow straight back. */
.app-shell__body--alerted {
    display: flex;
    flex-direction: column;
}

.app-shell__body--alerted > .page {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
}

@keyframes bottom-bar-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* resting cards stay quiet; interactive cards opt into --shadow-card hover at call sites */
.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    box-shadow: var(--shadow-sm);
}

/* commerce dialect only; dark uses surface-lightening (--color-card), not shadow */
.card--soft {
    background: var(--color-card);
    border-color: var(--color-card-border);
    box-shadow: var(--shadow-rest);
    /* scoped to --soft so the editor's base .card radius doesn't bubble */
    border-radius: var(--radius-xl);
}

.stat-tile {
    background: var(--color-card);
    border: 1px solid var(--color-card-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
    padding: var(--space-4) var(--space-5);
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
}

.stat-tile__value {
    font-size: var(--text-2xl);
    line-height: var(--leading-2xl);
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-tight);
    font-variant-numeric: tabular-nums;
    color: var(--color-primary-text);
}

.stat-tile__label {
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
    margin-top: var(--space-1);
}

/* soft-tinted, not solid fills - quieter and AA-safe in both themes */
.pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    border-radius: var(--radius-pill);
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    font-weight: var(--weight-semibold);
}

.pill--soft {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.pill--danger {
    background: var(--color-danger-soft);
    color: var(--color-danger-text);
}

/* reuses existing success/warning tokens - no new dark-sync work needed */
.pill--success {
    background: var(--color-success-soft);
    color: var(--color-success-text);
}

.pill--warning {
    background: var(--color-warning-soft);
    color: var(--color-warning-text);
}

.pill--neutral {
    background: var(--color-surface-muted);
    border: 1px solid var(--color-border);
    color: var(--color-text-muted);
}

/*
   POC shortcut: 150ms hover / 100ms active timings are tuned by feel.
   Production may want reduced-motion media query support.
   POC shortcut: every clickable surface uses :focus-visible discipline so
   mouse clicks don't leave a persistent ring; keyboard Tab focus shows it.
*/
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    /* prevents 2-line collapse in a shrinking flex row (e.g. editor header) */
    white-space: nowrap;
    border-radius: var(--radius-pill);
    padding: var(--space-2) var(--space-5);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-weight: var(--weight-semibold);
    border: 1.5px solid transparent;
    transition: background-color 150ms ease-out,
                color 150ms ease-out,
                border-color 150ms ease-out,
                box-shadow 150ms ease-out,
                transform 100ms ease-out;
    cursor: pointer;
    /* scoped to .btn, never inputs - WebKit #82692 breaks typing otherwise */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

.btn:active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
}

.btn:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.btn:focus:not(:focus-visible) {
    outline: none;
}

/* solid color under gradient avoids flash on hover/active */
.btn--primary {
    background-color: var(--color-primary);
    background-image: linear-gradient(to bottom, var(--color-primary-bright), var(--color-primary));
    color: var(--color-text-on-accent);
    box-shadow: var(--shadow-sm);
}

/* hover/active stay gradients - a solid resets background-image, causing a flash */
.btn--primary:hover {
    background: linear-gradient(to bottom, var(--color-primary), var(--color-primary-hover));
}

.btn--primary:active {
    background: linear-gradient(to bottom, var(--color-primary-hover), var(--color-primary-active));
    box-shadow: none;
}

.btn--ghost {
    background: transparent;
    color: var(--color-text);
    border-color: var(--color-border);
}

.btn--ghost:hover {
    background: var(--color-surface-hover);
    border-color: var(--color-border-strong);
}

.btn--ghost:active {
    background: var(--color-surface-active);
}

/* Stroked, not just filled: in dark the soft tint sits at the card's own luminance
   (#2A2010 on #262019 - 1.1:1), so the fill alone left an invisible button. */
.btn--soft {
    background: var(--color-primary-softer);
    color: var(--color-primary-text);
    border-color: var(--color-primary-text);
}

.btn--soft:hover {
    background: var(--color-primary-soft);
}

.btn--soft:active {
    background: var(--color-primary-soft);
    filter: brightness(0.96);
}

.btn--danger {
    background: var(--color-danger);
    color: var(--color-text-on-danger);
}

.btn--danger:hover {
    filter: brightness(0.94);
}

.btn--danger:active {
    filter: brightness(0.88);
}

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}

.btn:disabled:active,
.btn[aria-disabled="true"]:active {
    /* Don't translate-down a disabled button - it'd look like an interactive press. */
    transform: none;
}

/* square footprint + inherited pill radius from .btn = a circle (HIG icon-button geometry) */
.btn--icon {
    width: 40px;
    height: 40px;
    padding: 0;
}

.btn--sm {
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    gap: var(--space-1);
}

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--color-scrim);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-modal);
    /* @if-gated recreation replays the animation on every open */
    animation: backdrop-in 160ms ease-out;
}

.modal-backdrop--stack {
    z-index: var(--z-modal-stack);
}

.modal {
    position: relative;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-xl);
    padding: var(--space-6);
    width: min(440px, calc(var(--vw, 1vw) * 92));
    /* .app-shell and .app-shell__body are overflow:hidden (Phase 5B took the clamp off .page
       and .page__body, but not off the shell), so a modal taller than the viewport is CLIPPED,
       not scrollable-to - and its action row becomes unreachable. The phone sheet has always capped itself; desktop/tablet needs it too, and
       needs it more once type scales up. Modals that set their own max-height still win. */
    max-height: calc(var(--vh, 1vh) * 88);
    overflow-y: auto;
    box-shadow: var(--shadow-pop);
    /* transform resolves to none at animation end - no lasting side effects */
    animation: modal-pop 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.modal__title {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-tight);
    text-align: left;
    padding-right: var(--space-8);
}

.modal__close {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    width: 32px;
    height: 32px;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background-color 120ms ease, color 120ms ease;
}

.modal__close:hover {
    background: var(--color-surface-hover);
    color: var(--color-text);
}

/* canonical form layout - never replace with inline flex styles */
.modal__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin-top: var(--space-4);
}

.modal__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* Every form label in the app. Replaced two page-specific names, one of which was styled nowhere. */
.field-label {
    display: block;
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-weight: var(--weight-semibold);
    color: var(--color-text);
}

/* neutralizes page-specific label margins so flex gap is the only spacing */
.modal__field > label {
    margin-bottom: 0;
}

.modal__actions {
    display: flex;
    gap: var(--space-3);
    justify-content: flex-end;
    margin-top: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

/* re-skins every modal via the shared class trio; 599px = the phone breakpoint */
@keyframes sheet-up {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
}

@media (max-width: 599px) {
    .modal-backdrop {
        align-items: flex-end;        /* dock the sheet to the bottom edge */
    }

    /* .modal.modal, not .modal: ELEVEN modals (device-picker, pin-edit-modal, quote-modal,
       tier-setup-modal, tutorial-picker, ...) declare their own width/max-height in feature
       sheets that load AFTER this one. At equal specificity the later sheet won, so those
       modals stayed inset cards instead of docking full-width and the sheet contract only
       held for modals that happened not to set a width. Doubling the class (0,2,0) beats a
       single companion class, so the contract now holds for every modal, including new ones. */
    .modal.modal {
        width: 100%;
        max-width: none;
        border-radius: var(--radius-xl) var(--radius-xl) 0 0;   /* round the top only */
        border-bottom: none;
        max-height: calc(var(--vh, 1vh) * 88);
        overflow-y: auto;
        /* lifts content above the keyboard on iOS; Android's adjustResize already handles it */
        padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px) / var(--ui-scale, 1) + var(--keyboard-inset, 0px));
        animation: sheet-up var(--motion-medium) var(--ease-standard);
    }

    /* Grab handle - opt-in via .modal--resizable (wired through Modal's Class parameter, e.g.
       Class="foo-modal modal--resizable"), NOT drawn on every sheet. Dragging it does nothing -
       detent drag physics is explicitly not built (docs/HIG-REBUILD-PLAN.md §6) - so an affordance
       promising a capability the sheet lacks is worse than none; it used to render even on a
       2-button ConfirmDeleteModal. Reserve the class for a genuinely tall/scrollable sheet where
       the handle at least signals "this is a sheet," not a resize control. Centered; clears the
       absolute close button on the right. */
    .modal--resizable::before {
        content: "";
        display: block;
        width: 36px;
        height: 4px;
        margin: calc(-1 * var(--space-3)) auto var(--space-4);
        border-radius: var(--radius-pill);
        background: var(--color-border-strong);
    }

    /* overflows past 2 buttons on a narrow sheet; DOM order puts primary last */
    .modal__actions {
        flex-direction: column;
        align-items: stretch;
    }
}

/* not a modal - no backdrop, no focus trap */
.overflow-menu {
    position: relative;
    display: inline-flex;
}

.overflow-menu__trigger {
    /* just .btn btn--ghost - no extra styling needed */
}

.overflow-menu__panel {
    position: absolute;
    top: calc(100% + var(--space-1));
    right: 0;
    min-width: 200px;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-pop);
    padding: var(--space-1);
    z-index: var(--z-anchored);
    display: flex;
    flex-direction: column;
    gap: var(--space-0-5);
}

/* anchors left when right-anchoring would push the panel off-viewport */
.overflow-menu__panel--flip-left {
    right: auto;
    left: 0;
}

.overflow-menu__panel .btn {
    /* overrides .btn's default center-aligned inline-flex */
    justify-content: flex-start;
    width: 100%;
    text-align: left;
    border-radius: var(--radius-sm);
    font-weight: var(--weight-medium);
}

/* same touch hit-area pattern as the editor's dense controls */
.app-shell--touch .overflow-menu__trigger {
    position: relative;
}

.app-shell--touch .overflow-menu__trigger::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--touch-target-min);
    height: var(--touch-target-min);
    transform: translate(-50%, -50%);
}

/* gated by FormFactor - tablet gets touch sizing too, not just phone */
.app-shell--touch .btn,
.app-shell--touch .segmented__option,
.app-shell--touch .toolbar-tab,
.app-shell--touch .input {
    min-height: var(--touch-target-min);
}

.app-shell--touch .btn--icon {
    min-width: var(--touch-target-min);
}

/* Icon controls that are NOT .btn, so the min-height rule above never reached them: each is a
   28-34px visual chip. Rather than inflate the chrome, extend the hit area invisibly - the same
   ::before idiom as .overflow-menu__trigger. position:relative only establishes the containing
   block; it moves nothing. */
/* Pointer-scoped, NOT .app-shell--touch: app-level overlays (the Create sheet, PaywallModal,
   StartSurveySheet, WelcomeIntro) render as SIBLINGS of .app-shell, so every descendant rule
   off the shell silently misses them -- the close button on those sheets kept a 32px target
   while the identical button inside a page got 44. Modals render in both places. */
@media (pointer: coarse) {
    .modal__close::before {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: var(--touch-target-min);
        height: var(--touch-target-min);
        transform: translate(-50%, -50%);
    }
}

/* .modal__close is deliberately absent: it is ALREADY position:absolute (top/right), which
   is a containing block too. Listing it here demoted it to relative at specificity 0,2,0 and
   dropped the X out of the corner and into normal flow on every touch device. */
.app-shell--touch .app-chrome__settings,
.app-shell--touch .app-chrome__theme-toggle,
.app-shell--touch .inline-alert__dismiss,
.app-shell--touch .tutorial-coach__check,
.app-shell--touch .home-nav__gear,
.app-shell--touch .home-nav__avatar,
.app-shell--touch .design-editor__phone-back,
.app-shell--touch .design-editor__phone-overflow {
    position: relative;
}

.app-shell--touch .app-chrome__settings::before,
.app-shell--touch .app-chrome__theme-toggle::before,
.app-shell--touch .inline-alert__dismiss::before,
.app-shell--touch .tutorial-coach__check::before,
.app-shell--touch .home-nav__gear::before,
.app-shell--touch .home-nav__avatar::before,
.app-shell--touch .design-editor__phone-back::before,
.app-shell--touch .design-editor__phone-overflow::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--touch-target-min);
    height: var(--touch-target-min);
    transform: translate(-50%, -50%);
}

/* Wide-but-short controls: extend the hit box across the FULL width. The centred 44x44
   ::before used for icon chips would only thicken the middle of a 112px-wide chip. */
.app-shell--touch .stage-rail__chip {
    position: relative;
}

.app-shell--touch .stage-rail__chip::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: var(--touch-target-min);
    transform: translateY(-50%);
}

/* Same wide-but-short shape - the phone "Floor 1" building/floor switcher (measured 97x28 on
   device). Full-width ::before, not a centred square, for the same reason as .stage-rail__chip. */
.app-shell--touch .design-editor__phone-selector {
    position: relative;
}

.app-shell--touch .design-editor__phone-selector::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: var(--touch-target-min);
    transform: translateY(-50%);
}

/* Overlay controls under the touch floor on their short axis. All are isolated (their
   neighbours are separated by a gap), so a min dimension cannot steal an adjacent tap. */
.app-shell--touch .device-picker__controls .segmented__option,
.app-shell--touch .kits-page__component-row > .btn,
/* the LABEL is the tap target for a wrapped checkbox; at one line it was only ~34px tall */
.app-shell--touch .bom-export-modal__option {
    min-height: var(--touch-target-min);
    min-width: var(--touch-target-min);
}

/* The price-list import review's change-kind filter chips (All / New / Changed / Removed) -
   zero dedicated CSS before this; measured ~25px, under even the 28pt absolute minimum. Row gap
   lives with the base rule in designs.css. */
.app-shell--touch .catalog-import__chip {
    min-height: var(--touch-target-min);
}

/* Inline text crumb inside the device picker - vertical padding on an inline box extends
   the hit area without changing the line box. */
.app-shell--touch .device-picker__crumb {
    padding-block: var(--space-4);
}

/* Filter chips sit in a gapped scroll row (no neighbour to steal a tap); a taller pill is the
   whole affordance, so grow the pill itself rather than an invisible ::before. The padding alone
   only reached ~33px (measured on device) - min-height closes the rest while staying part of the
   same visible-growth approach, since align-items:center stretches the painted pill, not a hidden
   overlay. */
.app-shell--touch .device-picker__chip {
    padding-block: var(--space-2);
    padding-inline: var(--space-3);
    min-height: var(--touch-target-min);
}

/* NOT scoped to .app-shell--touch: WelcomeIntro is a fixed SIBLING of .app-shell (z-2000),
   so descendant selectors off the shell never reach it. It is a full-screen first-run
   surface, so the larger targets are appropriate on every input type anyway. */
.welcome-intro__top > .btn {
    min-height: var(--touch-target-min);
}

/* The scene dots are a 24px row with small gaps. Height is free to grow; WIDTH is not --
   a 44px-wide target would overlap its neighbours and steal their taps, which is worse than
   a small target. Swipe and the Back/Next buttons remain the primary way through. */
.welcome-intro__dot {
    min-height: var(--touch-target-min);
}

/* Row-shaped controls that measured just under the 44px floor on touch (35-43px tall).
   min-height, not height, so a wrapping label still grows the row. Kept on .app-shell--touch
   so pointer devices keep the tighter desktop rhythm. */
.app-shell--touch .site-detail-page__detail-line,
.app-shell--touch .photos-rail__row,
.app-shell--touch .pm-panel__summary,
.app-shell--touch .project-detail-page__section-action,
.app-shell--touch .list-row__link {
    min-height: var(--touch-target-min);
}

/* Icon-only card actions: the label is short enough to leave a 38px-wide target. */
.app-shell--touch .survey-card__actions > .btn {
    min-width: var(--touch-target-min);
}

/* The wordmark is the only global nav affordance and sat at 31px. It is already centred in
   the 3rem chrome band, so a min-height reaches the touch floor without moving anything.
   NOT the ::before idiom here - that pseudo is the brand mark. */
.app-shell--touch .app-chrome__home {
    min-height: var(--touch-target-min);
}

/* distinct from .overflow-menu: cursor-positioned, viewport-clamped by DesignsInterop */
.context-menu {
    position: fixed;
    /* `left` / `top` are written inline by the component after clamp. */
    min-width: 200px;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-pop);
    padding: var(--space-1);
    z-index: var(--z-menu);
    display: flex;
    flex-direction: column;
    gap: var(--space-0-5);
    /* items inside get :focus-visible; the panel itself shouldn't ring */
    outline: none;
}

.context-menu .btn {
    /* mirrors .overflow-menu__panel .btn */
    justify-content: flex-start;
    width: 100%;
    text-align: left;
    border-radius: var(--radius-sm);
    font-weight: var(--weight-medium);
}

.context-menu__separator {
    height: 1px;
    margin: var(--space-1) var(--space-2);
    background: var(--color-border);
    border: 0;
}

/* Destructive context-menu items (Delete pin/point/edge/room, ...) - always last in their
   menu, colored to match .btn--danger-ghost. Token exists in every theme block. */
.context-menu__item--destructive {
    color: var(--color-danger-text);
}

/* rounded-rect, not pill - capsule geometry is a .btn control affordance (§3.4);
   the segmented track is a container and keeps its own radius scale. */
.segmented {
    display: inline-flex;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-inset);
    /* Concentric corners: --radius-lg minus this padding == the key's --radius-md.
       At 3px the key's corner poked through the track's border. */
    padding: var(--space-1);
    border-radius: var(--radius-lg);
    gap: var(--space-0-5);
}

.segmented__option {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-md);         /* inner radius of the rounded-rect track above */
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-weight: var(--weight-medium);
    color: var(--color-text-muted);
    background: transparent;
    border: 0;
    cursor: pointer;
    white-space: nowrap; /* e.g. "Floor plan" must not wrap when the row is tight */
    transition: background-color 150ms ease-out, color 150ms ease-out,
                box-shadow 150ms ease-out, transform 100ms ease-out;
}

/* hover only on unselected segments - active is already the strongest state */
.segmented__option:not([aria-selected="true"]):not(.is-active):hover {
    color: var(--color-text);
}

.segmented__option:not([aria-selected="true"]):not(.is-active):active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
    color: var(--color-text);
}

.segmented__option:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.segmented__option:focus:not(:focus-visible) {
    outline: none;
}

.segmented__option[aria-selected="true"],
.segmented__option.is-active {
    background: var(--color-surface-raised);
    color: var(--color-text);
    font-weight: var(--weight-semibold);
    box-shadow: var(--shadow-sm);
}

.segmented__option[aria-selected="true"]:hover,
.segmented__option.is-active:hover {
    background: var(--color-surface-raised);
}

/* base raised-key state stays unchanged - never soften the editor (rule 6) */
.segmented--solid .segmented__option[aria-selected="true"],
.segmented--solid .segmented__option.is-active {
    background: var(--color-primary);
    color: var(--color-text-on-accent);
    box-shadow: none;
}
.segmented--solid .segmented__option[aria-selected="true"]:hover,
.segmented--solid .segmented__option.is-active:hover {
    background: var(--color-primary);
}

.toolbar-tab {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-weight: var(--weight-medium);
    color: var(--color-text-muted);
    background: transparent;
    border: 0;
    cursor: pointer;
    transition: background-color 150ms ease-out, color 150ms ease-out, transform 100ms ease-out;
}

/* hover only on non-active tabs */
.toolbar-tab:not([aria-selected="true"]):not(.is-active):not(.toolbar-tab--active):hover {
    background: var(--color-surface-hover);
    color: var(--color-text);
}

.toolbar-tab:not([aria-selected="true"]):not(.is-active):not(.toolbar-tab--active):active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
    background: var(--color-surface-active);
}

.toolbar-tab:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.toolbar-tab:focus:not(:focus-visible) {
    outline: none;
}

.toolbar-tab[aria-selected="true"],
.toolbar-tab.is-active {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
}

/* instrument-tight - adds swipe-strip feel without softening the canvas chrome */
.action-chip {
    flex: 0 0 auto;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-0-5);
    min-width: 52px;
    padding: var(--space-1) var(--space-2);
    background: transparent;
    border: 0;
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    text-decoration: none;
    cursor: pointer;
    scroll-snap-align: start;
    transition: color 150ms ease-out, transform 100ms ease-out;
}

.action-chip__glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: var(--radius-pill);
    background: var(--color-surface-muted);
    transition: background-color 150ms ease-out, color 150ms ease-out;
}

.action-chip__glyph svg {
    width: 18px;
    height: 18px;
}

.action-chip__label {
    font-size: var(--text-xs);
    line-height: 1;
    font-weight: var(--weight-medium);
    white-space: nowrap;
}

.action-chip:hover {
    color: var(--color-text);
}

.action-chip:hover .action-chip__glyph {
    background: var(--color-surface-hover);
}

.action-chip:active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
}

.action-chip:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.action-chip:focus:not(:focus-visible) {
    outline: none;
}

.action-chip:disabled,
.action-chip[aria-disabled="true"] {
    opacity: 0.4;
    cursor: not-allowed;
}

/* chip equivalent of the .btn--primary toggle state */
.action-chip--active {
    color: var(--color-primary-text);
}

.action-chip--active .action-chip__glyph {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.action-chip--danger {
    color: var(--color-danger-text);
}

.action-chip--danger:hover .action-chip__glyph {
    background: var(--color-danger-soft);
    color: var(--color-danger-text);
}

/* shared by floor-plan + map; each consumer adds only its position class */
.wall-toolbar {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-lg);
}

.wall-toolbar__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    color: var(--color-text-muted);
    flex: 0 0 auto;
}

/* 32px instrument-tight; touch min-target rules promote it on phone/tablet */
.wall-toolbar__iconbtn.btn--icon {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-pill);
}

/* shares the .segmented track/key language, smaller */
.segmented--sm {
    padding: var(--space-0-5);
    /* Same concentric rule at the tighter padding. */
    border-radius: var(--radius-md);
}

.segmented--sm .segmented__option {
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-sm);
}

/* touch's snap-defeat toggle; Alt on desktop */
.wall-toolbar__snap {
    color: var(--color-text-muted);
    background: transparent;
}

.wall-toolbar__snap.is-active {
    color: var(--color-primary-text);
    background: var(--color-primary-soft);
    font-weight: var(--weight-semibold);
}

.input {
    width: 100%;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-base);
    line-height: var(--leading-base);
    background: var(--color-surface);
    color: var(--color-text);
    transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}

.input::placeholder {
    color: var(--color-text-subtle);
}

.input:hover:not(:focus) {
    border-color: var(--color-border-strong);
}

.input:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: var(--shadow-focus-ring);
}

/* Date/time inputs on WebKit (iOS + the Mac Catalyst WKWebView).
   The value is not a text node - WebKit renders it inside a shadow tree that .input cannot
   reach, laid out at the NATIVE control's metrics. Two visible consequences: the value ignores
   the input's alignment and clips against .input's padding, and the box takes an intrinsic
   native width/height instead of the one the flex or grid column gives it, so it never matches
   the plain text fields beside it. Chromium already honours all of this; these are a no-op there. */
.input[type="date"],
.input[type="time"],
.input[type="datetime-local"] {
    /* drops the native control metrics, so .input's own padding/border/line-height govern */
    -webkit-appearance: none;
    appearance: none;
    /* without this the native intrinsic width beats the column's share and the field overflows */
    min-width: 0;
}

.input[type="date"]::-webkit-date-and-time-value,
.input[type="time"]::-webkit-date-and-time-value,
.input[type="datetime-local"]::-webkit-date-and-time-value {
    /* text-align on the input itself never reaches the shadow value */
    text-align: left;
    /* min-height, not height: WebKit lays the value out in a zero-height box, which is what
       clips it. em so it tracks the input's own font-size, matching a text input's line box. */
    min-height: calc(var(--leading-base) * 1em);
}

/* WebKit insets the editable fields again, on top of .input's padding. */
.input[type="date"]::-webkit-datetime-edit,
.input[type="time"]::-webkit-datetime-edit,
.input[type="datetime-local"]::-webkit-datetime-edit {
    padding: 0;
}

@keyframes page-enter {
    from { opacity: 0; transform: scale(0.985); }
    to   { opacity: 1; transform: none; }
}

@keyframes backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes modal-pop {
    from { opacity: 0; transform: translateY(10px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}

@keyframes toast-in {
    /* keyframe settles onto the resting translateX(-50%) style */
    from { opacity: 0; transform: translate(-50%, 12px); }
}

@keyframes spinner-rotate {
    to { transform: rotate(360deg); }
}

/* no fill-mode - a persistent transform here breaks fixed children inside .page. Phase 5B's
   position:relative does NOT have that effect (relative is not a fixed containing block), so
   this stays the only hazard on .page; it is also why the scale must keep never overshooting,
   now that .page no longer clips. */
.page {
    animation: page-enter var(--motion-medium) var(--ease-standard);
}

.icon-tile {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    background: var(--color-primary-softer);
    border: 1px solid var(--color-primary-soft);
    color: var(--color-primary-text);
    font-size: var(--text-md);
    line-height: 1;
    font-weight: var(--weight-bold);
    user-select: none;
}

/* Larger hero variant for card headers. */
.icon-tile--lg {
    width: 44px;
    height: 44px;
    border-radius: var(--radius-lg);
    font-size: var(--text-lg);
}

/* solid accent chip, not the low-contrast soft-tint base */
.icon-tile--label {
    font-size: var(--text-2xs);
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: var(--color-text-on-accent);
}

.list-row {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    transition: border-color 150ms ease-out, box-shadow 150ms ease-out,
                background-color 150ms ease-out, transform 100ms ease-out;
}

/* hover uses shadow only - a border would read as focus, not hover */
.list-row--interactive:hover {
    box-shadow: var(--shadow-card);
}

.list-row--interactive:focus-within {
    box-shadow: var(--shadow-card);
}

/* mirrors .btn:active so rows don't read as passive on tap */
.list-row--interactive:active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
    /* drops elevation to flat so rows visually depress, not just shift */
    box-shadow: var(--shadow-0);
}

.list-row__link:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius-md);
}

.list-row__link:focus:not(:focus-visible) {
    outline: none;
}

.list-row__link {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    text-decoration: none;
    color: inherit;
}

.list-row__main {
    display: flex;
    flex-direction: column;
    gap: var(--space-0-5);
    min-width: 0;
}

.list-row__title {
    font-size: var(--text-md);
    line-height: var(--leading-md);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-tight);
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-row__meta {
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-row__trail {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.list-row__chevron {
    flex: 0 0 auto;
    color: var(--color-text-subtle);
    font-size: var(--text-lg);
    line-height: 1;
    transition: transform 150ms ease-out, color 150ms ease-out;
}

.list-row--interactive:hover .list-row__chevron {
    transform: translateX(2px);
    color: var(--color-primary-text);
}

/* stacked rows in one card, unlike .list-row (one card per row) */
.hub-card {
    overflow: hidden;   /* clip row hover + dividers to the card radius */
    padding: 0;         /* the rows own their padding */
}

.hub-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-3) var(--space-4);
    background: transparent;
    border: 0;
    text-align: left;
    text-decoration: none;
    color: var(--color-text);
    font-family: inherit;
    cursor: pointer;
    transition: background-color 150ms ease-out;
}

.hub-row + .hub-row {
    border-top: 1px solid var(--color-border);
}

.hub-row:hover {
    background: var(--color-surface-hover);
}

/* A row whose ONLY interactive child is the switch. .hub-row carries cursor:pointer and a hover
   background for the navigational rows; on a toggle row that makes the whole row read as tappable
   when only the control is. Referenced by You.razor since the switch landed, defined here now.
   MUST stay after .hub-row:hover - both are (0,2,0), so source order is what decides. */
.hub-row--toggle {
    cursor: default;
}

.hub-row--toggle:hover {
    background: transparent;
}

.hub-row:active {
    background: var(--color-surface-active);
}

.hub-row:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
}

.hub-row:focus:not(:focus-visible) {
    outline: none;
}

.hub-row__icon {
    flex: 0 0 auto;
    display: inline-flex;
    color: var(--color-text-muted);
    font-size: var(--text-base);
    line-height: 1;
}

.hub-row__label {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--text-base);
    line-height: var(--leading-base);
    font-weight: var(--weight-medium);
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hub-row__value {
    /* SHRINKABLE, not fixed. A long value has to give way to the label - the label is the
       row's identity and the value is its state. Measured at 0 0 auto: "Walnut & berry -
       Light · Detailed" (184px) squeezed "Appearance" to 40px on a 320dp phone and to ZERO
       on a 360dp phone at the max 1.30 UiScale (360/1.3 = 277 layout px), then overflowed
       the row below that. Both sides ellipsize now, and shrink is proportional to base
       width, so the longer value always yields first. */
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    font-weight: var(--weight-semibold);
    color: var(--color-text-subtle);
}

.hub-row__value--success { color: var(--color-success-text); }
.hub-row__value--warning { color: var(--color-warning-text); }

.hub-row__chevron {
    flex: 0 0 auto;
    display: inline-flex;
    color: var(--color-border-strong);
    font-size: var(--text-sm);
    line-height: 1;
}

/* carries its own padding since .hub-card's rows own theirs; matches .hub-row's padding */
.hub-note {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--color-border);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
}

/* 599px = the phone breakpoint (a token cannot be used: var() is invalid in @media) */
@media (max-width: 599px) {
    .list-row {
        flex-wrap: wrap;
        gap: var(--space-2) var(--space-3);
    }

    .list-row__link {
        flex: 1 1 100%;
    }

    .list-row__chevron {
        display: none;
    }
}

/* rests as quiet text; danger wash only reveals on approach */
.btn--danger-ghost {
    background: transparent;
    color: var(--color-danger-text);
    border-color: transparent;
}

.btn--danger-ghost:hover {
    background: var(--color-danger-soft);
    border-color: var(--color-danger-soft);
    color: var(--color-danger-text);
}

.btn--danger-ghost:active {
    background: var(--color-danger-soft);
    filter: brightness(0.96);
}

/* zero markup changes - page classes keep their own padding/color */
.photo-detail__loading,
.design-editor__loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
}

.photo-detail__loading::before,
.design-editor__loading::before {
    content: "";
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    border: 3px solid var(--color-border);
    border-top-color: var(--color-primary);
    animation: spinner-rotate 700ms linear infinite;
}

/* centers a lone child (EmptyState, error card) for terminal pages */
.page-center {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--page-gutter);
}

/* -soft/-text token pairs already carry dark variants - no per-theme branch needed */
.inline-alert {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
}

.inline-alert--error {
    background: var(--color-danger-soft);
    color: var(--color-danger-text);
}

.inline-alert--warning {
    background: var(--color-warning-soft);
    color: var(--color-warning-text);
}

.inline-alert--info {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.inline-alert__icon {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
}

.inline-alert__msg {
    flex: 1 1 auto;
    min-width: 0;
}

/* currentColor - one rule works for all kinds/themes, like .inline-alert__icon */
.inline-alert__action {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    padding: var(--space-1) var(--space-3);
    border: 1px solid currentColor;
    /* square not pill - this is an inline text action, not a .btn instance,
       so it doesn't inherit the base capsule (§3.4) */
    border-radius: var(--radius-md);
    background: transparent;
    color: inherit;
    font-family: inherit;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--motion-base) var(--ease-out);
}

.inline-alert__action:hover {
    background: var(--color-surface);
}

.inline-alert__action:active {
    transition-duration: var(--motion-fast);
    transition-timing-function: var(--ease-in);
    transform: translateY(1px);
}

.inline-alert__action:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.inline-alert__dismiss {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    background: transparent;
    color: inherit;
    font-size: var(--text-lg);
    line-height: 1;
    opacity: 0.7;
    cursor: pointer;
    transition: opacity 120ms ease, transform 100ms ease-out;
}

.inline-alert__dismiss:hover {
    opacity: 1;
}

.inline-alert__dismiss:active {
    transform: translateY(1px);
}

.inline-alert__dismiss:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.inline-alert__dismiss:focus:not(:focus-visible) {
    outline: none;
}

/* Pre-export findings strip (FindingsStrip): an .inline-alert headline + an expandable list.
   Column flex child so a page can park it above its .page-dock without touching the scroller. */
.findings-strip {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-height: 0;
}

/* capped + self-scrolling: a long list must never push a phone .page-dock off-screen (rule 3) */
.findings-strip__list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    max-height: 9.5rem;
    overflow-y: auto;
    padding: 0 var(--space-2);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
    list-style: none;
}

.findings-strip__item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}

.findings-strip__item--block {
    color: var(--color-danger-text);
}

.findings-strip__glyph {
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    margin-top: var(--space-0-5);
    color: var(--color-warning-text);
}

.findings-strip__item--block .findings-strip__glyph {
    color: var(--color-danger-text);
}

.findings-strip__actions {
    display: flex;
    justify-content: flex-end;
}

/* outline, not border-color, for the selected ring - avoids a layout shift */
.picker-card {
    background: var(--color-card);
    border: 1px solid var(--color-card-border);
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: background-color 150ms ease-out, box-shadow 150ms ease-out,
                transform 100ms ease-out;
}

.picker-card:hover {
    box-shadow: var(--shadow-card);
    transform: translateY(-1px);
}

.picker-card:active {
    transition-duration: 100ms;
    transition-timing-function: ease-in;
    transform: translateY(1px);
}

.picker-card:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.picker-card:focus:not(:focus-visible) {
    outline: none;
}

.picker-card.is-selected {
    background: var(--color-primary-soft);
    box-shadow: var(--shadow-card);
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
}

/* z-900: above page chrome, below modals (1000) and Toast (1100) */
.tutorial-coach {
    position: fixed;
    right: var(--space-5);
    /* 24px clears the Mapbox attribution strip - a license requirement */
    bottom: calc(var(--space-5) + 24px);
    z-index: var(--z-coach);
    width: min(320px, calc(var(--vw, 1vw) * 100 - var(--space-6)));
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4);
    background: var(--color-card);
    border: 1px solid var(--color-card-border);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-pop);
    /* own keyframe, not toast-in - that bakes in translateX(-50%) centering */
    animation: coach-in 220ms ease-out;
}

@keyframes coach-in {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
}

/* 200px mirrors designs.css --phone-bottom-reserve + banner clearance; can't read that page-scoped var here */
@media (max-width: 599px) {
    .tutorial-coach {
        left: var(--space-3);
        right: var(--space-3);
        width: auto;
        bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px) / var(--ui-scale, 1) + 200px);
    }
}

.tutorial-coach__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.tutorial-coach__kicker {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-loose);
    color: var(--color-text-muted);
}

.tutorial-coach__close.btn--icon {
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    flex: 0 0 auto;
}

.tutorial-coach__title {
    font-size: var(--text-md);
    line-height: var(--leading-md);
    font-weight: var(--weight-semibold);
    color: var(--color-text);
}

.tutorial-coach__detail {
    margin: 0;
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
}

.tutorial-coach__actions {
    display: flex;
    justify-content: flex-end;
}

/* Completion flash - the auto-closing "done" state. */
.tutorial-coach--done {
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
}

.tutorial-coach__check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
    border-radius: var(--radius-pill);
    background: var(--color-success-soft);
    color: var(--color-success-text);
}

.tutorial-coach__check svg {
    width: 16px;
    height: 16px;
}

/* Picker modal - mirrors the kit-picker layout. */
.tutorial-picker {
    width: min(560px, calc(var(--vw, 1vw) * 94));
    max-height: calc(var(--vh, 1vh) * 86);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.tutorial-picker__hint {
    margin: 0;
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
}

.tutorial-picker__results {
    overflow-y: auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.tutorial-picker__result {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    text-align: left;
}

.tutorial-picker__result-main {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.tutorial-picker__result-name {
    font-size: var(--text-md);
    line-height: var(--leading-md);
    font-weight: var(--weight-semibold);
    color: var(--color-text);
}

.tutorial-picker__result-blurb {
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    color: var(--color-text-muted);
}

/* authoritative reduced-motion guard lives in animations.css, loaded after this and last */

/* built on the .modal trio; rows share .overflow-menu__panel's .btn.btn--ghost contract */

.action-sheet__title {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--color-text-subtle);
}

.action-sheet__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-0-5);
}

.action-sheet__body .btn {
    /* taller than .overflow-menu__panel rows - reached with a thumb, not a pointer */
    justify-content: flex-start;
    width: 100%;
    min-height: 52px;
    gap: var(--space-3);
    padding-left: var(--space-3);
    padding-right: var(--space-3);
    text-align: left;
    border-radius: var(--radius-md);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
}

/* fixed icon gutter keeps labels aligned with or without a glyph */
.action-sheet__body .btn > svg,
.action-sheet__body .btn > .icon {
    flex: 0 0 auto;
    width: 20px;
    color: var(--color-text-muted);
}

/* ActionSheet's own Cancel row (ShowCancel="true") - set apart from the sheet's own actions
   above it, matching the native anatomy of a separated Cancel row rather than just the last
   item in the same list. */
.action-sheet__cancel {
    justify-content: center;
    text-align: center;
    margin-top: var(--space-2);
    border-top: 1px solid var(--color-border);
    border-radius: 0;
    padding-top: calc(var(--space-3) + var(--space-2));
}

/* flex child, not fixed - takes its height out of .page__body's scroller (rule 3).

   The bottom padding must clear the phone tab bar, which the dock knows nothing about: the dock
   is a .page-level flex child AFTER .page__body, so the bar spacer inside .scroll-list::after
   does not push it clear, and reserving env(safe-area-inset-bottom) alone left 62 of its 72px -
   BOTH buttons - under the bar at 375x812 (measured). All four document pages that use a dock
   resolve to BottomBarMode.Work, so the bar is always up beneath them and Approve/Export was
   unreachable.

   max(), not a sum: --tab-bar-inset ALREADY carries the safe-area term (it is tab-bar-height +
   inset, matching .tab-bar's own bottom offset), so adding both would double-count the inset on
   every notched phone. max() takes whichever actually applies - the full bar clearance when the
   phone shell is up, the bare safe area when --tab-bar-inset is its 0px default. The safe-area
   read still goes through / var(--ui-scale, 1) on the bare arm; the other arm's is divided
   inside --tab-bar-inset. */
.page-dock {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4)
             calc(var(--space-3) + max(var(--tab-bar-inset),
                                       env(safe-area-inset-bottom, 0px) / var(--ui-scale, 1)));
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
}

/* flex:1, not 1 1 auto - splits the bar evenly regardless of label length */
.page-dock__btn {
    flex: 1;
    min-height: 48px;
    justify-content: center;
}

@media (max-width: 599px) {
    .page-dock {
        padding: var(--space-3) var(--space-3)
                 calc(var(--space-4) + max(var(--tab-bar-inset),
                                           env(safe-area-inset-bottom, 0px) / var(--ui-scale, 1)));
        background: var(--color-bg);
    }
}

/* ══ THE MATERIAL LADDER ═══════════════════════════════════════════════════════════════
   HIG-REBUILD-PLAN.md §3.1.2. Four rungs, in this order, and the order IS the safety
   architecture. Everything below lives in ONE contiguous block on purpose: a blur and the
   three rules that take it away must be readable together, and pre-merge gate 16 asserts
   every backdrop-filter has its reduced-transparency sibling in the same sheet.

   RUNG 0 - the opaque floor. It is not written here because it is already the resting
   declaration of each class: .toast and .context-menu / .overflow-menu__panel each paint
   var(--color-surface-raised) with a real border and --shadow-pop. Glass is LAYERED ON,
   never a replacement, so a WebView that never enters rung 1 is not in a fallback mode -
   it is in the permanent, equally-tested rendering (§2 decision 16: a stale Android
   WebView keeps the opaque path forever, unwarned and ungated).

   WHAT IS NOT HERE, AND WILL NOT BE: the canvas. Nothing in this block matches
   .design-editor*, .map-canvas or any bar that floats over them - those keep the
   --frost-canvas-* opaque set, which is their PERMANENT rendering, because Android's
   WebView re-blurs a backdrop on every GL frame (the measured note at designs.css's
   phone-chrome rules). §2 decision 3: the canvas surface is closed. Any future glass
   rule belongs in this block, where that constraint is stated, and nowhere else.

   The three sanctioned surfaces are named in rungs 2-4 ahead of their opt-in in rung 1.
   That is deliberate, not dead code: extending glass past the pilot is then a one-line
   edit to rung 1 that can never ship a blur without its removal rungs already in place. */

/* ── Rung 1: engine capability + platform opt-in ──────────────────────────────────────
   The ONLY rule in the app that adds a backdrop-filter. Two gates, both required:
   @supports (the engine can do it at all) and [data-glass="on"] (the platform opted in -
   stamped on <html> by ThemeInterop from IPlatformInfo, iOS-only per the §2 per-axis
   decision; flipping that axis is one line of C#, not a CSS rewrite).

   PILOT SCOPE: .toast--info only, per the phase's "pilot on the toast, measure on a
   physical Android device and a physical iPhone before touching anything else". The
   status variants keep their opaque tints - .toast--success / --error carry meaning in
   their fill and must not be diluted by a neutral one.

   .toast lives in designs.css; this rule is here because components.css is the sheet
   that owns the ladder, and gate 16 wants the blur and its fallbacks in one file. It
   outranks the designs.css declaration on specificity, not load order.

   §3.1.4 audit before any surface joins this rung: a backdrop-filtered element becomes
   the containing block for every position:fixed DESCENDANT, exactly like a transform.
   .toast and both menu panels are leaf overlays - buttons and text, no fixed children -
   which is why they were chosen. .modal is NOT in this rung and needs that audit first. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    [data-glass="on"] .toast--info {
        background: var(--glass-fill-regular);
        -webkit-backdrop-filter: blur(var(--glass-blur-regular)) saturate(var(--glass-saturate));
        backdrop-filter:         blur(var(--glass-blur-regular)) saturate(var(--glass-saturate));
        /* Establishes the stacking context the local band contracts for (tokens.css layer
           ladder). backdrop-filter creates one on its own; declaring it is the honest form. */
        isolation: isolate;
        /* The surface KEEPS its own 1px var(--color-border). The plan's snippet swapped in
           --color-card-border, which is transparent in light - a 72%-opaque fill over unknown
           content with no edge at all is the legibility case that border exists for. */
    }
}

/* ── Rung 2: Reduce Transparency ──────────────────────────────────────────────────────
   The .modal-backdrop half of this rung is UNCONDITIONAL - no [data-glass], no @supports,
   every platform. That blur(3px) predates the material layer and is the one translucency
   the app already shipped, so honoring the setting there is a plain accessibility fix that
   stands whether or not any glass ever lands. --color-overlay (not --color-scrim) is the
   right scrim once the blur is gone: separation the blur was carrying moves into opacity. */
@media (prefers-reduced-transparency: reduce) {
    .modal-backdrop {
        -webkit-backdrop-filter: none !important;
        backdrop-filter: none !important;
        background: var(--color-overlay);
    }

    [data-glass="on"] .toast--info,
    [data-glass="on"] .context-menu,
    [data-glass="on"] .overflow-menu__panel {
        -webkit-backdrop-filter: none !important;
        backdrop-filter: none !important;
        background: var(--color-surface-raised);
    }
}

/* ── Rung 3: Increase Contrast ────────────────────────────────────────────────────────
   Same removal, plus the hard border - accessibility.md asks for a higher-contrast scheme
   under this setting, and tokens.css answers the palette half by promoting High noon for
   System-theme users. A translucent panel is the opposite of what was asked for. */
@media (prefers-contrast: more) {
    [data-glass="on"] .toast--info,
    [data-glass="on"] .context-menu,
    [data-glass="on"] .overflow-menu__panel {
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        background: var(--color-surface-raised);
        border-color: var(--color-border-strong);
    }
}

/* ── Rung 4: gesture suspension ───────────────────────────────────────────────────────
   [data-gesture="active"] on <html> for the duration of a canvas manipulation. A blur is
   recomputed against whatever moved behind it, so an overlay held over a panning map is
   the one case that costs real frames - the same reason the canvas bars are permanently
   opaque. Stamped through ThemeInterop.SetGestureActiveAsync; the canvas pointerdown/up
   path is its caller. Specificity beats rung 1 (two attributes on the same ancestor), so
   it wins regardless of which rungs are also matching. */
[data-glass="on"][data-gesture="active"] .toast--info,
[data-glass="on"][data-gesture="active"] .context-menu,
[data-glass="on"][data-gesture="active"] .overflow-menu__panel {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: var(--color-surface-raised);
}

