/* ═══ MOBILE / RESPONSIVE LAYER ══════════════════════════════════════════
   Every rule here is gated behind max-width:1179px — i.e. it only applies
   BELOW the artboard's own 1180px floor (--ab-min-w), which are exactly the
   widths that previously just horizontally-scrolled the fixed desktop layout.
   At ≥1180px NOTHING in this file matches, so the desktop design is
   byte-for-byte unchanged. Linked LAST in index.html so these overrides win.

   Strategy (docs/mobile/03, "Approach B"): unlock the width, turn the fixed
   8×9 grid into a vertical stack, let the page scroll, and make each screen a
   normal stacked page. Desktop's overlay-cover + internal-scroll model is
   replaced here with plain document flow. */

@media (max-width: 1179px) {

  /* ── readability: bump the type scale (desktop keeps 12/10/9) ──────────── */
  :root {
    --text-xl: 24px; --text-lg: 17px; --text-base: 14px;
    --text-sm: 12px; --text-xs: 11px;
  }

  /* iOS Safari ignores `user-scalable=no`/`maximum-scale` in the viewport meta
     tag (deliberate, since iOS 10 — an accessibility guarantee that can't be
     overridden that way), so pinch-zoom still worked despite it. touch-action
     is a separate, lower-level gesture control that Safari does honor: pan-x
     pan-y allows normal scrolling/panning but drops the pinch and double-tap
     zoom gestures entirely. Html+body (not just .viewport) because the pinch
     gesture is recognized against the whole page, not just the scroller. */
  html, body { touch-action: pan-x pan-y; }

  /* ── shell: unlock width, stack vertically, scroll the whole page ──────── */
  .viewport {
    overflow-x: hidden; overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .artboard {
    width: 100%; min-width: 0;
    height: auto; min-height: 0;
    display: flex; flex-direction: column;
    gap: 18px;
    overflow: visible;
    /* safe-area insets so nothing hides under the notch / home indicator.
       TOP: env(safe-area-inset-top) is 0 inside a plain browser tab or the
       device-preview iframe (only a standalone/installed PWA reports a real
       inset), so the fallback is what actually reserves room for the status
       bar there. 59px is the iPhone 15's real top safe-area inset (393x852pt:
       59pt top / 34pt bottom) — it clears the 54pt status bar plus the notch
       band, matching the emulator; a real notched device reports ~59px too, so
       either way max() lands on the same value.
       BOTTOM is sized to clear the floating tab island (see .tabs below) —
       ~46px pill + 16px offset + a little breathing room — so the last
       block of page content never sits behind it. */
    padding:
      max(59px, env(safe-area-inset-top))
      max(16px, env(safe-area-inset-right))
      max(88px, calc(72px + env(safe-area-inset-bottom)))
      max(16px, env(safe-area-inset-left));
  }

  /* ── header: just logo + action icons — the tabs live in their own
        floating island, pulled out of the header entirely (below) ───────── */
  .hdr {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px;
  }
  .logo { flex: 1 1 auto; grid-column: auto; }
  /* margin-left:auto (not just justify-content:space-between on .hdr) keeps
     the icons pinned to the right edge even when .logo is the ONLY sibling
     hidden below — space-between with a single remaining flex item collapses
     to flex-start (left), which would otherwise yank the icons across the
     header the moment the title disappears. */
  .hdr-actions { flex: none; grid-column: auto; margin-left: auto; }

  /* Bell popover centers on the header instead of hugging the icon — on a
     narrow phone screen a strictly icon-anchored dropdown reads as cramped
     against the edge; desktop keeps the icon-aligned default (header.css/
     activity.css). Drop .hdr-popover-wrap's positioning so .activity-menu's
     containing block falls through to .hdr (header.css) instead of the tight
     wrap — only then does centering land on the full header, not the icon.
     right:auto cancels .user-menu's inherited right:0 (which would otherwise
     over-constrain left+right+width and stretch this to fill the gap instead
     of sizing to its own content). */
  .hdr-popover-wrap { position: static; }
  .activity-menu {
    left: 50%; right: auto;
    transform: translateX(-50%);
    animation: activity-menu-pop-in var(--dur-fast) ease-out;
  }

  /* App title (logo + "studio dashboard" subtitle) only makes sense as a
     top-level "where am I" marker on the Dashboard's own iOS-style large
     title; Project/Tracker already carry their own page heading in the cover
     bar ("Project Manager" / "Project Tracker"), so repeating the app name
     above it is redundant chrome eating vertical space on a small screen. */
  body:not([data-tab="dashboard"]) .logo { display: none; }

  /* ── bottom tab island: fixed to the true viewport (not the scrolling
        .artboard), so it stays put while the page scrolls under it. A
        transformed/filtered ancestor would hijack position:fixed's anchor —
        none of .artboard/.viewport set transform/filter, so this is safe. ─── */
  .tabs {
    position: fixed;
    left: 50%; bottom: max(16px, env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: 40;                      /* below modals(50)/popovers(55-70) */
    grid-column: auto;
    width: auto; max-width: calc(100% - 32px);
    display: flex; gap: 2px;
    padding: 5px;
    background: var(--popover-bg);
    -webkit-backdrop-filter: var(--popover-blur);
    backdrop-filter: var(--popover-blur);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-pill);
    box-shadow: 0 8px 28px rgba(0,0,0,.28);
  }
  .tab {
    flex: 1; text-align: center; white-space: nowrap;
    font-size: var(--text-sm);
    padding: 10px 20px;
    border-radius: var(--radius-pill);
  }
  /* Desktop's underline doesn't fit a pill — active state is a filled pill
     instead, the standard iOS segmented-control treatment. */
  .tab.active { background: var(--fg); color: var(--bg); }
  .tab.active::after { display: none; }

  /* ── inputs ≥16px: the single biggest iOS fix (stops focus-zoom) + taller
        touch targets on all form controls ──────────────────────────────── */
  input, select, textarea, .csel-trigger { font-size: 16px; }
  input:not([type="checkbox"]), select, .csel-trigger { min-height: 42px; }
  textarea { min-height: 84px; }

  /* ── tab-based page visibility ─────────────────────────────────────────
     Desktop hides off-tab dashboard chrome with visibility:hidden (keeps its
     grid space) and hides panels by overlaying a cover on top. In a stacked
     flow neither works — use display:none so hidden content takes no space,
     and turn the covers into normal stacked pages instead of faded overlays. */
  body:not([data-tab="dashboard"]) .dash-chrome,
  body:not([data-tab="dashboard"]) .panel { display: none; }

  .cover {
    position: static; z-index: auto;
    grid-column: auto; grid-row: auto;
    opacity: 1; transform: none; pointer-events: auto;
    display: none;
  }
  body[data-tab="project"]  #projectCover,
  body[data-tab="tracker"]  #trackerCover { display: flex; }
  .cover-backdrop { display: none !important; }

  /* ── dashboard: pair each heading with its panel, then stack, and give
        each block a real height (grid rows no longer allocate one) ───────── */
  .hd, .panel, .span-select, .tl {
    grid-column: auto; grid-row: auto;
  }
  .hd-overview  { order: 1; } .pn-overview { order: 2; }
  .hd-priority  { order: 3; } .pn-priority { order: 4; }
  .hd-todo      { order: 5; } .pn-todo     { order: 6; }
  .hd-timeline  { order: 7; } .span-select { order: 8; } #timeline { order: 9; }

  /* Heading alignment fix. header.css sets `.hd { align-self: end }`, which on
     the desktop CSS GRID means "bottom-align in the row cell" (block axis). In
     this mobile flex COLUMN, align-self runs on the CROSS (horizontal) axis
     instead, so that same rule silently becomes "right-align" — leaving the
     dashboard headings (Overview/Priority/To-Do/Timeline) hugging the right
     edge while every other heading in the app (logo, cover bars, modal h3s)
     sits left. Pin them back to the start so all headings read consistently
     left, and each one lines up with the left-aligned panel content it labels.
     Scoped to .dash-chrome so cover-bar's own `.cover-bar .hd` (higher
     specificity, correct on mobile) is untouched.

     Grouping (proximity → relationship) rides on the same selector: couple
     each heading to the panel it labels and add air between sections. The
     artboard's flat 18px gap makes a heading equidistant from its own panel
     and the previous one; negative margin-bottom pulls its panel up under it
     (→ ≈10px), while margin-top opens the gap to the section above (→ ≈28px).
     Flex honours negative margins, so this needs only the heading rule. */
  .hd.dash-chrome { align-self: start; margin-top: 10px; margin-bottom: -8px; }

  .panel { min-height: 300px; padding: 18px 16px; }
  .pn-overview { min-height: 340px; }   /* the scatter needs 2D room */

  /* Attention-board caption: on the desktop-sized plot it sits clear below the
     dots, but the mobile plot is far shorter and narrower, so it collides with
     the START/DUE axis ticks and the HEALTHY/CLEAR corner labels — three label
     rows crushed into a ~30px band at the bottom. Hide it here: it restates
     the x-axis ("← % elapsed →", not-started/overdue) that the always-visible
     top legend ("…left-to-right is how far through its own schedule it is")
     already explains, so nothing is lost — the ticks + corner quadrant labels
     stay, and they now clear each other. Redundant caption, not a feature. */
  .attn-caption { display: none; }
  #timeline { height: 360px; }
  .span-select { flex-wrap: wrap; }
  /* 6-Month's day-per-pixel ratio is too dense to read on a phone-width
     chart even with the day ruler dropped (timeline.js handles 3-Month's
     ruler for the same reason) — drop the option itself rather than ship a
     span that just renders as a blur of phase bars. */
  .span-select [data-span="180"] { display: none; }

  /* covers sit after the (now-hidden) dashboard blocks; credit last */
  #projectCover, #trackerCover { order: 20; }
  .credit { order: 30; position: static; margin-top: 4px; }

  /* ── press feedback (Apple: respond instantly on touch-down) ─────────────
     Touch has no :hover, so tappable cards/rows give no feedback on press.
     The tabs already carry the instant-scale idiom (.tab:active, header.css);
     extend it to the other genuinely-tappable targets — the to-do dashboard
     panel and the project rows, both role="button". Priority rows are
     display-only and deliberately get none. Mobile-scoped so desktop mouse
     behaviour stays byte-for-byte unchanged. A large surface takes a smaller
     scale than a small pill so the movement stays subtle. */
  .pn-todo { transition: border-color var(--dur-fast), transform var(--dur-fast); }
  .pn-todo:active { transform: scale(0.99); }
  .pm-row:active { background: var(--hover-bg); }

  /* ── modals → bottom sheets (fixed to the viewport, not the tall artboard) ── */
  .scrim {
    position: fixed;
    align-items: flex-end;
  }
  .modal,
  .modal-full,
  .modal-confirm,
  .pvp-modal {
    width: 100%; max-width: 100%;
    max-height: 88vh; height: auto;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
  }
  /* Sheet slides from the edge it's docked to instead of inheriting the
     desktop dialog's centered fade+nudge (modals.css) — a bottom-flush panel
     that only fades near the bottom reads as a mismatch once the backdrop
     itself started fading too (see .scrim.open, modals.css). Exit stays at
     --dur-fast (150ms) specifically so it finishes inside srModal.close's
     180ms class-removal timeout (core/overlays.js) — the sheet would
     otherwise still be mid-slide when display snaps to none. */
  @keyframes sheet-in { from { opacity: 0; transform: translateY(100%); } }
  @keyframes sheet-out { to { opacity: 0; transform: translateY(100%); } }
  .modal,
  .modal-full,
  .modal-confirm,
  .pvp-modal {
    animation: sheet-in var(--dur-base) ease-out;
  }
  .scrim.closing .modal,
  .scrim.closing .modal-full,
  .scrim.closing .modal-confirm,
  .scrim.closing .pvp-modal {
    animation: sheet-out var(--dur-fast) ease forwards;
  }
  /* New/Edit Project and the To-Do item modal (both plain .modal — the New/
     Edit To-Do form shares the same treatment, consistently) get a side inset
     instead of running edge-to-edge — .scrim's own justify-content:center
     (unchanged from the base rule) centers the now-narrower sheet, so no
     margin/auto is needed here. Still bottom-flush (sheet behaviour, not a
     floating card) — modal-confirm keeps its own all-sides-margin treatment
     below; .modal-full (All To-Dos) deliberately stays full width, closer to
     a page than a form. */
  .modal, .pvp-modal {
    width: calc(100% - 32px); max-width: calc(100% - 32px);
    margin-bottom: max(16px, env(safe-area-inset-bottom));
  }
  .modal-confirm { border-radius: var(--radius-md); margin: 0 12px 12px; width: calc(100% - 24px); }
  /* Select buttons ("New build", "Fast", phase owners) read oversized/bold
     against their 12px labels — the app-wide `input, select, textarea,
     .csel-trigger { font-size: 16px }` rule (above) exists specifically to
     stop iOS zooming the page when a text INPUT gains focus; .csel-trigger is
     a <button> (custom-select.js), never a real text-entry field, so it never
     triggers that zoom and doesn't need to hold the 16px floor. Real <input>/
     <textarea>/<input type=number"> fields keep 16px — shrinking those would
     bring the zoom bug back. */
  .modal-body .csel-trigger { font-size: var(--text-base); }

  /* ── form modals (New/Edit Project, New/Edit To-Do): fit without scrolling ─
     The `.frow` grid only exists in these two form modals, so targeting it
     scopes all of this to them (not .modal-full "All To-Dos" or .modal-confirm).
     Goal: no vertical OR horizontal scroll on a phone. Two problems fixed —
     (1) horizontal: `.frow`'s `1fr` field column had min-width:auto, so a date
     input's min-content ("18/07/2026" at 16px) forced the track — and the whole
     sheet — ~26px past the viewport. minmax(0,1fr) lets it shrink instead.
     (2) vertical: the body ran ~100px taller than its cap. Reclaimed mostly
     from the phase-owner block (5 stacked selects → a 2-up grid) plus trimmed
     row gaps, field heights, and sheet padding. */
  .frow {
    grid-template-columns: 120px minmax(0, 1fr);
    gap: 10px; margin-bottom: 7px;
  }
  /* min-width:0 on the field-side wrappers too — the grid track can shrink now,
     but a flex/inline child (prefixed-input, nstep, csel) still defaults to
     min-width:auto and would re-inflate the track from inside. */
  .frow input, .frow select, .frow textarea,
  .frow .csel, .frow .prefixed-input, .frow .nstep { min-width: 0; }
  .frow input:not([type="checkbox"]), .frow select,
  .frow .csel-trigger { min-height: 38px; }
  .frow textarea { min-height: 60px; }
  /* .nstep's number input matched the rule above (any non-checkbox <input> in
     a .frow) and grew to 38px, but its ± buttons — the same visual pill,
     border-matched via border-left/right:none on the input — kept their own
     fixed height:var(--control-h) (28px), unaffected since they're <button>
     not <input>. That 10px mismatch broke the pill: the taller input's
     top/bottom border stuck out past the now-shorter-looking buttons on
     either side. Bring the buttons up to the same 38px so the pill reads as
     one continuous border again. */
  .frow .nstep-btn { height: 38px; }

  /* Trim the sheet's own chrome (desktop values are generous for a big dialog). */
  .modal-body { padding: 12px 16px; }
  .modal-head { padding: 14px 16px 10px; }
  .modal-foot { padding: 10px 16px 14px; }

  /* Phase owners: the densest win. Desktop stacks 5 full-width selects (~240px);
     here the "Phase owners" label goes on its own line (frow → block) and the
     five phase selects pack into a full-width 2-column grid (~3 rows). Each
     cell keeps its phase name beside a shorter select — it's an advisory field,
     so it can sit tighter than the main inputs. */
  .frow-owners { display: block; }
  .frow-owners > label { display: block; padding-top: 0; margin-bottom: 6px; }
  .pm-owners { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; }
  /* Fixed label column, not auto — auto sizes each .pm-owner-row's own grid
     independently (they're separate formatting contexts, not one shared
     grid), so a row's label width was whatever ITS OWN phase name needed
     (measured 25px for "Data"/"DED" vs 72px for "List Schedule"), pushing
     each row's select to a different width (73-119px, a 46px spread) even
     though they're meant to read as one consistent column.
     Width picked from real measurements, not a guess: "Schematic" (56px) and
     "Development" (71px) are each ONE word with no space to wrap at, so any
     column narrower than the longer of the two forces a raw mid-word split
     ("Schem-" / "atic") — tried `hyphens:auto` for a proper syllable break
     instead, but Chromium doesn't reliably hyphenate even with lang="en" set
     (confirmed live: no hyphen character rendered, just a bare split), so it
     can't be relied on as the actual fix, only left in as a harmless
     progressive enhancement for engines that do support it. 74px (71px +
     buffer) is therefore the SMALLEST width that fits every label on one
     line with no wrap at all — smaller looks "denser" but reads as broken. */
  .pm-owner-row {
    grid-template-columns: 74px minmax(0, 1fr);
    gap: 6px; align-items: center;
  }
  .pm-owner-row > span {
    font-size: var(--text-sm); white-space: nowrap;
    hyphens: auto; -webkit-hyphens: auto;
  }
  .frow-owners .csel { min-width: 0; }
  .frow-owners .csel-trigger { min-height: 34px; }

  /* ── To-Do list modal ("All To-Dos") header — same density principle as the
     form modals above, applied to a header instead of a form. Desktop packs
     title + Sort + "+ Add to-do" + Close into one row; at 343px of content
     width that's ~400px of content fighting for space (title ~80px + sort
     ~140px + add ~110px + close ~36px + 3 gaps), genuinely cramped, not just
     visually busy. CSS Grid (not flex-wrap) so Close reliably stays paired
     with the title on row 1 regardless of how wide Sort/Add render — the
     title effectively gets the "dedicated heading" row, with Sort + Add
     demoted to their own toolbar row below instead of competing inline. */
  #todoListModal .modal-head {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas: "title close" "sort add";
    row-gap: 10px; column-gap: 12px;
  }
  #tdlTitle { grid-area: title; }
  #tdlClose { grid-area: close; justify-self: end; }
  .tdl-sort { grid-area: sort; }
  #tdlAdd { grid-area: add; justify-self: end; }
  /* Sort's select-button inherited the same 16px-for-iOS-zoom floor as every
     .csel-trigger, oversized for a header control — same fix as the form
     modals' Type/Response-speed/phase-owner selects (.csel-trigger is a
     <button>, never a real text field, so it was never actually load-bearing
     for that fix). Scoped to .modal-head since .modal-body already has its
     own version of this rule (form modals) — together they cover every
     select-button in any modal without a single unscoped, easy-to-forget rule. */
  .modal-head .csel-trigger { font-size: var(--text-base); }

  /* the insight chart modal: full width, phone-height */
  .pvp-modal { max-height: 88vh; }
  .pvp-canvas-wrap { min-height: 260px; }

  /* Flush, edge-to-edge sheets (.modal-confirm excluded — it stays a small
     floating card with margin on all sides, not a flush sheet, so it keeps
     desktop's shared modal-in/modal-out fade+settle) get the native
     bottom-sheet motion instead: a real slide from off-screen, matching where
     they're anchored — spatial consistency, not just a subtler fade. Desktop's
     @keyframes modal-in/modal-out (modals.css) are untouched; these are
     separately-named so neither definition can shadow the other. */
  @keyframes sheet-in  { from { opacity: 0; transform: translateY(100%); } }
  @keyframes sheet-out { to   { opacity: 0; transform: translateY(100%); } }
  .modal, .modal-full, .pvp-modal {
    animation: sheet-in var(--dur-base) ease-out;
  }
  .scrim.closing .modal,
  .scrim.closing .modal-full,
  .scrim.closing .pvp-modal {
    animation: sheet-out var(--dur-fast) ease-out forwards;
  }
  /* .modal-confirm also carries the base .modal class, so without this
     explicit override it would inherit sheet-in/-out from the rule above
     (same selector specificity, later rule wins) — restore desktop's shared
     fade+settle for it specifically. */
  .modal-confirm { animation: modal-in var(--dur-base) ease-out; }
  .scrim.closing .modal-confirm { animation: modal-out var(--dur-fast) ease forwards; }

  /* toast + status-menu overlay must track the viewport too, not the artboard.
     Toast's bottom offset is also bumped clear of the floating tab island. */
  .toast { position: fixed; bottom: max(80px, calc(64px + env(safe-area-inset-bottom))); }
  .status-menu-layer { position: fixed; }

  /* ── TRACKER ───────────────────────────────────────────────────────────
     The phase-card row is the heart of this request: 5-across on desktop,
     3+2 at tablet, stacked on a phone. The COLUMN COUNT is chosen in JS
     (tracker-cards.js applyCardLayout, which measures real content width and
     writes grid-template-columns inline); this block owns everything else —
     turning off the desktop "fixed-height single row + internal card scroll"
     model so the cards grow to their content and the PAGE scrolls instead. */
  .trk-scroll {
    flex: none;
    overflow: visible;
    min-height: 0;
  }
  .trk-cards {
    height: auto;
    grid-template-rows: none;   /* let cards wrap onto as many rows as needed */
  }
  .trk-card {
    height: auto; min-height: 0;
    /* min-width:0 lets the card shrink to its grid track instead of expanding
       to its longest item row (grid items default to min-width:auto). Without
       it a long task name forces the whole card wider than the column and it
       overflows the screen; with it the name ellipsizes like on desktop. The
       3-col track's minmax(260px,1fr) floor still stops it going actually small. */
    min-width: 0;
    overflow: visible;          /* show full content; no per-card scrollbar */
  }

  /* toolbar: stack picker / overall / insight vertically. Inside the overall
     block itself, also stack cap → percentage → bar (each on its own line)
     instead of desktop's single row, so the whole toolbar reads top-to-bottom
     as: project picker, "Overall Progress" label, the %, the bar, the help
     text, then the insight button. */
  .trk-toolbar {
    flex-direction: column; align-items: stretch;
    gap: 12px; padding: 14px;
  }
  /* Project picker reads as a plain heading, not a boxed field. The actual
     clickable/visible piece is .csel-trigger — custom-select.js (core/
     custom-select.js) replaces the native <select> with a button + portalled
     listbox pair and keeps the original <select> around only as the hidden
     data model, so restyling .csel-trigger is what changes the picker's
     LOOK; the click → open-listbox wiring is untouched, same trigger button. */
  .trk-picker { justify-content: flex-start; gap: 0; }
  .trk-picker-label { display: none; }
  .trk-picker .csel { min-width: 0; width: auto; max-width: 100%; }
  .trk-picker .csel-trigger {
    border: none;
    padding: 4px 22px 4px 0;
    height: auto; display: inline-block;
    width: auto; min-width: min(200px, 100%); max-width: 100%;
    font-size: 20px; font-weight: 600; color: var(--ink);
  }
  .trk-overall {
    flex-direction: column; align-items: stretch; flex-wrap: nowrap;
    gap: 8px;
  }
  .trk-overall-row { flex-direction: column; align-items: stretch; gap: 8px; }
  /* Desktop's flex:1 grows the bar along a ROW's main axis (width); inside
     this column-stacked row that same flex:1 instead targets the vertical
     main axis with a flex-basis of 0, collapsing the bar to zero height.
     Pin it back to a fixed-height, full-width block. */
  .trk-overall-bar { flex: none; width: 100%; min-width: 0; height: 8px; }
  .trk-overall-help { max-width: none; }
  .trk-insight-btn, #trkInsightBtn { width: 100%; }

  /* squares strip is the least essential piece — keep it, but let it scroll
     sideways rather than crush every phase grid illegibly narrow. The
     Completed/Remaining legend drops below the strip instead of racing it
     for width — same stacked treatment as the toolbar above. */
  .trk-overview {
    flex-direction: column; align-items: stretch;
    height: auto; gap: 10px;
  }
  /* renderOverview() (tracker-ui.js) derives the square grid's ROW COUNT from
     squaresEl.clientHeight — desktop keeps that height fixed and independent
     of the grid via .trk-overview's own height:118px + squares stretched to
     fill it (see that rule's comment: height must never depend on the grid
     it draws, or rows→gridHeight→clientHeight→rows feeds back and the strip
     shrinks a little on every re-render). .trk-overview above is height:auto
     here (needed so the legend can stack below it), so that stability has to
     come from .trk-squares itself instead — an explicit, content-independent
     height. 100px ≈ desktop's own stretched height, so row density matches. */
  .trk-squares { flex: none; height: 100px; overflow-x: auto; overflow-y: hidden; }
  /* Desktop's ellipsis assumes every phase segment is wide enough for its own
     name; several of the mobile-narrow ones aren't (Data/List Schedule are
     the shortest, only 2-3 squares wide) and clip to "D…"/"L.". Letting the
     label spill past its own segment's edge is safe here — it just bleeds
     into the empty space of whichever short/scrolled segment sits beside it,
     nothing else occupies that row. */
  .trk-squares .trk-sq-label { overflow: visible; text-overflow: clip; }
  .trk-legend { flex-direction: row; align-self: flex-start; gap: 16px; }
  .trk-legend .trk-sq-label { display: none; }   /* alignment spacer, moot once stacked */

  /* Sign-off gates: tracker-cards.js (renderCards) weaves each gate card
     inline right after the phase it follows — gate1 after Schematic, gate2
     after Development, gate3 after the last phase — instead of trailing all
     three in their own row. #trkGates is left empty in that mode; hide it so
     its own row doesn't leave a blank gap. Each gate card still spans the
     full grid row wherever it lands (3-col or 1-col) so it reads as a
     checkpoint, not a stray column. */
  #trkGates { display: none; }
  .trk-gate-card { grid-column: 1 / -1; }

  /* bigger touch targets for the per-item controls (≥ ~40px hit area) */
  .stepper { height: 30px; }
  .stepper .stp { width: 30px; }
  .stepper .stlabel { font-size: var(--text-xs); }
  .trk-scope input { width: 20px; height: 20px; min-width: 20px; min-height: 20px; }
  .trk-scope input:checked::after { left: 6px; top: 3px; width: 4px; height: 8px; }
  .trk-del { width: 30px; height: 30px; }
  .rev-btn { height: 28px; padding: 0 10px; }
  .trk-seg-btn { padding: 10px 10px; }

  /* ── number stepper (± controls) — bigger touch targets ────────────────── */
  .nstep-btn { width: 40px; }

  /* ── PROJECT list — two-row card per project ─────────────────────────────
     .pm-top/.pm-sub are display:contents on desktop (project-manager.css) so
     their children flatten into .pm-row's own 10-col grid there — here on
     mobile they become the real layout instead: .pm-top is the primary line
     (number, name, ring, chevron), .pm-sub is one muted subtitle line below
     it (location · dates · pace) instead of the desktop-only chip pills. */
  .pm-row {
    display: flex; flex-direction: column;
    /* align-items:stretch is essential, not cosmetic: the desktop base rule
       sets .pm-row{align-items:center} to vertically-centre its grid cells,
       but here the row is a COLUMN flex, so that inherited `center` centres on
       the cross (horizontal) axis instead — shrinking .pm-top/.pm-sub to their
       content and floating them to the middle of the row. Stretch makes each
       line fill the row's full width, so the number/name/subtitle sit hard
       left and the ring/chevron are pushed hard right (via .pm-name-cell's
       flex:1 below). Same layout-mode-flip trap as .hd{align-self:end}. */
    align-items: stretch; gap: 3px;
    position: relative; padding: 10px 16px 10px 30px;
  }
  .pm-top { display: flex; align-items: center; gap: 8px; padding-right: 50px; }
  .pm-top .pm-name-cell { flex: 1; min-width: 0; }
  /* Ring + chevron center against the FULL two-line card, not just .pm-top's
     own line — pulled out of .pm-top's flex flow (position:absolute removes
     them regardless of remaining .pm-top's DOM children) and centered against
     .pm-row itself (the nearest positioned ancestor) instead. .pm-top/.pm-sub
     both reserve 50px of right padding (ring 32px + 8px gap + chevron 10px)
     so their own text truncates before it would run under the icons — the
     ring especially has a transparent center, so overlapping text would show
     through it rather than just being cut off cleanly. */
  .pm-ring, .pm-chevron {
    position: absolute; top: 50%; transform: translateY(-50%);
  }
  .pm-ring { right: 26px; }
  .pm-chevron { right: 16px; }
  /* Plain inline flow (not flex) — .pm-loc/.pm-date are already inline spans
     and .chip is already inline-flex (base rule), so they read as one run of
     text and truncate as a single line, matching the mockup this was built
     from. A flex container here (tried first) put a hard box edge between
     each field, which ate the leading space in the "·" separator below.
     display:block is NOT the default here and must be explicit — .pm-top/
     .pm-sub are display:contents on desktop (project-manager.css) so their
     children flatten into .pm-row's grid, and that rule is unconditional
     (not desktop-scoped); .pm-top overrides it back below, .pm-sub needs the
     same or it silently keeps display:contents on mobile too, scattering
     pm-loc/pm-date/chip as direct flex items of the column-direction
     .pm-row — each landing on its own line instead of one subtitle line. */
  .pm-sub {
    display: block; min-width: 0; padding-right: 50px;
    font-size: var(--text-sm); color: var(--ink-faint);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .pm-sub .pm-loc, .pm-sub .pm-date { color: inherit; font-size: inherit; }
  /* Blank fields (location is optional) collapse instead of leaving a bare
     " · " — the ~ combinator only inserts a separator before an item that has
     an earlier NON-empty sibling, so it skips cleanly over any hidden one. */
  .pm-sub > :empty { display: none; }
  .pm-sub > :not(:empty) ~ :not(:empty)::before { content: " · "; }
  .pm-sub .chip {
    background: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit;
  }
  .pm-edit-icon { opacity: 1; }

  /* Reorder: press-and-hold anywhere on the row, no visible icon (see
     project-manager.js for the press/drag mechanics). .pm-handle stays in
     the DOM — Tab still reaches it and arrow keys still reorder, completely
     unchanged — just visually hidden until keyboard focus, the same
     hover/focus-reveal idiom .pm-edit-icon already used, swapped for
     :focus-visible since touch has no hover. */
  .pm-handle {
    position: absolute; left: 4px; top: 50%; transform: translateY(-50%);
    opacity: 0; z-index: 1;
  }
  .pm-handle:focus-visible { opacity: 1; }
  .pm-row-wrap { touch-action: pan-y; }
  .pm-row-wrap.pm-lifted {
    position: relative; z-index: 5;
    background: var(--panel-bg); box-shadow: 0 10px 24px rgba(0,0,0,.28);
    border-radius: var(--radius-md);
  }

  /* History drop-down: bounded and internally scrollable instead of pushing
     the whole page. A long-running project's history can run to dozens of
     snapshot columns — left to flow naturally, opening it would strand the
     row that triggered it far off-screen; a capped, scrollable panel keeps
     the row (and the rest of the list) reachable while it's open. */
  .pm-history { max-height: 50vh; overflow-y: auto; }
  /* Desktop indents the drop-down to clear the handle + number columns; on the
     two-line card there are no columns to clear, so reclaim the width. */
  .pm-history { padding-left: 16px; padding-right: 16px; }

  /* ── TO-DO list modal body: comfortable rows already; nothing structural ── */

  /* ── CONSTRUCTION VIEW: two cards side by side is a desktop luxury ─────
     Stack them, let the page scroll instead of each card scrolling its own
     body, and pin the row actions visible (touch has no hover). */
  .cx-cards { grid-template-columns: 1fr; gap: 14px; }
  .trk-construction { display: block; }
  .cx-card, .cx-body { min-height: 0; }
  .cx-body { overflow-y: visible; }
  .cx-issue .cx-actions, .cx-log-row .cx-actions { opacity: 1; }
  .cx-log-note, .cx-log-detail { margin-left: 0; }
  .trk-views { width: 100%; }
  .trk-view { flex: 1; }

  /* ── Single-project Gantt: the phase-name column costs a third of a phone's
     width, so it narrows and the bars keep the space. gantt.js reads this
     value back, so the chart re-fits itself with no JS breakpoint of its own. */
  .gt-chart { --gt-label-w: 68px; }
  .gt-legend { gap: 8px; }
}
