/* ============================================================
   THE PHONE SCALE — phone.css
   Loaded by index.html on EVERY product, and inert everywhere it
   does not apply: every rule is inside
   `@media (max-width: 900px) and (pointer: coarse)`.

   IT USED TO BE GATED ON `html.has-phone-nav`, WHICH WAS WRONG.
   That class means "this product armed the bottom bar", and the
   Nova Assistant session made the argument that settles it: their
   product has no rail and five gated routes, the chat's own
   sidebar IS the chrome, and arming a tab bar on a chat app purely
   to inherit a type scale is the tail wagging the dog. A scale is
   not a navigation. It belongs to the POINTER, which is what
   `(pointer: coarse)` asks about — so a phone browser gets it too,
   not only the three apps, and every product improves at once.

   The thing that does still belong to the bar — `--hn-clear`, what
   a bottom-anchored control has to clear — lives in homenav.css
   with the bar itself, and is 0px wherever the bar is not mounted.

   WHY A SCALE AND NOT A LIST OF FIXES
   A live sweep of all 40 Nova OS routes at 375pt
   (mobile/os/scripts/parity-sweep.mjs) measured interactive controls
   at 26, 27, 30, 33, 34, 36 and 38 pixels tall. Fixing them view by
   view would be forty edits and a fortieth-first the next time
   somebody adds a chip.

   Almost none of those controls set their own height. They inherit
   --ctl-h-sm / --ctl-h-md / --ctl-h-lg from tokens.css, which are
   designed for a mouse. So the lever is the TOKENS: raise them here
   and every button, chip, field and select in the product grows at
   once, in the proportions its designer chose. Same for type.

   This is the same argument the native bridge makes about 200 fetch
   sites: the fix belongs at the one place the value is born.

   THE FLOOR IS NOT THE TARGET. Apple's 44×44 is a minimum, and the
   brief was "bigger and way easier for mobile", so the default button
   lands at 48 rather than 44 and the primary at 52. A thumb is about
   45–57px of contact; designing to the legal minimum means every tap
   is a near miss.
   ============================================================ */

/* WHY SO MANY SELECTORS START WITH `:root`.

   They used to start with `html.has-phone-nav`, which was carrying TWO jobs:
   scoping (only products with the bottom bar) and specificity (0,2,0). Removing
   it fixed the scoping — a scale belongs to the pointer, not to a nav bar — and
   silently dropped the specificity to (0,1,0), which TIES with a view's own
   rule for the same class. A view's CSS is injected into a <style> at runtime,
   after this file's <link>, so a tie is a loss: `.cal-topbar` here lost to
   `.cal-topbar` in views/calendar.js and the phone treatment simply stopped
   applying.

   Nothing looked different in a screenshot of a CLOSED panel and no geometric
   measure moved, which is why it survived until the Nova Home session found the
   same class of regression by asking what was actually under a fingertip.

   `:root` is a pseudo-class, so it restores the (0,2,0) without restoring the
   scoping mistake: it matches <html> on every product, on the web and in the
   apps. Rules already scoped to a view (`.v-cal …`) or an id (`#view …`) do not
   need it and do not have it. */

@media (max-width: 900px) and (pointer: coarse) {
  /* `:root`, NOT `html`, AND THE DIFFERENCE IS THE WHOLE PHONE SCALE.
     This block spent its life doing nothing. `html` is (0,0,1); tokens.css
     declares every one of these on `:root`, which is (0,1,0), and specificity
     beats source order — so tokens.css won even though phone.css is linked
     seven stylesheets later. 14 of the 17 custom properties here were dead:
     the entire type scale (--fs-display through --fs-2xs), all three control
     heights, --sp-5, and BOTH tap-pads, which are what grow a touch target to
     44pt. Measured on the live product at 375pt with the media query matching,
     phone.css linked and .has-phone-nav on the root: --fs-body resolved to
     15px instead of 16px and --fs-2xs to 12px instead of 13px.

     Same family as the trap `phone-specificity` was written for — a phone rule
     that is quietly outranked — but that rule compares phone.css against a
     VIEW's own CSS, and this one loses to another stylesheet, which it did not
     look at. It does now. */
  :root {
    /* ---- CONTROL HEIGHTS ----
       32/38/44 on a desktop become 42/48/54. The ratio is kept so a
       dense toolbar still reads as denser than a primary CTA; it is
       the whole ladder that moves, not one rung. */
    --ctl-h-sm: 42px;
    --ctl-h-md: 48px;
    --ctl-h-lg: 54px;
    /* Fields get their own rung because ::before cannot pad a replaced
       element (tokens.css says so) — an input's real height is the only
       lever it has. */
    --ctl-h-field: 50px;
    --tap-pad: 48px;
    --tap-pad-sm: 44px;

    /* ---- TYPE ----
       Read at arm's length, one-handed, often outdoors. Body goes to
       16px, which is also the number below which iOS zooms the page on
       focus — see the input rule further down. The 12px floor becomes
       13px: on a phone 12px is a squint, not a caption. */
    --fs-display: clamp(2rem, 8vw, 2.4rem);
    --fs-h1: 1.6rem;      /* 25.6px */
    --fs-h2: 1.3rem;      /* 20.8px */
    --fs-h3: 1.125rem;    /* 18px   */
    --fs-body: 1rem;      /* 16px   */
    --fs-sm: 0.9375rem;   /* 15px   */
    --fs-xs: 0.875rem;    /* 14px   */
    --fs-2xs: 0.8125rem;  /* 13px — the phone floor */

    /* ---- ROOM ----
       Denser is not calmer on a small screen; it is just harder to hit.
       Every gap in the product is a multiple of these. */
    --sp-3: 14px;
    --sp-4: 18px;
    --sp-5: 24px;
  }

  /* ---- ICONS ----
     16px glyphs beside 16px type were balanced on a desktop. At phone
     scale they read as decoration on a label rather than as the thing
     the label names. */
  #view .btn svg,
  #view .btn .ico { width: 19px; height: 19px; }
  #view .icon-btn svg,
  #view .btn-icon svg { width: 21px; height: 21px; }

  /* ---- THE SAFETY NET ----
     For controls that never took the tokens — a view-local chip, a
     hand-rolled tab, a bare <button>. min-height rather than height, so
     nothing that is already taller is squashed, and it is deliberately
     NOT applied to the whole document: the topbar and the rail are
     fixed-height lockups whose height is not ours to spend (Findings
     F7 — a min-height there is what broke the brand block). */
  #view button,
  #view [role="button"],
  #view a.btn,
  #view label.btn,
  #view select {
    min-height: 44px;
  }
  /* Both axes. A control can clear 44 tall and still be a 24pt-wide sliver —
     `.fnl-lane-collapse` measured 24x44, `.cal-new` 40x44 — and a thumb misses
     a narrow target as reliably as a short one. */
  #view .btn,
  #view button,
  #view [role="button"],
  #view .icon-btn,
  #view .btn-icon { min-width: 44px; }

  /* A checkbox is the smallest thing anyone is ever asked to hit, and it
     is usually the one that matters (select this row, mark this done). */
  #view input[type="checkbox"],
  #view input[type="radio"] { width: 24px; height: 24px; }

  /* ---- FIELDS ----
     iOS zooms the whole page when a field under 16px takes focus, then
     leaves it zoomed. It reads as the app breaking. 16px is not a
     preference here, it is the threshold — see --fs-ios-nozoom in tokens.css. */
  #view input,
  #view textarea,
  #view select,
  :root .modal input,
  :root .modal textarea,
  :root .drawer input,
  /* Both halves of this arrived at once from two sessions, and each had
     something the other did not. `max(…, var(--fs-body))` never renders SMALLER
     than the product's body type; `--fs-ios-nozoom` names what the 16 actually
     is, so the next person does not read it as a type choice and "tidy" it.
     Together: never below Safari's threshold, never below body type, and the
     threshold is a token the ship gate can see. */
  :root .drawer textarea { font-size: max(var(--fs-ios-nozoom), var(--fs-body)); }
  #view .input,
  #view input:not([type="checkbox"]):not([type="radio"]) {
    min-height: var(--ctl-h-field);
  }

  /* ---- NOTHING SITS UNDER THE KEYBOARD ----
     assets/js/native/shell.js publishes the live keyboard height as
     --kb-height inside the app (0px in a browser, and 0px whenever the
     keyboard is down). Any layer that owns the bottom edge adds it. */
  :root .modal,
  :root .drawer,
  :root .me-sheet { padding-bottom: var(--kb-height, 0px); }

  /* ---- SHEETS, NOT CENTRED DIALOGS ----
     A centred desktop dialog on a phone is a small box in the middle of
     a dark screen with its actions out of thumb reach. The same modal
     becomes a bottom sheet: full width, rounded at the top, actions at
     the bottom where the hand already is. `views/wins.js` shipped this
     for one modal; it belongs to all of them. */
  :root .modal-backdrop .modal {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    width: 100%; max-width: none; margin: 0;
    /* 92dvh measured from the BOTTOM puts the top of a tall sheet at 8% of the
       screen — which on a phone is underneath the clock and the Dynamic Island.
       Photographed on an iPhone 17e: "Add transaction" opened with its title
       level with the wifi and battery icons. A sheet may use the whole screen
       EXCEPT the status bar, so the inset comes out of its height. */
    max-height: calc(92dvh - var(--safe-top)); overflow-y: auto;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding-bottom: calc(env(safe-area-inset-bottom) + var(--kb-height, 0px) + var(--sp-4));
  }
  /* The grab handle that says "this pulls down" — the affordance a
     bottom sheet needs and a centred dialog never had. */
  :root .modal-backdrop .modal::before {
    content: ""; display: block; width: 38px; height: 4px; margin: 2px auto 12px;
    border-radius: var(--r-pill); background: var(--border-strong); flex: none;
  }

  /* ---- A FULL-SCREEN DRAWER STARTS AT THE TOP OF THE SCREEN ----
     `.drawer-scrim` is `position: fixed; inset: 0` and components.css makes
     `.drawer` full-width under 640px, so on a phone a drawer covers the whole
     screen — including the status bar. Its header pads from y=0, which puts the
     title level with the clock and the battery.

     Photographed on an iPhone 17e: budget's "Add transaction" — which is an
     openDrawer(), not an openModal(), so the sheet rule above never touched it
     — opened with its title overlapping the wifi and battery icons. The CRM
     record drawer is the same component, and NOVA-MOBILE.md names that one
     specifically.

     Full-screen is the right shape for a long form, so the drawer keeps it and
     only its content moves down. Same fix as the topbar in #1015: spend
     --safe-top, do not shrink the surface. */
  /* Not keyed to has-phone-nav, for the same reason the scale is not: a status
     bar is a property of the DEVICE, not of whether a product armed a bottom
     bar. Nova Assistant has no bar and its drawers sit under the clock exactly
     as everyone else's did. */
  :root .drawer-head { padding-top: calc(var(--sp-5) + var(--safe-top)); }

  /* The calendar rules below carried html.has-phone-nav too. Harmless on the
     three apps — all of them arm the bar — but EMPIRE has a calendar and no
     bar, so on a phone it kept the desktop toolbar: a month strip and six
     controls on one 375pt line. Same argument as everything else in this file.
     A calendar toolbar is not a navigation decision. */

  /* ---- A CLOSED PANEL THAT STILL TAKES TAPS ----
     `.cal-detail-panel` is hidden by `transform: translateX(100%)`, and a
     transform moves paint, not hit-testing volume — the element is still there,
     still `pointer-events: auto`, still z-index 30. At 390pt the panel is
     344 wide (88vw) and translates by 343, so 47 pixels of a CLOSED composer
     sit down the right edge of the calendar. The `+` New button is at x=356.
     It was untappable, and it measured 44x44 the whole time.

     components.css already has a section for exactly this shape — CLICK-THIEVES,
     written after `.tk-grip` at `opacity: 0` was measured stealing a task
     checkbox's clicks. Same fix: if it is not open, it is not in the way.

     This is not phone-only in principle, but the geometry that makes it bite is
     (a desktop has room for the panel to sit fully outside the viewport), so it
     lives here rather than changing the desktop's hit-testing. */
  .v-cal .cal-detail-panel:not(.open) { pointer-events: none; }

  /* ---- AND THE CALENDAR COMPOSER IS A DESKTOP DRAWER ----
     The rule above catches `.modal`. The most-used creation surface in the
     product is not one: `.cal-detail-panel` is `position: absolute; right: 0;
     bottom: 0; width: 360px; max-width: 88vw`, sliding in from the RIGHT. On a
     phone that is a desktop side-drawer squeezed to 88% of the screen, with a
     strip of calendar still showing down the left edge — photographed on an
     iPhone 17e, which is what put it here.

     Two things were wrong with it and only one of them is visible.

     The visible one is the shape. The plan is explicit: sheets, not modals,
     because a bottom sheet puts its actions where the hand already is.

     The invisible one is worse. openComposer() focuses the title field 60ms
     after it opens, so the keyboard comes up EVERY time — and this panel is not
     `.modal`, `.drawer` or `.me-sheet`, so it was in none of the lists that add
     `--kb-height`. Its date, duration and save controls sat under the keyboard
     with nothing to scroll them into view.

     `position: fixed`, not absolute: as a sheet it belongs to the screen rather
     than to `.cal-main`, and it has to sit above the bottom bar (z-index 70)
     the way a sheet does. `--z-modal` is 100. */
  /* `.v-cal` prefix, and it is load-bearing rather than tidy. This rule was
     `html.has-phone-nav .cal-detail-panel` (0,2,0) and beat views/calendar.js's
     own `.cal-detail-panel` (0,1,0). When phone.css was decoupled from the nav
     class the prefix went, both selectors became (0,1,0), and the VIEW won on
     source order — its CSS is injected into a <style> at runtime, after this
     link. The sheet silently reverted to the desktop side-drawer and nobody
     would have known: it is off-screen either way, and only the tap-stealing
     above made it visible. */
  .v-cal .cal-detail-panel {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    width: 100%; max-width: none;
    max-height: calc(88dvh - var(--safe-top)); overflow-y: auto;
    border-left: none; border-top: 1px solid var(--border);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: 0 -8px 32px -12px rgba(0, 0, 0, 0.5);
    transform: translateY(100%);
    z-index: var(--z-modal);
    padding-bottom: calc(env(safe-area-inset-bottom) + var(--kb-height, 0px));
  }
  .v-cal .cal-detail-panel.open { transform: translateY(0); }
  /* The same grab handle the modals get, so it reads as a sheet rather than as
     a panel that happens to be at the bottom. */
  .v-cal .cal-detail-panel.open::before {
    content: ""; display: block; width: 38px; height: 4px; margin: 8px auto 4px;
    border-radius: var(--r-pill); background: var(--border-strong); flex: none;
  }

  /* ---- THE PAGE HEADER IS A DESKTOP HEADER ----
     Measured on Pipelines at 375pt: the eyebrow, a 38px display title, the
     subtitle and two full-size buttons ate about 60% of the screen before a
     single pipeline card. On a desktop that header is a generous entrance; on a
     phone it is a wall you scroll past every single time.

     Nothing is deleted — the eyebrow is Nova explaining what the page is, and
     PHILOSOPHY.md is explicit that plain language is not decoration. It is
     compressed: the title steps down from --fs-display to --fs-h1 (which is
     itself bigger here than on a desktop), the vertical rhythm halves, and the
     two header buttons share one row instead of stacking. That is roughly 120px
     of screen back, on every view in the product. */
  #view .view-head {
    gap: var(--sp-3); margin-bottom: var(--sp-4);
  }
  #view .view-head h1 {
    font-size: var(--fs-h1); line-height: 1.15;
  }
  #view .view-head .eyebrow { margin-bottom: 4px; }
  #view .view-head .view-sub {
    margin-top: 5px; font-size: var(--fs-sm); max-width: none;
  }
  #view .view-body { margin-top: var(--sp-4); }
  /* Header actions share a row when they fit and take a row each when they do
     not. `flex: 1 1 auto` — NOT `1 1 0`: a zero basis forces both into half the
     screen whatever they say, and "Import from a spreadsheet" then hangs its
     icon off the left edge and eats its own last word. With an auto basis the
     button will not shrink below its own text, so it wraps to its own line
     instead of truncating, and short pairs still sit side by side. */
  #view .view-head .view-actions {
    width: 100%; gap: var(--sp-2);
  }
  #view .view-head .view-actions > .btn { flex: 1 1 auto; }

  /* ---- WHERE GROWING THE PAINT BREAKS THE ROW, GROW ONLY THE TARGET ----

     The 44px minimum above is right for the great majority of controls, which
     have no height of their own and simply get taller. It is wrong for a
     control whose height somebody CHOSE and whose row was laid out around that
     number — there, min-height moves the paint, the row overflows, and the
     control lands on top of its neighbour. The 44pt floor then causes the exact
     failure it exists to prevent: a target a finger cannot reach.

     tokens.css has said how to do this properly the whole time, and the Nova
     Home session said it again: leave the paint alone and grow the TARGET with
     an absolutely-positioned pad. It is not applied globally because ::before is
     already spoken for on plenty of controls (a knob, a tooltip, an icon), so a
     blanket pad would clobber them — it goes where the pseudo-element is free
     and the paint genuinely must not move.

     Applied here to the pipeline header only. The chat start screen looked
     like the same shape and is not — see MOBILE-PARITY-NOVA-OS.md; a rule for
     it was written, measured, found to change nothing, and removed rather than
     left in place describing an effect it did not have. */

  /* The pipeline header is the other shape: a row of stage tabs plus an "Add
     pipeline" button carrying `flex: 0 0 auto`. Nothing there could shrink, so
     the wider controls simply overlapped — the same lesson as .view-actions
     below, which is that flex-wrap does nothing to an item that refuses to
     shrink. Allowing both is what makes the wrap happen. */
  :root #view .crm-tabsrow { flex-wrap: wrap; row-gap: 4px; }
  :root #view .crm-tabsrow > * { min-width: 0; }
  :root #view .crm-addpipe-btn { flex: 0 1 auto; }

  /* ---- ACTION ROWS WRAP, AND THEY SHRINK ----
     `.view-head .view-actions` already sets flex-wrap:wrap, and it was not
     enough: it also sets flex-shrink:0, and a flex item that refuses to shrink
     takes its max-content width no matter what wrapping it allows. Measured at
     401px inside a 377pt screen on Pipelines and Automations — the last button
     in the row is simply off the edge, and since the parent clips, it is gone
     rather than scrolled away. Allowing the shrink is what makes the wrap
     happen at all. */
  #view .view-actions,
  #view .view-head .view-actions {
    flex-wrap: wrap; flex: 0 1 auto; min-width: 0; max-width: 100%;
    justify-content: flex-start;
  }

  /* ---- AN ERROR MESSAGE LAID OUT SIDEWAYS ----
     `.pl-empty` is `flex-direction: row` with 40px of padding — three items
     across, which is fine on a desktop and is about 100px each on a 390pt
     phone. Photographed by failing /api/plans on purpose: the heading rendered
     as "Couldn't / load / Plans." down a narrow strip, the explanation in a
     second strip beside it, and the retry button in a third.

     This is the state a person sees at the worst moment, and it was the least
     readable screen in the product. NOVA-MOBILE.md asks that an error "says
     what happened and offers the retry" — it did both, sideways.

     It is the EMPTY state too, so this fixes the calm case as well as the bad
     one. Stacked, centred, and with the padding a phone can afford. */
  /* `:root` prefix, not decoration: views/plans.js scopes its own rule as
     `.v-plans .pl-empty`, which is (0,2,0) — the same as this without the
     prefix — and a view's CSS is injected into a <style> at runtime, AFTER this
     link, so a tie is a loss. The first version of this rule did nothing at all
     and the screenshot was identical. Same trap that killed 25 rules in #1096. */
  :root .v-plans .pl-empty {
    flex-direction: column; text-align: center;
    padding: var(--sp-6) var(--sp-5); gap: var(--sp-3);
  }
  /* AND GIVE THE BUTTONS BACK THEIR WORDS.

     views/plans.js hides EVERY `.pl-btn` label under 960px —
     `.v-plans .pl-btn span { display: none }` — which is a reasonable way to
     keep a dense toolbar from wrapping, and a bad thing to do to the three
     buttons that are not in a toolbar:

       · the Retry in the error state, which is the whole recovery path
       · "Create the first page" and "Import a document" in the Docs empty
         state (`.pl-list-empty`), which are the only two things a new person
         can do on that screen — it rendered as "No pages yet." followed by a
         plus and an up-arrow, and nothing to say what either does

     All three became bare glyphs. There is no hover on a phone, so an
     unlabelled icon is not a control with a hidden name — it is a control with
     no name, which the mobile plan lists as its own defect class. The toolbar
     keeps its compaction; a button standing alone in an empty or error block
     keeps its label. */
  :root .v-plans .pl-empty .pl-btn span,
  :root .v-plans .pl-list-empty .pl-btn span { display: inline; }

  /* ---- A HEADER THAT CANNOT WRAP CLIPS AT LARGE TEXT ----
     Found the moment Dynamic Type started working (theme-boot.js). At the
     largest accessibility size the Tasks habits header — title, "0 of 0",
     "Show all", "Add habit" — is wider than the card, and `.ht-card-head` is a
     flex row with no wrap: "Add habit" ran off the right edge, "Show all"
     printed on top of it, and "0 of 0" broke onto three lines.

     This is the clause NOVA-MOBILE.md asks for — "nothing clips at the largest
     accessibility size" — and Nova Home passed it for three ticks only because
     text never grew, so nothing could clip. Making the setting work is what
     made the bug visible.

     Wrapping is conditional by nature: a flex row that MAY wrap only does so
     when it must, so nothing moves at ordinary text sizes. The count is held on
     one line because a tabular "0 of 0" broken over three is not a number any
     more. Specificity matches the view's own `.v-htasks .ht-card-head`. */
  :root .v-htasks .ht-card-head { flex-wrap: wrap; row-gap: 8px; }
  :root .v-htasks .ht-card-count { white-space: nowrap; }

  /* ---- STRIPS SCROLL, THEY DO NOT CLIP ----
     A row of tabs or filter chips wider than the screen inside a
     non-scrolling parent is content that is GONE, not content that is
     off to the side. Measured on five routes. */
  #view .tabs,
  #view .tabstrip,
  #view .seg,
  #view .chips,
  #view .filter-row {
    overflow-x: auto; flex-wrap: nowrap;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  #view .tabs::-webkit-scrollbar,
  #view .tabstrip::-webkit-scrollbar,
  #view .seg::-webkit-scrollbar,
  #view .chips::-webkit-scrollbar,
  #view .filter-row::-webkit-scrollbar { display: none; }

  /* ---- THE CALENDAR TOOLBAR IS A DESKTOP TOOLBAR ----
     Nine controls in two flex halves: sidebar toggle, the date, Today, prev,
     next, a timezone picker, Select, Plan day, Tasks, New, and a Month/Week/Day
     switch. On a desktop they sit on one line. calendar.js already had a
     `@media (max-width:900px)` rule that let both halves WRAP, and wrapping is
     what produced the damage — measured on an iPhone 17, the toolbar became
     FIVE stacked rows and, with the Google banner under it, ate ~40% of the
     screen. Two hours of the day were visible.

     Wrapping is the wrong answer for a strip of controls on a phone; scrolling
     is the house answer (see `.segmented` in components.css and the strips
     above). So each half takes a full row and does not wrap, and the actions
     half scrolls sideways. That is TWO rows instead of five, and — this is the
     point, and why nothing here is `display: none` — every control is still
     reachable. The parity law is that no feature may be desktop-only, and a
     control hidden to save space is a feature deleted on a phone. */
  :root .cal-topbar { flex-wrap: wrap; gap: 6px; padding: 8px 10px; }
  :root .cal-topbar-l,
  :root .cal-topbar-r {
    flex: 1 0 100%; min-width: 0; flex-wrap: nowrap;
  }
  :root .cal-topbar-l { overflow: hidden; }
  /* Only the date may shrink. Everything beside it is a control, and a control
     that shrinks below its text wraps mid-word — "Today" rendered as "Toda /
     y" on the first pass, which is a squeezed label, not a button. */
  :root .cal-topbar-l > *:not(.cal-period) {
    flex-shrink: 0; white-space: nowrap;
  }
  /* The date is the one thing that may shrink: everything beside it is a
     control, and a control that ellipsises is a control you cannot read. */
  :root .cal-period {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  :root .cal-topbar-r {
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  :root .cal-topbar-r::-webkit-scrollbar { display: none; }
  :root .cal-topbar-r > * { flex-shrink: 0; }
  /* WHAT IS VISIBLE WITHOUT SCROLLING IS A DECISION, SO MAKE IT.
     Source order is a desktop's order, and it put the timezone picker and
     three tool buttons in front of the two controls people actually reach for.
     On the first pass the Month/Week/Day switch was entirely off-screen — still
     reachable by swiping, which satisfies the parity law, but invisible, which
     satisfies nobody. The view switch and New lead; the timezone, Select, Plan
     day and Tasks scroll. */
  :root .cal-topbar-r .cal-viewseg { order: -2; }
  :root .cal-topbar-r .cal-new { order: -1; }
  /* The connect banner is a third row on a screen that has none to give. */
  :root .cal-banner { padding: 7px 10px; gap: 7px; line-height: 1.3; }

  /* ---- MOMENTUM ----
     The view is the scroll container; without this it stops dead
     instead of gliding, which is the single most "this is a website in
     a box" feeling there is. */
  #view { -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain; }

  /* ---- AN EMPTY STATE IS A COLUMN ON A PHONE ----
     The canonical .empty block is a ROW: a 36px hero glyph with the text beside
     it. On a 375pt screen that leaves the copy 193px wide, so the one sentence
     explaining what the screen is for wraps to four cramped lines next to an
     icon. Stacked, the same sentence gets 291px and three.

     Measured before changing it, on four empty screens: projects and ideas are
     ALREADY column, and this rule is a no-op on both. Only kpis was still a
     row — so this is the odd one out being brought into line with the product,
     not a new idea about empty states. Same concept, same UI.
     :root on the second rule because `.empty .empty-text` TIES with
     `.v-settings .empty-text`, and a view's CSS is injected into a <style> tag
     at runtime, so on a tie the view wins and this rule silently loses. The
     ship gate caught it. */
  .empty { flex-direction: column; text-align: center; padding: var(--sp-8) var(--sp-5); }
  :root .empty .empty-text { align-items: center; }

  /* ---- A ONE-JOB SCREEN CENTRES ITS ONE JOB ----
     Fuel before setup is a heading, three tabs, one card asking for height and
     weight, and then nothing: the card sat at y=220 with 193px of dead screen
     under it on an SE, 458px on a Pro Max, because .v-fuel is display:block and
     content simply stacks from the top.

     Flex column and an auto block-margin on the card's wrapper puts it in the
     middle of what is left, which also brings its button down into reach.

       iPhone SE       gap below the card 193px -> 131, button 51% -> 61%
       15 Pro Max      gap below the card 458px -> 263, button 37% -> 58%

     Gated on .v-fuel-setup, which only the not-ready state renders. The
     POPULATED Fuel screen must keep filling from the top, and an earlier
     version of this rule that keyed on .v-fuel-card alone would have centred
     that too.

     THE CARD FILLS RATHER THAN FLOATS, which is the Nova OS session's finding
     on their own dashboards screen and it applies here unchanged: the space
     under a card is usually being DISTRIBUTED, not missing. My first version
     centred the card with an auto block-margin and left 131px under it on an
     SE and 263px on a Pro Max — the slack simply moved from below the card to
     both sides of it. Letting the card take the room and centring its contents
     inside collapses that to 68px on both, and the button does not move,
     because the content is still centred:

       iPhone SE     card 190px -> 315, clear of the nav 131px -> 68, button 61%
       15 Pro Max    card 190px -> 580, clear of the nav 263px -> 68, button 58%

     Measured in WebKit against the deployed page, with the class rehearsed onto
     the live card first so the selector under test is the one that ships.
     Judged by eye on the SE; the Pro Max is the same composition with more
     padding, and its 580px card is reported here rather than photographed. */
  :root .v-fuel:has(.v-fuel-setup) { display: flex; flex-direction: column; min-height: 100%; }
  :root .v-fuel:has(.v-fuel-setup) > div:has(> .v-fuel-setup) { flex: 1 1 auto; display: flex; }
  :root .v-fuel-setup { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }

  /* ---- AN EMPTY SCREEN CENTRES ITS EMPTY STATE ----
     Tracking with nothing tracked put its whole empty block — glyph, title,
     the sentence explaining the screen, and the New tracker button — up under
     the heading, with 172px of nothing below it on an SE and 458px on a Pro
     Max, where the button sat 35% down the screen.

     .v-trk is ALREADY a flex column; it simply was not filling, so the space
     was being distributed above the fold rather than missing. Same finding as
     the Nova OS session's dashboards card and the Fuel card above: let the
     block take the room and centre what is inside it.

       iPhone SE     gap to the nav 172px -> 40, button 52% -> 62%
       15 Pro Max    gap to the nav 458px -> 40, button 35% -> 57%

     Gated on :has(> .empty), so a Tracking screen WITH trackers still fills
     from the top. Measured in WebKit against the deployed page, and looked at
     on the SE. */
  :root .v-trk:has(> .empty) { min-height: 100%; }
  :root .v-trk > .empty { flex: 1 1 auto; justify-content: center; }

  /* ---- A TOOLBAR OF DISTINCT ACTIONS WRAPS; IT DOES NOT SCROLL ----
     This overrides the strips-scroll rule further up THIS file, deliberately.
     That rule is right for a long row of homogeneous chips, where swiping is
     the obvious gesture and a half-visible chip at the edge advertises the
     rest. It is wrong for a toolbar of distinct one-off actions, where nothing
     signals that more exists and the only clue on the calendar was the word
     "My time (" cut off mid-bracket.

     Measured on the deployed page: .cal-topbar-r scrolled to 599px inside a
     355px box, so 244px of it was parked off the right edge of an iPhone SE
     and 189px off a 15 Pro Max. Four working controls were unreachable without
     discovering a scroll nobody advertises — the timezone, a Select, Tasks
     waiting, and PLAN DAY, which is one of this product's headline features.

     Wrapping costs 50px of chrome, 119px to 169px, on both sizes. Four hidden
     features for 50px is a trade worth making.

       iPhone SE     244px off-screen -> 0, 4 controls hidden -> none
       15 Pro Max    189px off-screen -> 0, 4 controls hidden -> none */
  :root .v-cal .cal-topbar-r { flex-wrap: wrap; overflow-x: visible; row-gap: 6px; }

  /* ---- NO HOVER-ONLY AFFORDANCES ----
     There is no hover on a phone. A control that only appears on
     :hover does not exist here, so anything the product reveals that
     way is shown outright. */
  #view .row-actions,
  #view .hover-actions,
  #view .show-on-hover { opacity: 1 !important; visibility: visible !important; }
}

/* REDUCE MOTION IS ALREADY HANDLED, GLOBALLY.

   There was a copy of the standard clamp here. motion.css already carries the
   same rule at `@media (prefers-reduced-motion: reduce)` with no width or
   pointer scoping, so it covers phones too — and it does more than the copy did
   (animation-delay, iteration-count, scroll-behavior). Two clamps is one more
   than can be tuned in one place, which is the whole reason the lint rule that
   flagged it exists. */

/* ============================================================
   TOUCH DRAG — assets/js/touch-drag.js carries a clone of the
   dragged element under the finger. These two rules are the only
   thing it needs from CSS, and they are here rather than inline
   so the look matches the SortableJS ghost (assets/js/dnd.js).
   ============================================================ */
html.touch-dragging { -webkit-user-select: none; user-select: none; }
/* The original stays in place and dims, so the row you lifted is still legible
   underneath — the same behaviour as the house .dnd-ghost. */
html.touch-dragging [draggable="true"]:not(.touch-drag-clone) { transition: opacity .12s ease; }
:root .touch-drag-clone { will-change: transform, left, top; }

/* ============================================================
   A SCREEN SHOULD NOT SAY ITS OWN NAME TWICE.

   homenav.js marks `.view-head` with `.vh-dupe` when the view's <h1> is an
   EXACT match for the title already in the top bar. On Nova OS at 375pt that is
   nine of ten routes, and `.view-head` averages 199pt — a quarter of the screen
   spent before the first control, restating the nav bar.

   It is also most of the reach problem: 27 routes put their primary action
   35-44% down the screen, and a good part of that is the duplicated title
   sitting above it. This moves those buttons up without touching one of them.

   Scoped to `:root.product-nova-os` on purpose. The marker class is set on all
   products; only this product acts on it, because the Nova Assistant and Nova
   Home sessions have not each looked yet and a shared answer adopted before
   three people have looked is how the phone scale ended up with two copies.
   `:root` rather than `html` for the reason in phone-tokens-outranked.

   The description stays. It is the only line on the screen that says anything
   the nav bar does not.
   ============================================================ */
@media (max-width: 900px) and (pointer: coarse) {
  :root.product-nova-os .view-head.vh-dupe h1,
  :root.product-nova-os .view-head.vh-dupe .view-title,
  :root[data-product="nova-assistant"] .view-head.vh-dupe h1,
  :root[data-product="nova-assistant"] .view-head.vh-dupe .view-title { display: none; }
  :root.product-nova-os .view-head.vh-dupe .eyebrow,
  :root[data-product="nova-assistant"] .view-head.vh-dupe .eyebrow { display: none; }
  :root.product-nova-os .view-head.vh-dupe .view-sub,
  :root[data-product="nova-assistant"] .view-head.vh-dupe .view-sub { margin-top: 0; }
}

/* NOVA ASSISTANT OPTS IN — the second product to look, which is what the
   staged rollout above was waiting for. This product has five routable screens
   and `email-prefs` is the worst of them: the title appears THREE times (top
   bar, eyebrow + <h1>, then again on the card) with eleven lines of prose above
   the single switch on the page.

   `data-product` rather than a class, because `product-nova-os` is the SKIN
   switch and names one product by construction — see theme.js. Nova Home is
   still untouched; that session decides for itself.

   THE MARKER IS SAFE HERE ONLY BECAUSE IT CHECKS VISIBILITY. This product hides
   the top bar outright on the chat route (body.chat-immersive), so a pure text
   match would have hidden the <h1> while nothing on screen said the name at
   all. view-head-dedupe.js asks whether the crumb is actually VISIBLE, not just
   whether it exists. */

/* ============================================================
   THE EMPTY PORTFOLIO SCREEN WAS A CARD AT THE TOP AND A VOID.

   `#/dashboards` and `#/finances` are the same view (v-fin). With no companies
   yet, overviewHTML() returns the company GRID holding a single "Add company"
   card — correct on a desktop, where that card is the last tile in a row of
   them. On a 375x629 phone it renders a header ending at 190px, a card ending
   at 313px, and 316px of nothing under it: half the screen, and the one thing
   to do sits at 35% — the top third, where a thumb cannot reach.

   The card already carries both halves of a good empty state: it says what this
   is for ("Create a new portfolio company with its own dashboards") AND it is
   the button. Nothing needs rewriting — it needs to be where a thumb is. So the
   grid becomes a column that reaches down the screen and puts its one card at
   the bottom of that space.

   `:has()` rather than a class, because the empty case is a fact about the DOM
   — one add-card and nothing else — and adding a marker class would mean the JS
   and the CSS could disagree about which case this is. Supported in WebKit,
   which is the engine that renders the app.

   Only when it is ALONE. With one real company plus the add card, the grid is a
   grid again and this rule must not fire. */
@media (max-width: 900px) and (pointer: coarse) {
  :root.product-nova-os .v-fin-cc-grid:has(> .v-fin-cc-add:only-child) {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    /* 52dvh, not 58. At 58 the card's bottom landed 8px UNDER the phone nav bar
       — measured, not guessed — which is the exact defect mobile-parity-sweep
       calls `hidden`: a control the bar sits on top of. A fix for one rule that
       breaks another is not a fix, and this one would have been reported as an
       improvement by the idiom audit while the parity sweep failed it. */
    /* 40dvh, and the card FILLS it. The band of nothing on this screen was mine:
       52dvh made this container 439px tall and `justify-content: flex-end` put
       every one of the 334 spare pixels ABOVE the card. I then spent three runs
       trying to close it with min-height on the CARD — 34dvh, then 42dvh, both
       below the parent's floor, so they were parsed, matched, applied and could
       change nothing. The Nova Home session measured it and named it: the space
       was being distributed, not missing, and the lever is the parent's height
       downward rather than the child's upward.

       So the container is shorter and the card grows into what is left. The
       gate is still `:has(> .v-fin-cc-add:only-child)` — the EMPTY state only,
       never the populated dashboard, which needs its own floor.

       52dvh, NOT 40. Shortening the container to 40 closed the gap above the
       card and opened one below it — dead space went 15% to 27% and the picture
       showed slack between the card and the nav bar. The height was never the
       problem; `flex: 1` on the card was the missing half. At 52dvh with the
       card filling, it runs from the copy down to 30px clear of the bar, with
       nothing spare at either end. */
    min-height: 52dvh;
  }
  /* One rule for this card, not two. There were two blocks with an IDENTICAL
     selector both setting min-height — the second never took effect and I spent
     three runs asking why. Whatever the cascade was doing, two rules fighting
     over one property in one file is the bug, not the thing to debug. */
  :root.product-nova-os .v-fin-cc-grid:has(> .v-fin-cc-add:only-child) .v-fin-cc-add {
    width: 100%;
    flex: 1 1 auto;          /* fill the container rather than leave slack above */
  }
  /* AND THE SAME ACTION IS NOT OFFERED TWICE. While the portfolio is empty this
     screen shows "Add company" as a ghost button in the header at 23% AND as
     the card — the same thing, in two places, one of them out of reach. The
     header copy is right on a desktop, where a toolbar is somewhere the eye
     already is; on a phone the empty state IS the screen. Retiring it leaves
     one obvious action instead of two of the same one.

     The shape is the Nova Assistant session's, from their `knowledge` screen:
     the fix for an out-of-reach action is usually not to move the button but to
     put the action where the instruction is, and then stop saying it twice.

     Only while empty. With a company on the screen the header button is the
     only way to add another, and hiding it would strand the user. */
  :root.product-nova-os .v-fin:has(.v-fin-cc-grid > .v-fin-cc-add:only-child) #finAddCo { display: none; }

  /* THE HIERARCHY WAS UPSIDE DOWN, and only a screenshot showed it. Every
     number said this screen had improved — dead space 50% to 15%, the action
     from 33% to 68%. Then I looked at it: "Ask AI" is a full-width bright
     accent button at the top, and "Add company" is a faint DASHED ghost card at
     the bottom. On a portfolio with no companies in it, the visually dominant
     control asks an AI about finances that do not exist yet, and the only
     useful thing on the screen reads as a placeholder.

     No measurement I have could see that. `reach`, `dead`, `density` and the
     tap-target check all pass a screen whose loudest button is the wrong one.
     That is the same class as the coach banner Nova Home found — visible to a
     person, invisible to every instrument the three of us have built.

     So, while the portfolio is empty and only on a phone: the add card becomes
     the primary — a solid accent border and a soft tint, which is the pattern
     PHILOSOPHY/CLAUDE.md prescribes (never a coloured bar down one edge) — and
     it grows to fill the space it was leaving blank. "Ask AI" keeps its place
     but stops shouting: it is still there the moment somebody wants it, and it
     is no longer the answer to "what do I do on this screen". */
  :root.product-nova-os .v-fin:has(.v-fin-cc-grid > .v-fin-cc-add:only-child) #finAsk {
    background: transparent;
    color: var(--text-300);
    border: 1px solid var(--border);
  }
  :root.product-nova-os .v-fin-cc-grid:has(> .v-fin-cc-add:only-child) .v-fin-cc-add {
    /* NO min-height HERE, AND THAT IS DELIBERATE. I tried 34dvh and then 42dvh
       to close the band of nothing between the quiet "Ask AI" and this card,
       and neither took effect — three runs, the border and tint from this very
       block applying while min-height did not. I have not found why. Rather
       than leave a declaration that looks like it does something, it is gone:
       an inert rule that reads as a fix is the exact failure mode this file has
       been full of all week, and shipping one knowingly would be worse than
       shipping the gap.
       The gap is real and still there. It is written down in
       MOBILE-STATE-NOVA-OS.md as unfinished rather than dressed up. */
    border-style: solid;
    border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
    background: color-mix(in srgb, var(--accent) 8%, var(--surface-1));
  }
  :root.product-nova-os .v-fin-cc-grid:has(> .v-fin-cc-add:only-child) .v-fin-cc-addic {
    background: color-mix(in srgb, var(--accent) 14%, var(--surface-1));
    color: var(--accent);
  }
}

/* ============================================================
   THE CALENDAR TOOLBAR WAS 219px OF A 629px SCREEN.

   Measured, not eyeballed: `.cal-topbar` 375x219, its right group alone
   375x152 because six controls wrap to three rows, and the grid did not start
   until y=339 — more than half the phone gone before a single hour of the day.
   The date was truncated in the middle of it: `.cal-period` had 129px and
   needed 175, so a calendar showed "September ..." and withheld the one piece
   of orientation it owes you.

   NOTHING IS HIDDEN, AND THAT CONSTRAINT SHAPED THE ANSWER. The obvious phone
   move is to drop the labels off Select / Plan day / Tasks and leave icons, but
   the Nova Home session found exactly that on their Docs screen — a plus and an
   up-arrow with nothing to say what they do, and no hover on a phone to ask.
   Hiding the controls themselves is worse: NOVA-MOBILE.md's parity law says not
   a single feature may be desktop-only, and the sidebar toggle in particular
   looks like dead weight and is not — calendar.js:4839 opens that aside as a
   drawer on a small screen, so the toggle is the only way in.

   So the row scrolls instead. Everything stays, labelled, at full size; the
   toolbar takes one row rather than three. A horizontally scrolling strip of
   controls is a real phone idiom rather than a workaround, and the sweep's
   sideways-scroll check already tolerates an element that sets its own
   overflow-x — it is the PAGE scrolling sideways that is the defect.

   And the date gets its own line, which is what a phone calendar header looks
   like anyway: the day you are on as a title, the controls under it.
   ============================================================ */
@media (max-width: 900px) and (pointer: coarse) {
  /* THE DATE WAS NEVER TOO BIG — IT WAS BEING SHRUNK. `.cal-period` needed
     175px and had 129, and my first fix gave it a whole row of its own, which
     cured the truncation and made the toolbar TALLER than before: 219px to 250,
     with the grid starting 30px later. A fix that costs more than the bug.

     The four things in that row measure 351px against 355 available. They fit.
     The h1 was simply the flex item giving way, so it alone is told not to. */
  /* THE DATE WAS NEVER TOO BIG — IT WAS BEING SHRUNK, AND THEN I MADE THE
     TOOLBAR TALLER TWICE TRYING TO FIX IT. `.cal-period` needed 175px and had
     129. Giving it its own row cured the truncation and took the toolbar from
     219px to 250. Telling it not to shrink cured it and took the toolbar to
     271, because the controls beside it wrapped instead. Both "fixes" cost more
     than the bug.

     The four items in that row measure 351px against 355 available at 18px
     type. At `--fs-sm` the date needs ~146 instead of 175 and the row totals
     ~322 — it fits on one line, complete, with room to spare. 15px on a phone
     is the token scale's own body-adjacent size and well clear of the 13px
     floor, and a date is a label rather than reading matter.

     Measure the row before adding to it. */
  /* NO font-size HERE ANY MORE, AND THE REASON IS THE POINT. I shrank this to
     `--fs-sm` to stop "September 4, 2026" ellipsising, measured 146/146, and
     called it fixed. It fit BECAUSE TODAY IS THE 4th. The Nova Home session
     substituted the worst case instead of waiting for it: a two-digit day needs
     140 against 132 on their toolbar and truncates again. A label that fits for
     twenty-one days a month is not a fix, and every number I had said done
     because they were measured on a date that flattered them.

     They fixed it in the shared view instead (#1218): `periodLabel()`
     abbreviates the month below 430px, so "Sep 25, 2026" needs 121 at the
     ORIGINAL 18px — verified here by navigating the calendar to a two-digit day
     rather than trusting the arithmetic. Nothing has to shrink, so this rule is
     gone and the date is back to full size, which is the direction Vincent
     asked for.

     `min-width: max-content` stays: it is what stops the h1 being the flex item
     that gives way, which was the actual bug under the truncation. */
  :root.product-nova-os .v-cal .cal-period {
    flex: 0 0 auto;
    min-width: max-content;
  }
  /* THE RIGHT GROUP WRAPS. IT DOES NOT SCROLL, AND I HAD IT THE OTHER WAY ROUND
     FOR ABOUT TEN MINUTES.

     Making it one scrolling row took the toolbar from 219px to 150 and every
     check passed — parity 41 ok, calendar ok, nothing overflowed, nothing
     clipped, no tap target failed. Then I measured what was inside it:
     scrollWidth 618 in a 355px box, with `My time (GMT+2`, `Select`, `Plan day`
     and `Tasks` sitting off the right edge. Plan day is one of this product's
     headline features and I had just made it unreachable on every phone, behind
     a horizontal scroll that nothing advertises, to save 69px of chrome.

     The Nova Home session hit the identical thing on their calendar and shipped
     the answer before I could ship the mistake (#1211). Their rule is the one
     to keep: **a strip of homogeneous chips may scroll — swiping is the obvious
     gesture and a half-visible chip advertises the rest — but a toolbar of
     distinct one-off actions must wrap, because nothing signals that more
     exists.** Same mechanism, opposite correct answer, decided by what is in
     the row rather than by how much room it saves.

     So the saving here comes only from the date taking its own line, which it
     needed anyway. Two rows of buttons instead of three, everything visible. */
  :root.product-nova-os .v-cal .cal-topbar-r { flex-wrap: wrap; }
}

/* ============================================================
   A NEW CUSTOMER'S FIRST SCREEN HAD NOTHING TO DO ON IT.

   Measured on a genuinely fresh tenant — the first time this project has been
   able to, via scripts/nova-local-stack.mjs, which boots the product on a
   scratch database and mints an account through the real signup path. Every
   account any of the three sessions had ever measured came with onboarding
   already done, because the minters take SKIP_SETUP=1.

   On a 375x629 phone, `#/home`:

     .v-nos-hero    top 47   412px tall   65% of the screen
     .v-nos-stack   top 459  — everything you can DO starts here
     first action   y 844    — 280px BELOW the fold at 565

   Zero buttons, zero inputs above the fold. The visible screen is a date, a
   greeting, "What's next today, boss?" and a 168x168 decorative orb. The one
   actionable thing, "Revenue · Connect it", is cut off behind the nav bar.

   The orb is `aria-hidden` ornament and it is the largest element on a new
   customer's first screen. Below 720px the hero stacks and gives it 168px;
   beside the greeting at 92px it costs a fifth of that and still reads as
   alive, which is what it is for. Nothing is removed.
   ============================================================ */
@media (max-width: 900px) and (pointer: coarse) {
  :root.product-nova-os .v-nos-hero {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--sp-4);
  }
  :root.product-nova-os .v-nos-engine { order: 0; }
  :root.product-nova-os .v-nos-engine svg { width: 92px; height: 92px; }
}

/* ============================================================
   THE TOP BAR TITLE IS THE ONLY NAME THE SCREEN HAS NOW.

   The heading dedupe above hides the in-page <h1> when it repeats the top bar.
   That makes the top bar load-bearing — and on Nova Assistant at 375pt it was
   already exactly full: a back button, the title, and THREE 44pt icons (report
   an issue, notifications, toggle theme). Measured, "Email preferences" needs
   153px and has 143, so it rendered as "Email preferen…". The one piece of
   orientation the screen owes you was the part being cut.

   The theme toggle is the one to go, on this product, on a phone:
     · Settings already has theme, and on this product every screen carrying
       this bar is one tap from Settings.
     · an iOS app is expected to follow the system appearance, and the shell
       already tracks data-theme and repaints the status bar with it.
     · the chat route hides the top bar outright, so this removes nothing from
       the screen people actually spend their time on.

   Scoped to this product deliberately. Nova OS and Nova Home have their own top
   bars, their own title lengths and their own sessions to decide; shipping
   shared chrome on one product's measurement is the mistake .view-head nearly
   was. 44px back, and no title on this product truncates.
   ============================================================ */
@media (max-width: 900px) and (pointer: coarse) {
  :root[data-product="nova-assistant"] .topbar #themeBtn { display: none; }
}
