/* scenario_results.mobile.css -- the phone layout for the scenario-results
   applet. Loaded after scenario_results.css and overrides it only inside the
   one media query below; the desktop sheet is never edited for phone reasons
   (the seam rule with proj/applet-improvements, PLAN-mobile-explore-mvp §5).

   Layout on a phone: the map owns the screen. The measurements dock is
   permanently on it as a drag-to-resize bottom sheet, at rest showing only its
   drag row -- the grip strip above the one-line readout; the sidebar opens over
   the map as a full-height sheet from the top, raised by a gear in MapLibre's
   top-right control stack. There is no bottom bar any more (Taylor,
   2026-09-12). The legend collapses to a title chip at top-left. Every dock
   panel reflows to one vertical stack and a table wider than the phone
   scrolls inside its own box (ruling D3). The
   mode-choice decomposition becomes one card per mode (treatment B), which
   needs sheets.js to copy the column headers into `td[data-label]`. The
   sidebar sheet is denser than the desktop sidebar it is made of: 36px
   targets, 13px type, one-row scenario and button+select rows (S9).

   The media query MUST stay byte-identical to LAYOUT_MQ in layout.js, so the
   CSS and the JS can never disagree about which layout is on screen. */

/* Phone-only chrome. These rules sit outside the media query on purpose:
   the markup is always present, and on a desktop it must not render. */
#mobile-summary { display: none; }
#panel-close { display: none; }
.sheet-handle { display: none; }
#ctrl-3d { display: none; }
#ctrl-settings { display: none; }

@media (max-width: 760px), (max-height: 500px) {
  /* The dock's resting height, and so the height the map gives up at the
     bottom: an 18px grip strip over the 38px readout. It was 84px while a
     bar carried the readout plus a 46px Controls/Results button row; the
     buttons are gone (the gear raises Controls, the dock never closes), so
     the same chrome costs 28px less. No safe-area term: the viewport meta
     has no `viewport-fit=cover`, so the browser keeps the page above the
     home indicator / gesture bar itself and `env(safe-area-inset-*)` reads
     0. With cover, the inset became a ~20-34px empty grey band under the
     buttons on Android Chrome edge-to-edge (Taylor, 2026-09-12). */
  :root {
    --grip-h: 18px; --readout-h: 38px;
    /* Plus the sheet's own 1px top border: --bar-h is the dock's BORDER box,
       and without the term the drag row overflows its resting height by
       exactly that border and the readout's last pixel is clipped away by
       `overflow: hidden`. sheets.js adds the same 1px (as `clientTop`) to the
       drag floor, so a pull to the floor lands on the resting height rather
       than one pixel under it. */
    --bar-h: calc(var(--grip-h) + var(--readout-h) + 1px);
  }

  /* ---- The frame ---- */
  /* `dvh` tracks mobile Safari's URL bar; `vh` is the fallback for browsers
     without it. Both #app and body lose their flex column: the two sheets and
     the bar are all fixed, so nothing needs to share the height any more. */
  body { display: block; }
  #app { display: block; height: 100vh; height: 100dvh; }
  /* The map runs the whole height and the dock floats on it (Taylor,
     2026-09-12), so the basemap shows through the sheet's rounded top corners
     instead of stopping square above them. --bar-h stops being the map's
     bottom inset and stays what it always was for everything else: the dock's
     resting height, which is where the Controls sheet stops and what the
     MapLibre corner controls clear. map.js pads the opening fitBounds by the
     dock's measured height so the activity peak still lands on the middle of
     the VISIBLE map, not of the container. */
  #map-area { position: absolute; inset: 0; }
  /* MapLibre's bottom corners (the attribution) would otherwise sit under the
     resting dock. They clear it, and do not follow a dragged sheet -- the
     attribution is not worth reflowing on every drag frame. */
  #map .maplibregl-ctrl-bottom-left, #map .maplibregl-ctrl-bottom-right {
    bottom: var(--bar-h);
  }

  /* ---- The readout ---- */
  /* The summary strip: indicator · value on one line. The value never
     shrinks; the indicator takes the rest and ellipsizes (the legend chip
     names it in full). No zone name: ids mean nothing to a reader and a
     tract name ellipsized at 390px carried nothing the map's highlight does
     not. It now sits inside the dock's drag row, under the grip, and so
     rides up and down with the drag (Taylor, 2026-09-12) instead of staying
     on a bar below the sheet. It is still visible under the Controls sheet,
     which stops at --bar-h: pick an indicator in Controls and the readout
     changes under the sheet's foot, as before. It takes the row's full width:
     the ⇆ / ✕ pair that used to share it is gone from the phone layout. */
  #mobile-summary {
    display: flex; align-items: center; gap: 6px; height: var(--readout-h); flex: 0 0 auto;
    padding: 0 12px; font-size: 14px; line-height: 1.2; color: var(--n-55);
    white-space: nowrap;
  }
  /* The name is a button (S12) that opens the indicator menu below. It
     looks like the text it was: no chrome, the strip's font and colour, an
     arrow as the one affordance -- a flex sibling of the text rather than
     its tail, so the ellipsis on a long profile-suffixed title cannot
     swallow it. The visible text stays the legend title, which carries the
     profile suffix and units the sidebar labels lack. */
  #ms-ind {
    flex: 1 1 auto; min-width: 0; display: flex; align-items: center;
    height: 100%; margin: 0; padding: 0; border: 0; background: none;
    font: inherit; color: inherit; text-align: left; cursor: pointer;
  }
  /* The headline: 16px on one line. When the name does not fit at that size
     (a profile suffix -- "People per km² ($68k–$113k · Car · LTS≤1)"),
     summary.js sets `.wrap` on the button and the name alone drops to 13px
     over two clamped lines; the value keeps its size. The strip's 38px is
     what two 15px lines need, so --bar-h never moves. */
  #ms-ind-text {
    flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    font-size: 16px; font-weight: 600; color: var(--n-22);
  }
  #ms-ind.wrap #ms-ind-text {
    font-size: 13px; line-height: 15px; white-space: normal;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    line-clamp: 2;
  }
  /* The picker's one affordance, so it carries the weight of looking like a
     control: 18px and --n-44 rather than 14px and --n-88 (Taylor,
     2026-09-12). `line-height: 1` keeps the taller glyph from growing the
     38px row it sits in. */
  #ms-ind:has(#ms-ind-text:not(:empty))::after {
    content: "\25BE"; flex: none; margin-left: 5px;
    font-size: 18px; line-height: 1; color: var(--n-44);
  }
  #ms-ind[aria-expanded="true"]::after { content: "\25B4"; }
  #ms-ind:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: 3px; }

  /* ---- The indicator menu ---- */
  /* Opens from the readout over the map -- upward by preference, downward
     once the dock has been dragged up far enough that there is more room
     below -- capped at 60% of the screen so the map stays in view (the
     26-row Advanced list scrolls inside; the 10-row basic list shows
     whole). Above the sheets. Group headings are the sidebar's row labels;
     the active row is ticked.

     summary.js writes `top` / `bottom` / `max-height` on open: the readout
     now rides the dock's drag row, so its distance from the bottom is
     whatever the reader last dragged to and no static offset can anchor it.
     The values here are what an unpositioned menu falls back to -- the
     resting case, which is the old bar geometry. */
  #ms-ind-menu {
    position: fixed; left: 8px; right: 8px; bottom: calc(var(--bar-h) + 6px);
    max-height: 60vh; max-height: 60dvh; overflow-y: auto; overscroll-behavior: contain;
    z-index: 11; padding: 4px 0; background: var(--ground-raised); border: 1px solid var(--n-cc);
    border-radius: 10px; box-shadow: 0 6px 24px var(--shade-22); outline: none;
  }
  #ms-ind-menu[hidden] { display: none; }
  .msm-head {
    padding: 8px 14px 3px; font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--n-6b);
  }
  .msm-head + .msm-head, .msm-opt + .msm-head { border-top: 1px solid var(--n-ee); margin-top: 4px; }
  .msm-opt {
    display: flex; align-items: center; gap: 8px; min-height: 40px;
    padding: 6px 14px 6px 34px; font-size: 14px; color: var(--n-22); cursor: pointer; position: relative;
  }
  .msm-opt[aria-selected="true"] { font-weight: 600; color: var(--label-em); }
  .msm-opt[aria-selected="true"]::before {
    content: "\2713"; position: absolute; left: 14px; color: var(--label-em); font-weight: 700;
  }
  .msm-opt.msm-cur, .msm-opt:active { background: var(--sel-soft); }
  .msm-swatch { flex: none; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--shade-15); }
  #ms-val { flex: 0 0 auto; margin-left: auto; font-weight: 600; color: var(--n-22);
            font-variant-numeric: tabular-nums; }
  #ms-val.placeholder { font-weight: 400; color: var(--n-88); font-style: italic; }
  /* "Region" before the no-selection figure: the value keeps #ms-val's own
     weight and colour, the qualifier takes the placeholder's muted treatment
     so the number still reads as the number. */
  #ms-val .ms-region { font-weight: 400; color: var(--n-88); font-style: italic;
                       margin-right: 5px; }
  #ms-ind:has(#ms-ind-text:not(:empty)) + #ms-val:not(:empty)::before {
    content: "\00b7"; margin-right: 6px; color: var(--n-aa); font-weight: 400; font-style: normal;
  }

  /* ---- The gear (Controls) ---- */
  /* A MapLibre control group at the top of the top-right stack, above the
     compass (Taylor, 2026-09-12): sheets.js prepends it, so the stack reads
     gear · compass · top-down · 2D/3D. Sized and coloured like MapLibre's own
     buttons -- 29px is the library's own control size -- with the pressed
     state the bar's Controls button used to carry. */
  #ctrl-settings { display: block; }
  #ctrl-settings button {
    display: flex; align-items: center; justify-content: center;
    width: 29px; height: 29px; color: var(--n-33); cursor: pointer;
  }
  #ctrl-settings button[aria-expanded="true"] {
    background: var(--sel); color: var(--on-accent);
  }

  /* ---- The sidebar sheet (#panel, "Controls") ---- */
  /* Full-height: its content is ~880px tall and no partial height shows a
     useful amount of it. `min-width: 0` is load-bearing, not cosmetic -- below
     380px the desktop `min-width: 380px` is what widened the layout viewport
     and left the map 0px wide (AUDIT-phone-viewports §2). Slides down from the
     top, so it and the dock never fight over the same edge. */
  #panel {
    position: fixed; inset: 0 0 var(--bar-h) 0;
    width: auto; min-width: 0; max-width: none;
    border-right: none; z-index: 9; overflow-y: auto;
    transform: translateY(-105%); transition: transform .18s ease-out;
  }
  #panel.open { transform: none; }
  #panel-close {
    display: block; position: absolute; top: 6px; right: 8px; z-index: 1;
    border: none; background: none; font-size: 24px; line-height: 1;
    padding: 4px 8px; color: var(--n-77); cursor: pointer;
  }

  /* ---- The sheet's density (S9, Taylor's ruling 2026-09-11) ---- */
  /* The desktop sidebar stacked its title block three lines deep, let every
     button + select row wrap so each select sat alone at half width, and
     put 11px type in 40px boxes. On a phone that was 1227px of sheet for a
     panel that reads fine at ~980 (DRAFT-controls-density.md). */

  /* Header: title · nav · one line of subtitle / copyright / Advanced. The
     title is the least useful line on a phone (the nav says where you are),
     so it is a quiet 12px; the block is a wrapping flex row so the subtitle
     and the footer can share a line without touching the markup. */
  #panel-title { display: flex; flex-wrap: wrap; align-items: center; padding: 8px 12px 6px; }
  #panel-title h2 { flex: 0 0 100%; font-size: 12px; font-weight: 600; color: var(--n-55); line-height: 18px; padding-right: 32px; }
  #panel-title .applet-nav { flex: 0 0 100%; margin-top: 4px; border-radius: 6px; }
  #panel-title .applet-nav a { padding: 7px 8px; font-size: 12px; }
  #panel-title .subtitle { flex: 0 1 auto; margin: 5px 6px 0 0; font-size: 11px; color: var(--n-55); }
  #panel-title .subtitle:empty { display: none; }
  #panel-title .title-footer { flex: 1 1 auto; margin-top: 5px; gap: 8px; }
  #panel-title .copyright { margin: 0 auto 0 0; font-size: 11px; color: var(--n-6b); }
  #panel-title .subtitle:not(:empty) + .title-footer .copyright::before { content: "\00b7\00a0"; }
  .adv-toggle { font-size: 11px; color: var(--n-6b); gap: 5px; }
  .adv-toggle input { width: 16px; height: 16px; }

  /* One label style for every section head: SCENARIO and TRAVELER PROFILE
     were small caps, Indicators and City Snapshot were 14px bold titles.
     The label colour is pinned to the 4.5:1 bar on the head's own ground, so
     it moves whenever that ground does: --n-6b was the lightest grey clearing
     4.5:1 on the old #f0f0f0 heads, and rung 3's warm band (--ground-band) is
     darker, which took it to 4.47. --n-66 on the band is 4.8 (the desktop's
     small caps fail axe on the same ground and are a separate fix). */
  .sec-label, .traveler-label, .picker-label {
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--n-66);
  }
  .sec-label { padding: 6px 12px; }
  .section-body { padding: 6px 10px 8px; }
  #snapshot-digest { font-size: 11px; text-transform: none; letter-spacing: 0; font-weight: 400; }

  /* Scenario: `SCENARIO [baseline v]` on one row. The flat-scenario Explore
     grid carries its own "Scenario" header cell -- a repeat of the section
     label -- over an empty axis-name column that pushed the select to 2/3
     width. Header row hidden, empty column collapsed; a multi-axis study
     keeps the axis-name column, which carries real names there. */
  #scenario-picker { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 6px 10px; }
  #scenario-picker .scenario-section-header { flex: 0 0 auto; margin: 0; }
  #scenario-table-container { flex: 1 1 160px; min-width: 0; }
  .scenario-table { margin-top: 0; }
  .scenario-table-explore .st-header { display: none; }
  .scenario-table-explore .st-axis-name:empty { display: none; }
  .scenario-table-explore { grid-template-columns: minmax(0, 1fr); }
  .scenario-table-explore:has(.st-axis-name:not(:empty):not(.st-header)) {
    grid-template-columns: minmax(0, 90px) minmax(0, 1fr);
  }
  /* Compare (applet-2, 2026-09-13): the A / B table cannot share the strip's
     row -- two selects and their headers in the ~180px left beside
     "Scenario · Explore | Compare" rendered as two bare chevrons with the
     REFERENCE / ALTERNATIVE headers printed over each other, and the sheet
     scrolled 24px sideways (measured at 390px). It takes a line of its own
     under the label + strip. The header row stays: Reference / Alternative
     is what tells the reader which select is which. A flat-scenario study's
     empty axis-name column collapses, header cell included, so the two
     selects split the width; a study with named axes keeps a 90px name
     column as explore does. */
  #scenario-picker:has(.scenario-table-compare) #scenario-table-container { flex: 1 1 100%; }
  .scenario-table-compare:not(:has(.st-axis-name:not(:empty))) {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .scenario-table-compare:not(:has(.st-axis-name:not(:empty))) .st-axis-name { display: none; }
  .scenario-table-compare:has(.st-axis-name:not(:empty)) {
    grid-template-columns: minmax(0, 90px) minmax(0, 1fr) minmax(0, 1fr);
  }

  /* Traveler profile: the header's 6px bottom margin only earns its place
     when the Specific controls are open under it. */
  #traveler-section { padding: 6px 10px; }
  .traveler-header { margin-bottom: 0; }
  #uc-specific-controls.visible { margin-top: 6px; }
  .uc-toggle-strip { border-radius: 6px; }
  .uc-toggle-btn { font-size: 12px; padding: 0 12px; }
  .traveler-grid { gap: 6px; }

  /* Indicator rows: 13px type in a 36px box, 3px gaps. A button and its
     select share the row again -- the S1 sheet let `.ind-row-inline` wrap,
     which dropped every select to its own half-width line; now the button
     label wraps to a second line before the row does. The select's cap is
     what "All destinations" needs at 13px. */
  .ind-group { margin-bottom: 6px; }
  .ind-row { margin-bottom: 3px; }
  .ind-btn { font-size: 13px; padding: 5px 9px; margin-bottom: 3px; border-radius: 5px; }
  .ind-row-inline { flex-wrap: nowrap; gap: 6px; }
  .ind-row-inline > .ind-btn { white-space: normal; line-height: 1.25; }
  .row-select { flex: 0 1 auto; max-width: 54%; padding: 3px 4px; }
  /* Below 340px "All destinations" runs under its own chevron at any cap
     worth having, so the select drops to a full-width line of its own. */
  @media (max-width: 340px) {
    .ind-row-inline { flex-wrap: wrap; }
    .ind-row-inline > .row-select { flex: 1 1 100%; max-width: 100%; }
  }
  .imp-row-label { font-size: 12px; padding: 3px 3px 3px 0; }
  .imp-btns { gap: 4px; margin: 0 0 5px; }
  .imp-btn.ind-btn { min-height: 32px; font-size: 12px; padding: 3px 9px 3px 7px; }
  .tip { width: 15px; height: 15px; font-size: 10px; line-height: 15px; }
  /* Compare mode on phones, first cut (applet-2, 2026-09-13; supersedes the
     explore-MVP ruling D6 that hid the whole strip). The Explore / Compare
     strip is visible and compare works as it does on a desktop: same
     wiring in app.js / compare.js, no isMobile() gate anywhere in JS -- the
     layout is decided here and only reported by JS, so a compare entered on
     a wide window survives a rotation with its toggle still on screen.
     Two pieces stay hidden, deferred rather than declined:
     - the change / outcome strip: the outcome view is stacked 3D prisms
       plus the cap slider in the Advanced legend fold, its own design
       surface. compare.js clears the inline display on entry, so this rule
       is what keeps it off; state.compareView stays at its 'change' default.
     - the compare-summary cards (#sec-compare-summary): four desktop cards,
       a histogram and a per-profile table among them, with no phone
       treatment yet. Hidden with the same inline-clear caveat. The snapshot
       digest it replaces on a desktop is hidden by compare.js in its turn,
       so in compare the Controls sheet simply has neither. */
  #compare-view-toggle, #sec-compare-summary { display: none !important; }

  /* ---- The dock sheet (#measurements, "Results") ---- */
  /* Permanently on screen since 2026-09-12 (Taylor): there is no bar to open
     it from and no closed state to open out of. It rests at --bar-h, showing
     its drag row and nothing else, and sheet_drag.js writes an inline px
     height for anything taller -- free between that floor and 24px short of
     the top. A tap on the drag row is the shortcut to 35dvh, which is what
     the stylesheet used to open the sheet at: on an 844px phone that gives
     #mp-body ~230px, the lead rows of any panel (the mapped number and its
     decomposition) with most of the map still on show. Anything with a table
     wants dragging up (AUDIT §5).

     `overflow: hidden` is load-bearing at rest: the flex column's header sits
     below a floor that is now the drag row alone, and without it the header
     paints outside the sheet's rounded top over the map. The height
     transition is what makes the tap read as a movement rather than a jump;
     sheet_drag.js sets `transition: none` for the duration of a drag, so the
     height never lags the finger. */
  #measurements {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    flex: none; height: var(--bar-h); z-index: 8; overflow: hidden;
    border-top: 1px solid var(--n-dd); border-radius: 14px 14px 0 0;
    box-shadow: 0 -4px 18px var(--shade-16);
    transition: height .18s ease-out;
  }
  /* The drag row is the sheet's whole resting state: an 18px grip strip over
     the 38px readout, one 56px surface. Merged rather than stacked as two
     rows of chrome (Taylor, 2026-09-12) -- it buys back 28px of map against
     the bar it replaces. The whole row drags and the whole row taps, with no
     holes: sheet_drag.js is passed `grabsControls`, so a press on the
     indicator picker starts a drag like a press anywhere else on the
     parchment, while the picker keeps its own click for a press that does not
     move (Taylor, 2026-09-12). `touch-action: none` keeps the browser from
     turning the gesture into a page scroll or pull-to-refresh before the
     pointer events see it, and `user-select: none` keeps a slow drag from
     selecting the readout's text -- the drag cannot preventDefault a press on
     the picker without also eating the picker's click.

     The 24px invisible hitbox that used to reach past the row is gone
     (2026-09-12). It existed to make a 44px handle easier to grab; the row is
     56px now and sits on the screen's bottom edge, and since a tap on it
     collapses the sheet, an invisible tap zone lying over the header and the
     first rows of the results would dismiss them from a tap that looks like
     it landed on content. */
  .sheet-handle {
    display: flex; flex-direction: column; padding: 0;
    height: calc(var(--grip-h) + var(--readout-h));
    box-sizing: border-box; cursor: grab; flex: 0 0 auto; touch-action: none;
    -webkit-user-select: none; user-select: none;
    border-radius: 14px 14px 0 0; background: var(--ground-trim); position: relative;
  }
  .sheet-grip {
    flex: 0 0 auto; height: var(--grip-h);
    display: flex; align-items: center; justify-content: center;
  }
  /* Reversed on 2026-09-12, same day: the drag bar is *darker parchment*, not
     slate (Taylor). The handle row is plate -- the brand's structural-trim
     stop -- against the sheet's parchment body, so the drag surface is marked
     off by value rather than by temperature. The warm family has no mid-tone
     that is not gold, so the grip pill takes --gold-deep; at 2.10:1 on plate it
     is a better affordance than the cool pill it replaces (1.48:1 on the old
     bar), and it is the one place gold appears without meaning "selected". */
  .sheet-grip::before { content: ""; width: 38px; height: 4px; border-radius: 2px; background: var(--gold-deep); }
  /* The header (S12 ruling): no context line and no O -> D pills on phones.
     The context repeated what the summary strip under the sheet already
     says (indicator · value), and zone ids are H3 cells that say nothing to
     a reader. What is left is the OD family's stage prompt, and the two
     icon-only buttons (⇆ / ✕, the aria-labels carry the verbs), which are
     lifted out of the header's flow onto the handle row. Outside the OD
     family the prompt is empty and the header collapses to nothing, so the
     body starts 44px from the sheet's top. */
  #mp-header { flex-wrap: wrap; padding: 0 12px 6px; }
  #mp-header .mp-spacer, #mp-header .mp-od, #mp-header .mp-btn-label, #mp-header #mp-context { display: none; }
  #mp-header .mp-stage { flex: 1 1 0; min-width: 0; white-space: normal; }
  #mp-header .mp-stage:empty { display: none; }
  #mp-header:has(.mp-stage:empty) { padding: 0; border-bottom: none; }
  /* Both header buttons are gone from the phone layout (Taylor, 2026-09-12).
     Clear has a full replacement in the map: tapping a pinned zone again
     unpins it, which is what the OD stage prompt already tells the reader to
     do. Swap does not -- there is no other way to reverse an O -> D pair on a
     phone. They are hidden rather than removed from the markup, so app.js's
     wiring and renderMeasurementsHeader's `hidden` bookkeeping are untouched
     and the desktop keeps both. */
  #mp-header .mp-btn { display: none; }
  #mp-body { max-height: none; overflow-y: auto; }

  /* ---- The legend ---- */
  /* Top-left keeps it clear of the bottom sheets and of MapLibre's controls
     at top-right. Always open and basic (Taylor, 2026-09-12): the title and
     the colour ramp -- or the category / mode swatches that stand in for a
     ramp -- and map.js's own "Map Overlays" fold, which starts closed. The
     Advanced block (scale mode, opacity, height, cap sliders) folds in
     with the overlays: it is ordered after them and hidden while the fold
     is closed (sheets.js mirrors the fold as `.adv-folded`); app.js's
     Advanced checkbox still decides whether it exists at all. The 3D
     toggle and the compare-view caption are desktop-only here (the caption
     names a view the phone cannot switch); the 3D toggle
     is relayed through the #ctrl-3d map control below. */
  #legend {
    top: 10px; left: 10px; right: auto; bottom: auto;
    max-width: min(62vw, 240px); padding: 6px 8px;
    /* Below the sheets (8, 9): a fully raised dock's handle sits at 24px from
       the top and the desktop's z-index 900 would put the chip on top of it.
       Still above MapLibre's controls (z 2). */
    z-index: 5;
    /* A three-column grid: row 1 is the title with the overlays fold's
       glyph at its right; row 2 is min . bar . max (the ramp's labels flank
       the bar rather than sit under it -- Taylor, 2026-09-12, vertical
       space); every other child spans the row. `.leg-labels` and
       `#legend-overlays` become `display: contents` so their children are
       placed by the grid directly; the `order`s put min, bar, max ahead of
       every order-0 child so auto-placement lands the three together. */
    display: grid; grid-template-columns: auto 1fr auto; column-gap: 6px; align-items: center;
  }
  #legend > * { grid-column: 1 / -1; }
  #legend .leg-labels, #legend #legend-overlays { display: contents; }
  #legend #leg-title { grid-row: 1; grid-column: 1 / 3; font-size: 12px; line-height: 16px; margin: 0 0 3px; }
  #legend #leg-min { grid-column: 1; order: -3; color: var(--n-55); }
  #legend .leg-bar { grid-column: 2; order: -2; margin: 0; min-width: 100px; height: 10px; }
  #legend #leg-max { grid-column: 3; order: -1; color: var(--n-55); }
  /* The fold's heading is the "Map Overlays ►" line map.js builds; on a
     phone only its arrow shows, as a glyph on the title row (the heading
     text stays in the DOM for screen readers). The tap target is the
     whole 16px row across the third column. The divider moves to the
     fold's body so a closed fold costs no height at all. */
  #legend #legend-overlays-toggle {
    grid-row: 1; grid-column: 3; justify-self: end; margin: 0 0 3px;
    font-size: 0; line-height: 16px; padding: 0 0 0 10px; cursor: pointer;
  }
  #legend #legend-overlays-toggle .collapse-arrow { font-size: 12px; color: var(--n-77); margin: 0; }
  #legend #legend-overlays-body { grid-column: 1 / -1; order: 3; border-top: 1px solid var(--n-dd); margin: 6px 0 0; padding: 3px 0 0; }
  #legend #leg-advanced { order: 4; margin: 6px 0 0; }
  #legend.adv-folded #leg-advanced { display: none !important; }
  #legend #btn-3d-toggle, #legend #leg-compare-view { display: none !important; }

  /* ---- The 2D/3D map control ---- */
  /* sheets.js appends it to MapLibre's top-right corner under the compass
     and top-down buttons, styled as one more control group; the text names
     the view a tap gives, and that label is the whole state display -- no
     active colour (Taylor, 2026-09-12). Hidden off the phone layout, where
     the legend's own toggle is the control. */
  #ctrl-3d { display: block; }
  #ctrl-3d button { font-size: 11px; font-weight: 700; color: var(--n-33); display: flex; align-items: center; justify-content: center; }
  #ctrl-3d button:disabled { color: var(--n-bb); }

  /* ---- The dock panels: the reflow (ruling D3) ---- */
  /* Every panel becomes one vertical stack. The three flex rows and the
     multicolumn body are what laid the desktop dock out sideways; the
     `max-width: none`s release the columns that were sized for a 1280px
     dock. A table wider than the phone scrolls inside its own box. */
  #mp-body { columns: auto; column-gap: normal; column-rule: none; }
  .pn-mc-main, .dcp-cols, .stp-cols { flex-direction: column; align-items: stretch; gap: 10px; }
  .dcp-col-mid, .stp-col-lead, .pn-mc-side { max-width: none; flex-basis: auto; }
  /* The dock is ONE scroller on a phone: #mp-body. Side by side these regions
     divide a 1280px dock's width, which is why each is a flex item that may
     shrink and carries its own scroller; stacked they would divide the dock's
     ~220px of HEIGHT instead, and the region at the bottom of the stack gets
     whatever the ones above it leave. That is how the multi-profile
     mode-choice panel lost its per-profile table: 137px of table in a 33px
     slot, inside its own scroller, under a lead block that had already taken
     177px -- and #mp-body had nothing to scroll, because .stp-cols had shrunk
     to fit (Taylor, 2026-09-12). So on a phone every region sizes to its
     content and nothing shrinks; the overflow lands on #mp-body, which is the
     scroller the reader already has a gesture for. Dragging the sheet taller
     still reveals more of the same column, it is no longer the only way to
     reach the bottom of it. */
  .pn-mc-main, .dcp-cols, .stp-cols,
  .pn-mc-tbl, .pn-mc-side, .dcp-col-mid, .stp-col-lead { flex: 0 0 auto; }
  /* .dcp-col-cats is the panel that actually loses data on a desktop dock at
     phone width -- it collapsed to 4px and was clipped with no scrollbar
     (AUDIT §5). Stacked and released it gets the full width back. */
  .dcp-col-cats { flex: 0 0 auto; }
  /* Horizontal only: a table wider than the phone still scrolls inside its own
     box. `overflow-y: visible` alongside it computes to `auto`, which is inert
     here -- the box is content-height, so there is never anything to scroll. */
  .stp-col-tbl, .dcp-col-cats { flex: 0 0 auto; overflow-x: auto; overflow-y: visible; max-width: 100%; }
  .dcp-col-cats table { min-width: max-content; }
  /* Two-up category sub-tables stack when the pair does not fit. */
  .dcp-cat-cols { flex-wrap: wrap; }
  /* The cumulative panel's purpose + by-mode tables are a flex row on the
     desktop; at 390px the second table sat entirely off-screen to the right
     of the scroll box, with nothing to say it was there (found reading the
     panels, 2026-09-11). Stacked, each scrolls on its own. */
  #mp-body .cum-tbls { flex-direction: column; align-items: stretch; }
  #mp-body .cum-tbls > table { max-width: 100%; }
  #mp-body .cum-tbls > * { overflow-x: auto; }

  /* ---- Mode choice: one card per mode (treatment B, ruled 2026-09-11) ---- */
  /* The column headers become in-row labels via `td::before { content:
     attr(data-label) }`; sheets.js copies each `thead th` into the cells after
     every render. If the labeler has not run (JS off, or a render it did not
     see) the cells simply show without labels -- the numbers are still right,
     and the formula line under the table still names the columns in order. */
  .pn-mc-tbl { flex: 0 0 auto; max-width: 100%; overflow-x: visible; }
  .pn-decomp, .pn-decomp tbody { display: block; width: 100%; }
  .pn-decomp thead { display: none; }
  .pn-decomp tbody tr {
    display: block; width: 100%;
    margin: 0 0 8px; padding: 6px 8px; border-radius: 6px;
    border: 1px solid var(--shade-12);
  }
  /* Label left, value(s) packed right: a cell may hold more than one node
     ("$0.597" + its "(1.7)" span), and `space-between` would spread them. */
  #mp-body .pn-decomp td {
    display: flex; justify-content: flex-end; align-items: baseline;
    flex-wrap: wrap; gap: 4px 8px; padding: 2px 0; border: 0;
    text-align: right; min-width: 0; white-space: normal;
  }
  #mp-body .pn-decomp td::before {
    content: attr(data-label); text-align: left; color: var(--n-66);
    font-weight: 400; font-size: 11px; flex: 0 0 auto; margin-right: auto;
  }
  /* The mode-specific cell holds N chips; they wrap instead of widening the
     card (Walk's three chips overflowed 390px by 27px in the S2 mock). */
  #mp-body .pn-decomp td.pn-decomp-chips { max-width: none; row-gap: 4px; }
  /* The mode name is the card's heading, not a labelled row. */
  #mp-body .pn-decomp td:first-child {
    display: block; text-align: left; font-weight: 600;
    font-size: 14px; margin-bottom: 4px;
  }
  #mp-body .pn-decomp td:first-child::before { content: none; }
  /* The mapped-mode marks were drawn as cell-edge box-shadows on a row of
     cells; on a card they become the card's own border. */
  #mp-body .pn-decomp tr.pn-mapped-mode { border-color: var(--n-44); border-width: 2px; }
  #mp-body .pn-decomp tr.pn-mapped-mode td,
  #mp-body .pn-decomp tr.pn-mapped-mode td:first-child { box-shadow: none; }
  /* The collapsed unavailable-modes row is a sentence with colspan; it keeps
     its own styling and gets no label. */
  #mp-body .pn-decomp tr.pn-mode-unavail { border: none; padding: 0 2px; }
  #mp-body .pn-decomp tr.pn-mode-unavail td:first-child {
    font-size: 10px; font-weight: 400; margin: 0;
  }
  #mp-body .pn-decomp tr.pn-mode-unavail td::before { content: none; }

  /* ---- Type and touch targets ---- */
  /* 36px is the floor for a finger (S9 ruling; was 40 -- well above WCAG
     2.5.8's 24px, under Apple's 44pt guideline). 42 targets were under it
     and every <select> was at 12px (AUDIT §3). `#app` is for specificity:
     the desktop sheet sizes selects by class and id.
     Selects are 13px at rest and 16px on focus: iOS Safari zooms the page
     when a focused control's type is under 16px, and it decides at focus
     time -- the usual workaround, NOT yet verified on a real phone (S8;
     fall back to 16px at rest if it zooms). */
  #app select, #app input, #app textarea { font-size: 13px; }
  #app select:focus, #app input:focus, #app textarea:focus { font-size: 16px; }
  #app select { padding: 4px 6px; line-height: 22px; min-height: 32px; }
  .ind-btn, .mp-btn, .uc-toggle-btn, .mode-btn, .norm-btn, #btn-3d-toggle { min-height: 36px; }
  .uc-toggle-strip { flex-wrap: wrap; }
  input[type="range"] { height: 28px; }
}
