  @font-face {
    font-family: 'Fustat';
    font-weight: 200 800;
    font-style: normal;
    font-display: swap;
    src: url('/fonts/Fustat-Variable.ttf') format('truetype-variations');
  }
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  [hidden] { display: none !important; }
  html, body { margin: 0; padding: 0; }
  body {
    background: var(--bg); color: var(--ink); font-family: var(--font);
    height: 100vh; height: 100dvh; overflow: hidden;
    display: flex; justify-content: center;
  }
  #app { width: 100%; max-width: 480px; height: 100%; display: flex; flex-direction: column; position: relative; }

  #screen-auth,
  #screen-ingredients-onboarding,
  .onboard-slide {
    --ink: #fff;
    --ink-soft: rgba(255,255,255,.72);
    --ink-faint: rgba(255,255,255,.48);
    --surface: rgba(255,255,255,.08);
    --line: rgba(255,255,255,.12);
    --line-strong: rgba(255,255,255,.22);
    background:
      linear-gradient(180deg, rgba(8,10,9,.62) 0%, rgba(8,10,9,.92) 58%, #080a09 100%),
      url('/assets/landing-mobile-bg.png') center top / cover no-repeat,
      #080a09;
    color: var(--ink);
  }
  #screen-auth .card,
  #screen-ingredients-onboarding .card,
  .onboard-slide .card {
    background: rgba(255,255,255,.08);
    border-color: rgba(255,255,255,.14);
    box-shadow: none;
  }
  #screen-auth input,
  #screen-ingredients-onboarding input,
  .onboard-slide input,
  .onboard-slide select {
    background: rgba(255,255,255,.94);
    color: #080a09;
  }
  #screen-auth .tertiary-link,
  #screen-ingredients-onboarding .tertiary-link,
  .onboard-slide .tertiary-link {
    color: rgba(255,255,255,.76);
  }
  .entry-brand {
    display: inline-flex;
    align-items: center;
    align-self: center;
    gap: 7px;
    margin-top: .35rem;
    margin-bottom: .25rem;
  }
  .entry-cleaver { width: 14px; height: auto; display: block; }
  .entry-wordmark { width: 68px; height: auto; display: block; filter: grayscale(1) brightness(0) invert(1); }
  .entry-role-switch {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    flex-wrap: wrap;
    color: rgba(255,255,255,.62);
    font-size: .9rem;
    font-weight: 700;
    text-align: center;
  }
  .entry-role-switch a {
    color: #fff;
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  .top-bar {
    display: flex; flex-direction: column; position: relative;
    padding: env(safe-area-inset-top, 0) 1.1rem .7rem; padding-top: max(.9rem, env(safe-area-inset-top));
    flex-shrink: 0; border-bottom: none;
  }
  /* Holds what used to be .top-bar's own direct flex row (see manager.html
     comment) -- keeps the hamburger/logo/Orders row laid out identically
     to before now that .top-bar itself is a column. */
  .top-bar-row { display: flex; align-items: center; justify-content: space-between; }
  /* Mobile density pass (2026-07-30, mobile Home density match packet) --
     the logo/top nav reads slightly lighter, matching a "zoomed-out"
     feel without touching viewport scale. */
  .top-bar img { height: 16px; }
  /* Standalone mark logo (2026-07-31, replace in-app Home logo packet) --
     the old wordmark was wide/landscape, so 16px height read fine at that
     width. The new mark (assets/mayoom-mark.svg) is tall/narrow
     (854x1151, no text) -- at the same 16px height it would render ~12px
     wide and disappear next to the hamburger. Sized on its own so it
     reads as an intentional icon-sized mark instead. */
  #app-logo {
    height: 26px; width: auto; transform: rotate(2deg); transform-origin: center;
    transition: transform .22s cubic-bezier(.2,.8,.2,1);
  }
  /* HOME INTERACTION ANIMATION PASS (2026-08-02) -- a tiny settle on the
     mark itself when the hamburger menu opens/closes (same .menu-open
     class setMenuOpen() already toggles on .top-bar, no new JS), composed
     with the mark's existing permanent 2deg tilt rather than replacing it. */
  .top-bar.menu-open #app-logo { transform: rotate(-9deg) scale(1.08); }
  @media (prefers-reduced-motion: reduce) {
    #app-logo { transition: none; }
  }
  /* URGENT POLISH PASS (2026-07-31) -- #app-logo is now an inline <svg>
     (was an <img src="mayoom-mark.svg">, broken in at least one serving
     environment where the server didn't return an image content-type for
     it). The black-fill<->white-fill swap on menu-open, previously done
     by swapping the <img>'s src between two asset files, is now a plain
     CSS rule off the same .top-bar.menu-open class switchTab() already
     toggles -- no JS asset-swap needed. */
  #app-logo .logo-fill-main { fill: #ff0000; }
  #app-logo .logo-fill-accent { fill: white; }
  .top-bar.menu-open #app-logo .logo-fill-main { fill: #ff0000; }
  .top-bar.menu-open #app-logo .logo-fill-accent { fill: black; }

  /* Removed from view entirely (founder, 2026-07-27) -- kept in the DOM
     only as a harmless anchor point switchTab() uses to restore the
     top-bar's position on non-Home tabs; updateLiveStrip() still runs
     but has nothing visible to update. */
  .live-strip { display: none; }

  /* Home tab (2026-07-27): the shared top-bar is physically reparented
     into #tab-home (via #home-hero-topslot) on switchTab('home') (see JS)
     so the green gradient is truly edge-to-edge from the top of the
     screen -- no separate white bar sitting above it. Moved back out to
     its original slot (just before .live-strip) on every other tab,
     completely unchanged there. */
  #tab-home .top-bar {
    background: transparent; border-bottom: none;
  }
  /* City-control colorway flip (2026-07-29, direct instruction) -- Home
     used to force every top-bar element white to read against the heavy
     green gradient. The gradient's gone (see #tab-home's own background
     below), so these white-forcing overrides are removed entirely and
     everything just falls back to the same dark/default styling every
     other tab already uses -- no new rules needed. The real pink logo
     (assets/logo.svg) was never filtered white by anything other than
     the rule just removed, so it now renders in its own real color
     unchanged, no separate fix required. */
  #tab-home #orders-cart-btn { color: var(--ink-soft); }
  /* Tab bar is physically reparented into #tab-home on Home too (see JS)
     -- floats as a transparent overlay directly on the gradient, exactly
     like the artifact, instead of reserving its own white flex-row space
     below the content (which was the actual source of the white field
     the founder kept seeing behind the nav icons -- .screens never
     painted anything back there because the tab bar sat outside it).
     Restored to its normal flex-sibling slot at the bottom of
     #screen-app on every other tab, completely unchanged there. */
  /* Deliberately NOT position:absolute (2026-07-28, fixed a real bug):
     the tab bar used to overlay the bottom of the screen as a floating
     element, which needed the CTA button above it to guess a clearance
     distance -- two independently-tuned numbers that drifted out of sync
     twice. #tab-home's gradient background already covers the entire
     screen regardless of whether the tab bar is absolutely positioned or
     just its normal last flex child, so there was never a visual reason
     for the overlay -- only collision risk. Normal flow + .screen's own
     gap (.6rem, ~10px) guarantees real, non-zero space between the button
     and the tab bar with no manual number to keep in sync. */

  /* Home "next best action" monitor line (2026-07-28) -- dark text now
     (2026-07-29, city-control colorway flip; was white-on-green) matching
     the rest of the hero copy until there's a real action to route to
     (updateMonitorLine() toggles this class + the element's own disabled
     state together, never one without the other). No eyebrow label, no
     chip, no boxed frame -- just the line itself. */
  #monitor-line.tappable-row { color: var(--ink); font-weight: 600; cursor: pointer; }

  .toggle-switch {
    flex-shrink: 0; width: 50px; height: 30px; border-radius: 999px; border: none; background: var(--line-strong);
    position: relative; cursor: pointer; padding: 0; transition: background .15s;
  }
  .toggle-switch[aria-checked="true"] { background: #000; }
  .toggle-knob {
    position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: left .22s cubic-bezier(.34, 1.8, .4, 1), width .1s .05s;
  }
  .toggle-switch[aria-checked="true"] .toggle-knob { left: 23px; }
  /* Knob stretches slightly toward the direction of travel while sliding
     (the classic iOS-style overshoot) instead of just teleporting. */
  .toggle-switch:active .toggle-knob { width: 28px; }
  .toggle-switch[aria-checked="true"]:active .toggle-knob { left: 19px; }
  .icon-btn { background: none; border: none; cursor: pointer; padding: .5rem; color: var(--ink-soft); font-size: 1.1rem; min-width: 44px; min-height: 44px; }
  .menu-line { display: block; width: 14px; height: 1.5px; background: var(--ink); border-radius: 1px; }
  #menu-btn .menu-line { transition: transform .18s cubic-bezier(.2,.8,.2,1), opacity .12s ease, background-color .15s ease; transform-origin: center; }
  #menu-btn.menu-open .menu-line:nth-child(1) { transform: translateY(4.5px) rotate(45deg); }
  #menu-btn.menu-open .menu-line:nth-child(2) { opacity: 0; transform: scaleX(.35); }
  #menu-btn.menu-open .menu-line:nth-child(3) { transform: translateY(-4.5px) rotate(-45deg); }

  /* Black push-down menu section (2026-08-01, replaces the black top-third
     OVERLAY packet from 2026-07-31 -- that version made .menu-open
     position:absolute against #app at a fixed 33vh height + z-index:300,
     which floated the black zone on top of everything below it, including
     Home's money hero (same DOM stacking context, .top-bar sits right
     above #home-economic-hero-block once reparented into
     #home-hero-topslot -- see switchTab()). Founder call: the menu should
     behave like a normal section expanding downward, pushing content
     below it, never covering the hero. Fix is layout-only: .top-bar keeps
     its own base rule's `position: relative` (for its children's z-index
     needs) but .menu-open no longer takes it out of flow -- no
     position:absolute, no fixed height, no z-index stacking trick. It
     just grows to fit #menu-dropdown's real content, and every sibling
     after it (the reparented Home hero on Home, `.screens` on every other
     tab) gets pushed down by exactly that much, the same way any other
     growing block-level element pushes its siblings. No animation, per
     direct instruction -- open/closed is an instant state, not a motion
     moment. */
  /* #tab-home .top-bar (below, transparent-on-map for the normal state)
     is an id+class selector -- higher specificity than a plain two-class
     `.top-bar.menu-open`, so on Home that transparent rule silently won
     and the black menu section never actually painted black there (found
     live-testing this exact packet: DOM/layout was correct, computed
     background stayed rgba(0,0,0,0)). Scoping this rule to `#tab-home
     .top-bar.menu-open` too guarantees it wins on Home without touching
     the base `#tab-home .top-bar` rule non-menu-open tabs still need. */
  .top-bar.menu-open,
  #tab-home .top-bar.menu-open {
    background: #000;
  }
  /* Brand/menu elements inside the black zone invert to white. app-logo
     swaps to a dedicated white-mark asset via JS (setMenuOpen()) instead
     of a CSS filter, so the mark's own black/white detail stays exact. */
  .top-bar.menu-open .menu-line { background: #fff; }
  .top-bar.menu-open #orders-cart-btn { color: #fff; }
  .top-bar.menu-open #orders-cart-btn .soft { color: #fff; }
  .top-bar.menu-open #orders-cart-btn svg { filter: invert(1) brightness(2); }

  /* flex:1/overflow-y:auto dropped (2026-08-01) -- those only meant
     something when .top-bar.menu-open had a fixed 33vh height to fill;
     now the section sizes to its own content, so the dropdown just needs
     normal block spacing. overflow-y:auto kept as a defensive cap in case
     the menu option list ever grows long enough to need its own scroll,
     without constraining it while it's short (today: 4 options). */
  #menu-dropdown {
    display: flex; flex-direction: column; align-items: stretch;
    margin-top: 1.1rem; max-height: 60vh; overflow-y: auto;
    transform-origin: top center;
  }
  #menu-dropdown[hidden] { display: none !important; }
  /* HOME INTERACTION ANIMATION PASS (2026-08-02) -- same auto-playing
     reveal-on-paint approach as the project switcher above; the section's
     own push-down growth (no position:absolute, see the block comment
     above) stays exactly as designed, this only softens how its *content*
     appears once #menu-dropdown itself is no longer [hidden]. Closing
     stays instant, per this same section's own existing "not a motion
     moment" comment. */
  #menu-dropdown:not([hidden]) {
    animation: homeDropdownRevealPlain .16s cubic-bezier(.2,.8,.2,1) both;
  }
  /* Deliberately a separate keyframe from #home-project-switcher-menu's
     homeDropdownReveal -- that one bakes in translateX(-50%) to preserve
     the pill's horizontal centering through the animation, which would
     wrongly shift this full-width, non-centered dropdown sideways (and
     leave it shifted at rest, since animation-fill-mode:both holds the
     `to` state) if reused here. */
  @keyframes homeDropdownRevealPlain {
    from { opacity: 0; transform: translateY(-4px) scale(.98); }
    to { opacity: 1; transform: translateY(0) scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    #menu-dropdown:not([hidden]) { animation: none; }
  }
  #menu-dropdown .text-btn {
    width: 100% !important; flex: none; border-top: 1px solid rgba(255,255,255,.16) !important; border-left: none !important;
    text-align: left !important; padding: 1rem .1rem !important; min-height: 46px; font-weight: 750;
    color: #fff;
  }
  #menu-dropdown .text-btn:first-child { border-top: none !important; }
  @media (prefers-reduced-motion: reduce) {
    #menu-btn .menu-line, .top-bar.menu-open { transition: none !important; animation: none !important; }
  }

  /* Home project switcher pill (2026-08-01, widened + one-active-recipe
     packet) -- quiet app chrome. min-width makes it read as an
     intentional, deliberately-sized control (not a shrink-to-fit label)
     while max-width + the label's own ellipsis (below) still guarantee
     one line, no wrap, no overflow for a long recipe name. */
  #home-project-switcher-btn {
    display: flex; align-items: center; gap: .35rem;
    min-width: 210px; max-width: 240px; padding: .4rem .75rem;
    border-radius: 999px; border: 1px solid var(--line);
    background: rgba(255,255,255,.72); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    font-family: var(--font); font-size: .78rem; font-weight: 600; color: var(--ink);
    cursor: pointer;
  }
  #home-project-switcher-label { min-width: 0; flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: left; }
  #home-project-switcher-btn svg { color: var(--ink-soft); transition: transform .15s ease; }
  #home-project-switcher-btn[aria-expanded="true"] svg { transform: rotate(180deg); }
  #home-project-switcher-menu {
    position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
    min-width: 210px; max-width: 260px;
    background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(26,14,16,.12); padding: .3rem;
    transform-origin: top center;
  }
  #home-project-switcher-menu[hidden] { display: none !important; }
  /* HOME INTERACTION ANIMATION PASS (2026-08-02) -- a plain @keyframes
     animation (not a transition) auto-plays the instant `hidden` is
     removed and the element is first painted, so opening gets a soft
     reveal with zero JS changes to the existing hidden-attribute toggle in
     manager.js. Closing stays instant, matching this app's own existing
     hamburger-menu precedent ("open/closed is an instant state, not a
     motion moment" per that packet) -- not adding a mismatched asymmetry,
     following one already established here. transform includes the
     existing translateX(-50%) centering in every keyframe step so the
     reveal never breaks the pill's horizontal centering mid-animation. */
  #home-project-switcher-menu:not([hidden]) {
    animation: homeDropdownReveal .16s cubic-bezier(.2,.8,.2,1) both;
  }
  @keyframes homeDropdownReveal {
    from { opacity: 0; transform: translateX(-50%) translateY(-4px) scale(.98); }
    to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    #home-project-switcher-menu:not([hidden]) { animation: none; }
  }
  #home-project-switcher-menu .text-btn { width: 100% !important; text-align: left !important; padding: .6rem .55rem !important; }
  /* Current/selected item (2026-08-01) -- static text, not a button: reads
     as "this is what's active" without competing with the one real action
     below it. Same one-line/ellipsis discipline as the pill face itself. */
  #home-project-switcher-current-title {
    padding: .6rem .55rem .3rem; font-size: .82rem; font-weight: 700; color: var(--ink);
    overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  }
  /* Red TEXT action (2026-08-01, direct instruction: "red text, not red
     background") -- same .text-btn sizing/tap-target as before, just
     recolored and not full-width-primary. */
  #home-project-switcher-pick-new-btn { color: #c0392b !important; font-weight: 700; }
  #home-project-switcher-pick-new-btn:active { background: rgba(192,57,43,.08) !important; }
  /* Calm inline "still moving" message (2026-08-01) -- small/plain text,
     never a modal or toast. Hidden by default; shown only for the moment
     the maker clicks the red action on a for-sale/booked launch, cleared
     the next time the dropdown opens/closes (see manager.js). */
  #home-project-switcher-moving-msg {
    padding: .45rem .55rem .3rem; font-size: .72rem; line-height: 1.4; color: var(--ink-soft);
  }
  #home-project-switcher-moving-msg[hidden] { display: none !important; }

  /* RAINBOW FLICKER DOT GRID (2026-08-03, founder direct request --
     replaces the sparse-cluster "electric circuit board" pass above).
     #home-map-paper-overlay (manager.html) is a plain decorative
     paper-texture div sitting ABOVE the map, not the map/Mapbox itself --
     its own inline grey dot grid (`radial-gradient(circle, rgba(0,0,0,.11)
     1px, transparent 1.2px) 0 0/16px 16px`) is left completely untouched;
     this adds two pseudo-element color layers on the exact same 16px
     grid/position so the tint lands on every real grey dot, not a sparse
     subset. Each layer cycles a `filter: hue-rotate()` through the full
     spectrum (a real rainbow, not a narrow blue/green range) while its
     opacity jitters on an irregular multi-stop keyframe with a stepped
     timing function, for a flicker rather than a smooth breathing fade.
     ::before/::after run different hue-cycle and flicker durations so the
     two layers drift out of phase. Direct founder instruction: skip the
     prefers-reduced-motion opt-out for this effect. No Mapbox source/
     layer/paint code touched -- see CLAUDE.md's City Takeover Map lock. */
  #home-map-paper-overlay::before {
    content: "";
    position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(circle, #ff3b6b 1px, transparent 1.2px) 0 0 / 16px 16px;
    mix-blend-mode: soft-light;
    animation: homeDotRainbowHueA 8s linear infinite, homeDotRainbowFlickerA 2.6s steps(10, end) infinite;
  }
  #home-map-paper-overlay::after {
    content: "";
    position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(circle, #3d7fd6 1px, transparent 1.2px) 0 0 / 16px 16px;
    mix-blend-mode: soft-light;
    animation: homeDotRainbowHueB 6.4s linear infinite, homeDotRainbowFlickerB 3.4s steps(9, end) infinite;
  }
  @keyframes homeDotRainbowHueA {
    0% { filter: hue-rotate(0deg); }
    100% { filter: hue-rotate(360deg); }
  }
  @keyframes homeDotRainbowHueB {
    0% { filter: hue-rotate(180deg); }
    100% { filter: hue-rotate(540deg); }
  }
  @keyframes homeDotRainbowFlickerA {
    0%, 100% { opacity: .05; }
    12% { opacity: .32; }
    24% { opacity: .09; }
    38% { opacity: .4; }
    50% { opacity: .11; }
    64% { opacity: .36; }
    76% { opacity: .1; }
    88% { opacity: .26; }
  }
  @keyframes homeDotRainbowFlickerB {
    0%, 100% { opacity: .04; }
    15% { opacity: .22; }
    30% { opacity: .07; }
    46% { opacity: .3; }
    58% { opacity: .08; }
    70% { opacity: .28; }
    83% { opacity: .06; }
    93% { opacity: .18; }
  }

  .screens { flex: 1; overflow: hidden; position: relative; }
  .screen { position: absolute; inset: 0; overflow-y: auto; padding: .5rem 1.1rem 1.5rem; display: flex; flex-direction: column; gap: .6rem; -webkit-overflow-scrolling: touch; }
  /* CHAT UI STRUCTURE PASS (2026-08-01) -- Home only: the screen frame
     itself must not scroll (top bar/money hero/map/bottom nav stay put),
     only its own #home-chat-scroll transcript region does. Higher
     specificity (id) than .screen's overflow-y:auto above, and scoped to
     this one screen -- every other tab keeps the shared whole-page scroll
     behavior untouched. */
  #tab-home { overflow-y: hidden; }
  /* HOME REBUILD (2026-08-01) -- Home is now a fixed app shell: money/map
     stay in place, the conversation is the only scroll region, and the
     grounded composer owns the bottom of the screen. The bottom tab bar is
     still present/reparented for other tabs, but hidden on Home so it no
     longer fights the chat input. */
  /* =====================================================================
     BEGIN FOUNDER-LOCKED HOME/CITY GEOMETRY — KNOWN GOOD: 85b7ce8
     Do not edit without an explicit founder request naming this geometry.
     See CLAUDE.md and docs/MAYOOM-CITY-MAP-LOCK.md.
     =====================================================================
     Shared compact hero geometry (2026-08-02): normal Home and Hide UI use
     the same in-flow amount + controls arrangement. The old scaffold is
     empty; removing it from layout entirely is more reliable than
     display:contents in WebKit flex layout, which was still leaving the
     controls on a different Hide-UI path on the founder's device. */
  #tab-home #home-plan-scaffold-wrap { display: none !important; }
  /* Hero + eye/project controls are one real layout unit in both Home and
     City View. Keeping them under the same parent prevents iOS/WebKit from
     resolving the controls against a different flex remainder when the
     chat shell is removed. */
  #tab-home #home-hero-controls-group {
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    gap: 0;
    position: relative;
    z-index: 3;
  }
  #tab-home #home-chat-shell {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    padding: 0 1.1rem max(.7rem, env(safe-area-inset-bottom, 0px)) !important;
    margin-top: -150px;
  }
  #tab-home #home-economic-hero-block {
    max-height: none;
    overflow: visible;
  }
  /* These legacy Home nodes are intentionally unavailable, but the old
     visibility:hidden button still occupied real height inside the hero.
     Remove their layout footprint so the controls follow the visible
     number—not an invisible 180px box—in both Home modes. */
  #tab-home #monitor-line,
  #tab-home #savings-biz-name,
  #tab-home #view-offers-btn { display: none !important; }
  #tab-home #economic-hero-label { margin-top: 0 !important; }
  #tab-home #home-controls-row { margin-top: 2px !important; }
  /* City View keeps the original hero, eye toggle, and restaurant pill in
     their shared group. Only the conversation/composer disappears. */
  #tab-home.home-ui-hidden #home-chat-shell {
    display: none !important;
  }
  /* =====================================================================
     END FOUNDER-LOCKED HOME/CITY GEOMETRY
     ===================================================================== */
  #home-city-view[hidden] { display: none !important; }
  /* HIDE UI MAPBOX PALETTE + DOORDASH TOGGLE PASS (2026-08-02) -- campaign
     framing swap, presentation only. The real "$0.00" / "Money made +
     saved" text is untouched in the DOM (manager.js never writes to these
     elements differently in Hide UI) -- font-size:0 visually collapses
     the real text to zero width without touching its content or the JS
     that sets it, and a ::before with its own font-size paints the
     Hide-UI-only copy on top. Turning Hide UI off removes the
     .home-ui-hidden class (see setHomeScaffoldUiVisible()), which is the
     single switch back to the real label/number -- no separate "restore"
     code needed. Money/revenue state itself is never mutated.
     !important is required on the real element's font-size/color: both
     carry inline styles (font-size:3.57rem/.74rem, color:var(--ink)) in
     manager.html, which otherwise beat any plain stylesheet rule
     regardless of selector specificity. */
  #tab-home.home-ui-hidden #economic-hero-label-text {
    font-size: 0 !important;
  }
  #tab-home.home-ui-hidden #economic-hero-label-text::before {
    content: "Road to a million.";
    font-size: .74rem; font-weight: 700; letter-spacing: .02em; color: #fff;
  }
  #tab-home.home-ui-hidden #economic-hero-info-btn {
    border-color: rgba(255,255,255,.6); color: #fff;
  }
  #tab-home.home-ui-hidden #savings-total {
    font-size: 0 !important; color: #fff !important;
  }
  #tab-home.home-ui-hidden #savings-total::before {
    content: "0,000,000";
    font-size: 3.57rem; font-weight: 800; line-height: 1; color: #fff;
  }
  /* Share bar text button removed from the Hide UI tray (the two icon
     buttons already do the action) -- element/listener stay in the DOM
     untouched, just unreachable while Hide UI is open; #share-url's own
     flex:1 automatically claims the freed width, no other row CSS
     changes needed. */
  #home-city-view #view-pool-btn { display: none !important; }
  /* DoorDash/you map-mode toggle -- quiet pill, same surface language as
     the share tray. aria-pressed (set in manager.js, never a new class)
     is the only state hook so the pill's look tracks the real state it
     announces to assistive tech, not a separate visual-only flag.
     !important required: the button carries an inline
     background/border-color/color (manager.html), which otherwise beats
     this rule regardless of selector specificity -- same issue as the
     hero label/number swap above. */
  #home-city-mode-toggle[aria-pressed="true"] {
    background: rgba(26,14,16,.82) !important; border-color: rgba(26,14,16,.82) !important; color: #fff !important;
  }
  /* CITY VIEW CAMERA COMPOSITION AMENDMENT (2026-08-03) -- the hero number/
     label quietly step aside while the user is actively dragging/zooming
     the Hide UI map (class toggled in manager.js's movestart/moveend/tap
     handlers), so the white text never has to fight a moving map
     underneath it, then return once the gesture settles. Opacity only --
     no layout/geometry change, so this does not touch the founder-locked
     Home/City View geometry invariants above. Normal Home is untouched:
     the map is never interactive outside Hide UI, so this class is never
     added there. */
  #tab-home.home-ui-hidden #home-economic-hero-block {
    transition: opacity .2s ease;
  }
  #tab-home.home-ui-hidden.home-city-map-interacting #home-economic-hero-block {
    opacity: 0;
  }
  @media (prefers-reduced-motion: reduce) {
    #tab-home.home-ui-hidden #home-economic-hero-block { transition: none; }
  }
  #home-city-takeover-stat {
    position: absolute; left: 50%; bottom: 16%; transform: translateX(-50%);
    max-width: 290px; text-align: center; pointer-events: none;
    background: rgba(255,255,255,.86); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    border: 1px solid var(--line-strong); border-radius: var(--radius-md);
    padding: .65rem .9rem;
  }
  #home-city-takeover-stat-line { font-size: .82rem; font-weight: 700; color: var(--ink); line-height: 1.4; }
  #home-city-takeover-stat-sub { font-size: .74rem; font-weight: 600; color: var(--ink); margin-top: .2rem; }
  #tab-home #home-launch-thread {
    flex: 1 1 auto !important;
    min-height: 0 !important;
    gap: .55rem !important;
    margin-bottom: 0 !important;
  }
  #tab-home #home-tabbar-slot {
    display: none !important;
  }

  h1 { font-size: 1.25rem; margin: 0; text-wrap: balance; }
  h2 { font-size: .95rem; margin: 0; }
  p { margin: 0; line-height: 1.5; }
  .soft { color: var(--ink-soft); font-size: .88rem; }
  label { display: flex; flex-direction: column; gap: .35rem; font-size: .88rem; color: var(--ink-soft); font-weight: 600; }

  /* Non-Home legibility + visual hierarchy pass (2026-07-30) -- shared
     rhythm/contrast rules for The Day/Planned/Menu/Waitlist's section
     headers and operating rows. One consistent label/value convention:
     the label (what this row is) reads bold/ink, the value (the answer)
     reads muted/right-aligned -- some rows had this inverted before,
     reading as competing gray-on-gray. Chips get a real pill background
     instead of color-only text so a blocker/next-action doesn't have to
     fight the row around it for attention. Opt-in classes only -- Home is
     untouched. */
  .section-title { font-size: .74rem; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; color: var(--ink); margin: 0; }
  .op-note { font-size: .82rem; color: var(--ink-soft); line-height: 1.5; margin: 0; }
  .op-row { display: flex; align-items: baseline; justify-content: space-between; gap: .7rem; padding: .5rem 0; border-top: 1px solid var(--line); }
  .op-row:first-child { border-top: none; padding-top: 0; }
  .op-label { font-weight: 700; color: var(--ink); font-size: .84rem; }
  .op-value { color: var(--ink-soft); font-size: .82rem; text-align: right; }
  .op-chip { font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .03em; padding: .22rem .55rem; border-radius: 999px; flex-shrink: 0; white-space: nowrap; background: var(--surface); color: var(--ink-soft); }
  .op-chip-ready { color: var(--success); background: var(--success-wash); }
  .op-chip-blocked { color: var(--danger); background: var(--danger-wash); }
  .op-alert { font-size: .82rem; color: var(--danger); font-weight: 600; line-height: 1.5; margin: 0; }
  .next-action-line { font-size: .88rem; font-weight: 800; color: var(--ink); background: var(--accent-wash); border-radius: var(--radius-sm); padding: .6rem .75rem; margin-top: .1rem; }
  input, select, textarea { font-family: var(--font); font-size: 16px; padding: .8rem .85rem; border-radius: var(--radius-sm); border: 1px solid var(--line-strong); background: #fff; color: var(--ink); width: 100%; }
  /* iOS Safari automatically zooms the entire viewport when a focused form
     control computes below 16px. Legacy inline field styles must never win:
     make 16px the hard floor everywhere, so the same control cannot zoom iOS
     while looking different in another browser. */
  input, select, textarea { font-size: 16px !important; }
  textarea { resize: vertical; }
  input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
  /* LIVE FOUNDER FEEDBACK (2026-08-03): the large-title treatment below
     (#new-offering-input-scaffold, added for the restaurant-name stage)
     was bleeding into the placeholder text via normal font inheritance --
     ::placeholder only had its own color/size set, so weight/family fell
     through from the host input (800/Inter) and rendered the placeholder
     bold. Explicit reset here keeps the placeholder calm/normal regardless
     of what the host input's own typed-text styling does. */
  #new-offering-input-scaffold::placeholder {
    color: var(--ink-faint); font-size: .82rem;
    font-weight: 400; font-family: var(--font);
  }
  /* RESTAURANT NAME + DISH PROMPT SVGs RETIRED (2026-08-03, live founder
     feedback): both bubbles used to show a founder-supplied SVG (an
     illustrated, non-interactive mockup of an input+button) -- retired in
     favor of a real, typeable field in that same spot (see
     #new-offering-input-scaffold below and renderHomeDishInlineInput() in
     manager.js). .home-restaurant-prompt-img/.home-dish-prompt-img no
     longer have any markup referencing them (grepped clean), removed.
     #new-offering-input-scaffold is a dedicated element used ONLY for
     restaurant-name entry -- never retargeted to any other stage's
     composer input -- so this large-title treatment is scoped to this one
     stage by construction, with no conditional JS needed to keep other
     stages' sizing untouched. */
  #new-offering-input-scaffold {
    font-family: 'Inter', var(--font), sans-serif;
    font-weight: 800;
    font-size: 26px;
    line-height: 1.2;
  }
  /* Guided launch thread, Packet 1 (2026-07-31) -- clean, minimal
     bubbles, not a generic chatbot look: quiet fill, asymmetric corner
     (chat-tail feel) instead of a full pill, no avatar/label. No inline
     style on these elements so the class fully controls alignment --
     see manager.html's comment on #home-plan-scaffold-caption. */
  /* CHAT UI STRUCTURE PASS (2026-08-01) -- white/near-white against the map
     (was var(--surface) grey, too close to the map's own pale wash), plus
     a soft border and the same subtle shadow already used by this app's
     other white-surface cards (.home-chat-card below) -- still natural
     chat-bubble sizing, not a card. flex column so a bubble can hold a
     text line AND action buttons (CTAs must render inside the Mayoom
     bubble that asks for them, never as a floating full-width button). */
  /* Bubble size + text pass (2026-08-01, direct instruction: "+12%") --
     padding and font-size both scaled up from the prior .55rem/.75rem
     padding and .82rem text. */
  /* MOBILE TYPE LEGIBILITY (2026-08-02) -- reading text raised .92rem
     (14.72px) -> 1.0625rem (17px), the primary chat copy a maker is
     expected to actually read. Everything else about the bubble
     (padding, radius, shadow, weight) is untouched -- this is a type
     size change only, not a redesign. */
  /* TASTE CEILING PASS (2026-08-02) -- fine border + a two-part shadow (a
     tight near-black contact shadow for depth, a hairline white inset for
     a quiet surface highlight) replaces the single flat shadow. Same
     bubble, more considered material. */
  /* =====================================================================
     HOME CHAT DESIGN SYSTEM (2026-08-03, coherency/legibility pass)
     ---------------------------------------------------------------------
     One set of tokens for the whole Home conversation. Before this pass the
     transcript carried ten different border-radii (4/6/9/10/12/14/16/18/20/
     999px, including asymmetric iMessage corners and a speech tail) and ten
     unrelated font-sizes, so nothing read as one designed surface.
     Two rules govern everything below:
       1. SHAPE IS UNIFORM. Message surfaces use --hc-radius, controls inside
          them use --hc-radius-ctl. No asymmetric corners, no tails, no pills
          except genuinely tiny utility chips.
       2. HIERARCHY IS SURFACE, NOT OPACITY. Settled history recedes by
          losing its border/shadow and sitting on a softer white; its TEXT
          stays full-contrast. (The old pass dimmed whole bubbles to
          opacity .78, which traded away the legibility this pass is for.)
     Scoped to #tab-home so no other tab inherits any of it. */
  #tab-home {
    /* COMFORT PASS (2026-08-03, direct founder instruction: 30px) --
       softer, playful message-surface radius. --hc-radius-ctl (controls:
       inputs/buttons inside bubbles) is intentionally left alone -- the
       shape distinction between a message surface and the controls inside
       it is what the design system's own comment above already calls
       "shape is uniform... controls use --hc-radius-ctl." */
    --hc-radius: 30px;
    --hc-radius-ctl: 10px;
    --hc-line: rgba(26,14,16,.09);
    --hc-line-strong: rgba(26,14,16,.15);
    --hc-shadow: 0 1px 2px rgba(26,14,16,.04);
    --hc-shadow-active: 0 6px 18px rgba(26,14,16,.10), 0 1px 3px rgba(26,14,16,.05);
    --hc-action: #00A6FF;
    --hc-body: 1.0625rem;   /* 17px - question / message text */
    --hc-answer: .9375rem;  /* 15px - the maker's own answer chip */
    --hc-helper: .8125rem;  /* 13px - helper + secondary copy */
    --hc-control: .875rem;  /* 14px - buttons, option rows */
    --hc-meta: .6875rem;    /* 11px - receipt chips, dividers */
  }
  /* SETTLED HISTORY -- the default state of any message already answered.
     No border, no shadow, softer white: it reads as transcript, not as a
     card competing with the live question. Text stays at full --ink. */
  .home-thread-bubble {
    align-self: flex-start; max-width: 90%;
    display: flex; flex-direction: column; gap: .5rem;
    /* COMFORT PASS (2026-08-03) -- was .58rem .8rem (~9px/13px), pressed
       text against the border. Now ~16px/17px, inside the packet's
       14-16px vertical / 16-18px horizontal target. */
    padding: 1rem 1.0625rem; border-radius: var(--hc-radius);
    background: rgba(255,255,255,.62); color: var(--ink); border: 1px solid transparent;
    font-size: var(--hc-answer); font-weight: 500; line-height: 1.4; text-align: left;
  }
  /* ART DIRECTION PASS (2026-08-02) -- "current stage anchor." The
     active/answerable bubble is the one that still has a live control
     inside it (a primary CTA, a secondary action, or a slot option row).
     Completed bubbles only ever hold .home-chat-done, never these classes,
     so this stays a reliable presentation-only signal with no JS state to
     keep in sync -- the instant a bubble's action resolves and the next
     render drops that control, the bubble quietly loses this emphasis on
     its own. This is deliberately a much bigger jump than the last pass's
     shadow tweak: a real elevation tier, a speech-tail pointing at the
     composer below it (nothing else in the transcript gets a tail), and a
     snap-settle entrance so the current question visibly *arrives*, not
     just re-renders. Entrance motion is scoped to ONLY this selector (not
     the base bubble class) on purpose -- renderHomeChatStages() rebuilds
     the whole transcript from scratch on every stage change, so animating
     every bubble would replay the entire history's fade-in each time
     instead of just the one new thing arriving. */
  /* THE ACTIVE DECISION MODULE -- the one live surface. Identified by the
     fact that it still holds a control (a primary CTA, a secondary action,
     or a slot option row); settled messages only ever hold .home-chat-done,
     so this stays a presentation-only signal with no JS state to sync.
     It earns attention by being the ONLY solid-white, bordered, shadowed
     object in the transcript -- not by a tail (removed: it was literal
     iMessage language and contradicted the standing "no bubbles with
     tails" rule), not by asymmetric corners, and not by the previous
     0 18px 38px shadow, which was the exact soft AI-card shadow this
     product avoids. Same 12px radius as everything else.
     FRAME CONSISTENCY FIX (2026-08-03): :not([hidden]) added to all three
     -- #home-plan-scaffold-caption's input/button are static markup that
     goes `hidden` (not removed from the DOM) once answered, so a plain
     :has(.home-bubble-cta) kept matching it forever, permanently stuck in
     this "active" look while every other completed bubble correctly
     settles. This is the one place in the whole transcript that control
     could ever be hidden-but-present, but the guard is harmless everywhere
     else (a genuinely active control is never hidden). */
  .home-thread-bubble:has(.home-bubble-cta:not([hidden])),
  .home-thread-bubble:has(.home-bubble-cta-secondary:not([hidden])),
  .home-thread-bubble:has(.home-bubble-option-row:not([hidden])) {
    /* COMFORT PASS (2026-08-03) -- slightly more than the settled/normal
       padding above, for bubbles carrying an SVG visual, an input, a
       button, or a list -- "without creating a giant card." */
    padding: 1.15rem 1.2rem;
    background: #fff;
    border-color: var(--hc-line-strong);
    box-shadow: var(--hc-shadow-active);
    /* The live module owns the largest type on the surface. Settled turns
       sit at --hc-answer (15px), matching the maker's own answer chip, so a
       finished question+answer reads as one paired transcript row rather
       than competing with the question being asked right now. */
    font-size: var(--hc-body);
    animation: homeBubbleAnchorIn .2s cubic-bezier(.2,.8,.2,1) both;
  }
  /* The one exception to "active == holds a control": the very first
     question (#home-plan-scaffold-caption, static in manager.html) is
     answered through the grounded composer, so it never contains an
     in-bubble CTA and would otherwise render as settled history on a
     brand-new maker's first screen. #home-first-waiting-marker is hidden
     by renderHomeChatStages() the moment the idea is submitted
     (ideaSubmitted), which makes "that marker is still visible" an exact,
     already-maintained signal for "the first question is still live" --
     no new JS state. */
  .home-chat-scroll:has(#home-first-waiting-marker:not([hidden])) #home-plan-scaffold-caption {
    padding: 1.15rem 1.2rem;
    background: #fff;
    border-color: var(--hc-line-strong);
    box-shadow: var(--hc-shadow-active);
    font-size: var(--hc-body);
  }
  /* COMFORT PASS (2026-08-03, direct instruction) -- the question/headline
     text inside EVERY Mayoom bubble, not just the active/has-a-control
     ones as before. Structural selector: this codebase's convention is
     that the bubble's first child div IS its heading (see
     homeChatMayoomLine()) -- extending the same "> div:first-child" rule
     to plain .home-thread-bubble (not just the :has(...) active variants)
     reaches every settled bubble's heading too, with no JS change. Never
     matches .home-msg-user (a separate class, not nested in
     .home-thread-bubble) or an <img> first child (the SVG bubbles'
     artwork), which is exactly the "not user replies / not SVG text"
     exclusion the packet asked for. Weight/tracking replace the old
     650/-.011em (tight) look. */
  .home-thread-bubble > div:first-child,
  #home-plan-scaffold-caption > div:first-child {
    font-family: 'Inter', var(--font), sans-serif;
    font-weight: 700;
    letter-spacing: 1.5px;
  }
  /* BOTTOM TRAY PASS (2026-08-02) -- "one focused object, calm history
     above it": completed turns recede further than the prior pass (.9 ->
     .78) so the current tray is unmistakably the one thing commanding
     attention, not just slightly-favored among equals. Text stays fully
     legible at 500 weight/17px against white even at this opacity. */
  @keyframes homeBubbleAnchorIn {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
  }
  /* Chat rhythm (2026-08-02): a real back-and-forth reads as tight
     question->answer pairs with a clearer breath before the next question,
     not one uniform gap between every bubble. Targets the actual DOM
     adjacency renderHomeChatStages() already produces (Mayoom bubble, then
     the user's own reply, then the next Mayoom bubble) -- no render change
     needed. ART DIRECTION PASS: the pause after a user reply is bigger
     still, so the incoming current-stage bubble reads as landing in its
     own zone, not just the next line in a list; a Mayoom ack line
     immediately followed by the real active prompt (goalAckText -> price
     prompt, the math/waitlist asks, etc.) gets the same treatment. */
  /* Three gaps, one rhythm: an answer sits tight under its question, and a
     new question gets real air before it lands. */
  .home-thread-bubble + .home-msg-user { margin-top: -.15rem; }
  .home-msg-user + .home-thread-bubble { margin-top: .95rem; }
  .home-thread-bubble + .home-thread-bubble:has(.home-bubble-cta:not([hidden])),
  .home-thread-bubble + .home-thread-bubble:has(.home-bubble-cta-secondary:not([hidden])),
  .home-thread-bubble + .home-thread-bubble:has(.home-bubble-option-row:not([hidden])) {
    margin-top: .7rem;
  }
  /* UI-ONLY CHAT CONVERSION (2026-08-01) -- the maker's own reply, mirrored
     right against Mayoom's quiet left-aligned bubble above, so a completed
     stage reads as a real back-and-forth turn instead of a receipt line.
     No avatar, no typing animation, per that packet.
     CHAT UI STRUCTURE PASS (2026-08-01): reserved for actual typed answers
     only -- tapped actions (Prepare reservations, a picked slot, a skipped
     step) are never rendered as a user bubble, per that packet's rules.
     TASTE CEILING PASS (2026-08-02) -- softened off-black instead of pure
     --ink, plus its own quiet contact shadow, so a typed answer reads as a
     confident reply, not a flat label. */
  /* COHERENCY PASS (2026-08-03) -- was a full-width 17px slab with an
     asymmetric corner and a drop shadow, i.e. exactly the "giant black
     rectangle" this product's own spec rules out. Now a compact chip:
     sized to its content, a step down in type from the question it
     answers, uniform 12px corner, no shadow. It reads as a settled
     transcript entry, which is what a completed answer is. */
  .home-msg-user {
    align-self: flex-end; width: fit-content; max-width: 82%;
    padding: .38rem .68rem; border-radius: var(--hc-radius);
    background: #1c1c1e; color: #fff;
    font-size: var(--hc-answer); font-weight: 500; line-height: 1.35; text-align: left;
  }
  @media (prefers-reduced-motion: reduce) {
    .home-thread-bubble:has(.home-bubble-cta),
    .home-thread-bubble:has(.home-bubble-cta-secondary),
    .home-thread-bubble:has(.home-bubble-option-row) { animation: none; }
  }
  /* CHAT UI STRUCTURE PASS (2026-08-01) -- only the message transcript
     scrolls; #tab-home itself no longer does (overflow-y:hidden below).
     min-height:0 is required for a flex child to actually be allowed to
     shrink to its container instead of growing to its content, which is
     what lets overflow-y:auto engage here instead of on the whole screen.
     Top fade: a plain CSS mask, transparent at the very top edge to fully
     opaque ~30px down, so old messages dissolve before colliding with the
     hero/eye area above instead of hard-clipping. Money hero itself is
     outside this element entirely, so it's never touched by the mask. */
  .home-chat-scroll {
    flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
    display: flex; flex-direction: column; gap: .5rem;
    /* Hide the scrollbar track (2026-08-01, direct instruction) -- content
       still scrolls normally, just no visible bar. scrollbar-width:none
       covers Firefox; the ::-webkit-scrollbar rule below (opacity:0 +
       zero width, per instruction) covers Chrome/Safari/iOS. */
    scrollbar-width: none;
    /* Rebuild reset: remove the old -300px/+310px compensation hack. The
       scroll box itself now starts where the chat should live. A tall
       top mask creates a visible "hider" band near the eye/Hide UI line
       without moving the transcript with fake padding math. TASTE CEILING
       PASS (2026-08-02): bottom padding raised slightly so the active
       prompt always lands with real breathing room above the composer,
       never touching it. */
    margin-top: 0;
    /* Bottom value only (2026-08-03, scroll/composer packet): 1.6rem left
       the last message ~34px off the grounded composer, which read as
       crowded rather than finished. The 210px top value and the mask below
       are the approved top-fade geometry and are deliberately untouched. */
    padding: 210px 0 2.4rem;
    scroll-padding-block: 210px 2.4rem;
    /* Fade lift (2026-08-03) -- same softness curve, moved 28px upward so
       messages disappear before reaching the Hide UI / project-pill line. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent 92px, rgba(0,0,0,.08) 122px, rgba(0,0,0,.22) 142px, rgba(0,0,0,.45) 160px, #000 182px);
    mask-image: linear-gradient(to bottom, transparent 0, transparent 92px, rgba(0,0,0,.08) 122px, rgba(0,0,0,.22) 142px, rgba(0,0,0,.45) 160px, #000 182px);
    /* Quiet local wash (2026-08-02) -- a soft, low-opacity lift behind the
       transcript so bubbles read cleanly against a busy map without a full
       card panel; strongest near the composer where reading concentrates,
       fully transparent by the top fade so the map still shows through up
       top. Scrolls with the content on purpose (it's part of the
       transcript's own surface, not a fixed viewport overlay). */
    background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.16) 45%, rgba(255,255,255,.22) 100%);
  }
  .home-chat-scroll::-webkit-scrollbar { width: 0; opacity: 0; background: transparent; }
  /* DISH BRIEF (2026-08-03) -- the optional model-backed starting point.
     It sits between the maker's dish and the restaurant-name question, so
     it must read as reference material, NOT as the thing to answer next.
     It therefore keeps the settled-history surface and uses its own
     .home-brief-action class instead of .home-bubble-cta*, which is what
     keeps the "this is the live module" styling pointing at the
     restaurant-name prompt below it. No AI badge, no sparkle, no
     confidence score -- it earns attention by being useful, not labelled. */
  .home-brief { gap: .3rem; }
  .home-brief-head {
    font-weight: 650; letter-spacing: -.011em; margin-bottom: .1rem;
  }
  .home-brief-line {
    font-size: var(--hc-helper); line-height: 1.5; color: var(--ink-soft);
  }
  .home-brief-label { font-weight: 700; color: var(--ink); }
  /* COMFORT PASS (2026-08-03) -- light internal structure: a quiet
     hairline separates the action row from the content above it (the
     "prompt/helper text -> action area" grouping the packet asked for),
     using the existing low-contrast --hc-line token. Scoped to this one
     class -- it never reaches simple single-line bubbles. */
  .home-brief-actions {
    display: flex; flex-wrap: wrap; gap: .1rem .9rem; margin-top: .4rem;
    padding-top: .5rem; border-top: 1px solid var(--hc-line);
  }
  .home-brief-action {
    padding: .3rem 0; min-height: 32px; border: none; background: none;
    font-family: var(--font); font-size: var(--hc-helper); font-weight: 700;
    color: var(--ink-soft); text-align: left; cursor: pointer;
    text-decoration: underline; text-underline-offset: 3px;
    text-decoration-color: rgba(23,23,23,.2);
  }
  .home-bubble-subtext {
    margin-top: .1rem; color: var(--ink-soft); font-size: var(--hc-helper);
    font-weight: 500; line-height: 1.45;
  }
  /* PRICE CHECK BUBBLE (2026-08-03) -- real, draggable range slider ($1-
     $15, matching the server's own hard cap) in place of the founder's
     ai-price-bubble.svg, which was a static illustration of a slider, not
     a real one (see renderHomePriceInlineControls() in manager.js). Track
     gradient is the same red(max)->amber->green(cheap) language the SVG
     used; thumb is a plain white circle with a hairline border and a
     quiet shadow, no glass/neon. -webkit-/-moz- pseudo-elements are the
     only way to style a native range input's track/thumb -- there is no
     unprefixed standard yet. */
  .home-price-range {
    width: 100%; margin: .2rem 0 0; -webkit-appearance: none; appearance: none;
    background: transparent; height: 28px; cursor: pointer;
  }
  .home-price-range::-webkit-slider-runnable-track {
    height: 8px; border-radius: 999px;
    background: linear-gradient(90deg, #2fa500 0%, #f5c518 50%, #e03131 100%);
  }
  .home-price-range::-moz-range-track {
    height: 8px; border-radius: 999px;
    background: linear-gradient(90deg, #2fa500 0%, #f5c518 50%, #e03131 100%);
  }
  .home-price-range::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none; margin-top: -8px;
    width: 24px; height: 24px; border-radius: 50%;
    background: #fff; border: 1px solid var(--hc-line-strong);
    box-shadow: var(--hc-shadow-active);
  }
  .home-price-range::-moz-range-thumb {
    width: 24px; height: 24px; border-radius: 50%;
    background: #fff; border: 1px solid var(--hc-line-strong);
    box-shadow: var(--hc-shadow-active);
  }
  .home-price-range-scale {
    display: flex; justify-content: space-between; margin-top: -.1rem;
    color: var(--ink-faint); font-size: var(--hc-meta); font-weight: 600;
  }
  .home-price-guidance {
    color: var(--ink-soft); font-size: var(--hc-helper);
    font-weight: 600; line-height: 1.4;
  }
  .home-price-ai-line {
    color: var(--ink-soft); font-size: var(--hc-helper);
    font-weight: 500; line-height: 1.4;
  }
  /* COMFORT PASS (2026-08-03) -- light internal structure: a quiet
     hairline separates the price bubble's guidance copy from its action
     button, mirroring .home-brief-actions above. :has(+ ...) targets
     whichever of guidance/ai-line is the LAST one actually present (only
     an immediate-next-sibling match, not every earlier sibling too), so
     exactly one divider ever renders. */
  .home-price-guidance:has(+ .home-bubble-cta),
  .home-price-ai-line:has(+ .home-bubble-cta) {
    padding-bottom: .5rem; border-bottom: 1px solid var(--hc-line);
  }
  /* Quiet system divider (2026-08-02), not a debug placeholder -- same
     text/content as before (untouched, see homeChatWaitingMarker()), just
     given the flanking-hairline treatment iMessage-style date dividers
     use so it reads as "the app is pausing here," not a leftover string. */
  .home-chat-waiting {
    align-self: stretch; display: flex; align-items: center; gap: .5rem;
    margin: .15rem 0; padding: 0; color: var(--ink-faint);
    font-size: var(--hc-meta); font-weight: 600; letter-spacing: .01em;
    line-height: 1.35; text-align: center;
  }
  .home-chat-waiting::before, .home-chat-waiting::after {
    content: ""; flex: 1; height: 1px; background: rgba(26,14,16,.07);
  }
  .home-chat-waiting-text { flex-shrink: 0; }
  /* HOME INTERACTION ANIMATION PASS (2026-08-02) -- a quiet 3-dot breathing
     pulse next to the divider text, staggered so it reads as a designed
     "still waiting" beat, not a spinner or fake-loading tell. */
  .home-chat-waiting-dots { flex-shrink: 0; display: inline-flex; align-items: center; gap: 3px; }
  .home-chat-waiting-dots i {
    width: 3px; height: 3px; border-radius: 50%; background: currentColor;
    opacity: .35; animation: homeWaitDotPulse 1.4s ease-in-out infinite;
  }
  .home-chat-waiting-dots i:nth-child(2) { animation-delay: .18s; }
  .home-chat-waiting-dots i:nth-child(3) { animation-delay: .36s; }
  @keyframes homeWaitDotPulse {
    0%, 100% { opacity: .3; transform: scale(1); }
    50% { opacity: .9; transform: scale(1.35); }
  }
  @media (prefers-reduced-motion: reduce) {
    .home-chat-waiting-dots i { animation: none; opacity: .55; }
  }
  /* A quiet receipt tag inside the bubble that asked the question, not a
     disabled button -- borderless, low-contrast fill only. */
  /* A completed step's receipt. Raised off --ink-faint to --ink-soft: at
     11px this was previously near-invisible, so a settled turn gave no
     glanceable confirmation of WHAT was completed. 999px is retained --
     it is a genuinely tiny utility chip, the one place a pill belongs. */
  .home-chat-done {
    align-self: flex-start;
    width: fit-content;
    max-width: 100%;
    margin-top: .02rem;
    padding: .17rem .5rem;
    border: none;
    border-radius: 999px;
    background: rgba(26,14,16,.06);
    color: var(--ink-soft);
    font-size: var(--hc-meta);
    font-weight: 700;
    letter-spacing: .005em;
    line-height: 1.3;
    white-space: nowrap;
  }
  /* 16px is not cosmetic here: anything under it makes iOS Safari zoom the
     whole viewport on focus. The global input rule enforces it too; stating
     it locally keeps this declaration honest rather than looking like 13px. */
  .home-bubble-field {
    width: 100%; min-height: 44px; padding: .6rem .7rem;
    border: 1px solid var(--hc-line-strong); border-radius: var(--hc-radius-ctl);
    background: #fff; color: var(--ink); font-family: var(--font); font-size: 16px;
  }
  .home-waitlist-link {
    overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
    padding: .5rem .65rem; border-radius: var(--hc-radius-ctl);
    background: rgba(26,14,16,.035);
    border: 1px solid var(--hc-line); color: var(--ink-soft); font-size: var(--hc-helper);
    font-variant-numeric: tabular-nums;
  }
  /* LIVE FOUNDER FEEDBACK (2026-08-03) -- "0/N" reservation counter in the
     corner of the waitlist bubble (see saveHomeThreadPublishAndInvite()/
     activeHomeRecipeOrders() in manager.js). position:relative scoped to
     this one bubble only, not the shared .home-thread-bubble base class. */
  .home-waitlist-bubble { position: relative; }
  .home-waitlist-goal-badge {
    position: absolute; top: .7rem; right: .8rem;
    padding: .2rem .55rem; border-radius: 999px;
    background: rgba(26,14,16,.05); border: 1px solid var(--hc-line-strong);
    color: var(--ink-soft); font-size: var(--hc-meta); font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  /* Composer polish (2026-08-02) -- built-in control, not floating glass:
     lighter blur/shadow than the prior pass (heavy blur reads as generic
     glassmorphism), near-opaque fill so it stays a stable grounded object
     over the map rather than a translucent panel. Grid/id/behavior
     unchanged. */
  #home-chat-composer {
    flex: 0 0 auto !important;
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) 40px !important;
    align-items: center !important;
    gap: .4rem !important;
    padding: .4rem !important;
    border: 1px solid var(--hc-line-strong);
    border-radius: 14px;
    background: rgba(255,255,255,.97);
    box-shadow: var(--hc-shadow-active);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
  #home-chat-composer input:not([hidden]) {
    min-width: 0;
    height: 40px;
    padding: 0 .8rem;
    border: none;
    border-radius: 999px;
    background: transparent;
    text-align: left !important;
  }
  #home-chat-composer input:focus-visible {
    outline: none;
  }
  /* Idle composer state (2026-08-02) -- a button/select-only stage hides
     the fake disabled input entirely (see homeChatComposerShowIdle() in
     manager.js) and shows this one quiet passive line instead, so the
     grounded area reads as "waiting on purpose," not "broken form
     control." Sits above #home-chat-composer, same column as
     #home-composer-tagline. */
  #home-composer-idle-line {
    flex-shrink: 0; padding: .1rem 0 .05rem; text-align: center;
    font-size: .74rem; font-weight: 600; color: var(--ink-faint);
  }
  /* Rounded square, not a circle: a 50% send button is the single most
     generic chat-app signature, and it was the only circle in a system
     otherwise built on 10-12px corners. --hc-radius-ctl puts it in the
     same family as every other control. */
  #home-chat-composer button:not([hidden]) {
    width: 40px !important;
    min-width: 40px !important;
    height: 40px !important;
    min-height: 40px !important;
    padding: 0 !important;
    border-radius: var(--hc-radius-ctl) !important;
    font-size: 0 !important;
    line-height: 1 !important;
    box-shadow: none !important;
    transition: background-color .16s ease, transform .18s cubic-bezier(.34,1.56,.64,1);
  }
  #home-chat-composer button:not([hidden])::before {
    content: "↑";
    font-size: 1.25rem;
    font-weight: 800;
    line-height: 1;
    display: block;
    transition: transform .16s cubic-bezier(.2,.8,.2,1);
  }
  /* Send-ready state (2026-08-02, pushed further in the art direction pass)
     -- a small confident pop plus the arrow itself nudging up, so the
     moment the button turns actionable actually registers as "ready to
     go," not just a color flip. Same blue-tinted shadow tone the current
     bubble's own shadow now shares, so the composer reads as physically
     connected to the question above it, not a separate control. */
  #home-chat-composer button.composer-ready:not([hidden]) {
    background: #00A6FF !important; border-color: #00A6FF !important; color: #fff !important;
    transform: scale(1.06);
    box-shadow: 0 4px 14px rgba(20,115,230,.34) !important;
  }
  #home-chat-composer button.composer-ready:not([hidden])::before {
    transform: translateY(-1px);
  }
  @media (prefers-reduced-motion: reduce) {
    #home-chat-composer button:not([hidden]),
    #home-chat-composer button:not([hidden])::before { transition: background-color .16s ease; }
    #home-chat-composer button.composer-ready:not([hidden]) { transform: none; }
    #home-chat-composer button.composer-ready:not([hidden])::before { transform: none; }
  }
  /* Primary action rendered inside a Mayoom bubble (Prepare reservations,
     Pay for this slot, Add ingredient) -- smaller/tighter than
     .home-thread-cta's full-page sizing since it now lives inside a
     bubble, not a card or the page's grounded composer. */
  /* ONE ACTION COLOUR (2026-08-03). This was #000 while the composer's send
     button was #00A6FF -- two different "this is the action" colours on the
     same screen, often visible at the same time. Blue is now the single
     actionable-primary across the whole Home surface; black is reserved for
     type, and the maker's own answer chip. Secondary actions stay text
     links and option rows stay white, so blue never becomes decoration. */
  .home-bubble-cta {
    align-self: stretch; min-height: 44px;
    padding: .6rem .9rem; border-radius: var(--hc-radius-ctl); border: 1px solid var(--hc-action);
    background: var(--hc-action); color: #fff; font-family: var(--font);
    font-size: var(--hc-control); font-weight: 700; text-align: center; cursor: pointer;
    letter-spacing: -.01em;
  }
  /* :disabled here is a brief in-flight "saving" state (see e.g.
     saveHomeThreadQuantity's btn.disabled = true while its fetch is
     pending) -- never a permanent completed state, that's
     .home-chat-done's job. A dimmed version of the real button reads as
     "submitting," not "broken/greyed-out form control." */
  .home-bubble-cta:disabled {
    background: rgba(20,115,230,.55); border-color: transparent; color: rgba(255,255,255,.85);
    cursor: default; opacity: 1;
  }
  .home-bubble-option-row:disabled {
    background: rgba(26,14,16,.03); border-color: var(--hc-line); color: var(--ink-soft);
    cursor: default; opacity: 1;
  }
  /* Secondary/optional action inside a Mayoom bubble (Skip for now, Add
     grocery costs) -- plain text-button weight, never competes with the
     bubble's primary CTA above it. */
  .home-bubble-cta-secondary {
    align-self: flex-start; padding: .35rem 0; min-height: 32px; border: none; background: none;
    font-family: var(--font); font-size: var(--hc-helper); font-weight: 700;
    color: var(--ink-soft); text-align: left; cursor: pointer;
    text-decoration: underline; text-underline-offset: 3px;
    text-decoration-color: rgba(23,23,23,.2);
  }
  .home-bubble-cta-secondary:disabled { color: var(--ink-faint); text-decoration-color: transparent; cursor: default; }
  /* A tappable option row inside a Mayoom bubble (a bookable launch slot).
     Clean white row, not a grey dashboard list item -- lighter than
     .home-bubble-cta so a list of these doesn't read as a stack of primary
     buttons. */
  .home-bubble-option-row {
    align-self: stretch; width: 100%; min-height: 44px; text-align: left;
    padding: .6rem .75rem; border-radius: var(--hc-radius-ctl);
    border: 1px solid var(--hc-line-strong);
    background: #fff; color: var(--ink); font-family: var(--font);
    font-size: var(--hc-control); font-weight: 600; line-height: 1.35; cursor: pointer;
    transition: border-color .14s ease, background-color .14s ease;
  }
  .home-bubble-option-row:not(:disabled):active {
    border-color: var(--hc-action); background: rgba(20,115,230,.05);
  }
  /* Home mobile taste pass (2026-07-31) -- the active prompt is the one
     thing on the page that should read as "do this now": a full step up
     in size from a regular bubble, not just bold. */
  .home-thread-prompt { font-weight: 700; font-size: .95rem; line-height: 1.35; }
  /* Compress-into-receipts packet (2026-07-31), retuned in the taste pass
     -- deliberately flatter and quieter than .home-thread-bubble: a
     completed step should read as one settled block of memory, not a
     mini table of separated rows (dropped the old per-line divider,
     tightened line spacing) or another chat turn competing with the
     active step for attention. */
  .home-thread-receipt {
    font-size: .72rem; font-weight: 600; color: var(--ink-soft);
    padding: .12rem 0; line-height: 1.5;
  }
  /* The Day final state (2026-07-31) -- "you made it here": same quiet
     bubble language as everywhere else, just given real presence via the
     app's own existing accent-wash token. No new visual system. */
  #home-thread-day-prompt { font-size: 1.05rem; background: var(--accent-wash); }
  /* Home-thread CTAs were undersized (34px) against this app's own
     .btn-outline default (48px) -- restored to a real, calm tap target
     without the full hero-button weight. Only sizing; the activation
     grey/black look is still driven by JS's own inline background/
     border-color/color, untouched. */
  .home-thread-cta { min-height: 46px; padding: .75rem 1.25rem; text-align: center; }
  /* Home-native stage cards (2026-07-31) -- taste direction: the one
     active stage should read as a small, purpose-shaped card (menu card,
     money card, grocery/cost card, slot card, day card), not a generic
     chatbot bubble. Same soft-rounded/white-surface/black-type family the
     rest of the app already uses (.role-card, .plan-option) -- no new
     color, no gradient, no icon/emoji. Only one ever renders at a time. */
  .home-chat-card {
    display: flex; flex-direction: column; gap: .65rem;
    background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius-md);
    padding: 1.05rem 1.1rem; box-shadow: 0 1px 2px rgba(26,14,16,.04);
  }
  .home-chat-card-label { font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); }
  .home-chat-card-prompt { font-size: .98rem; font-weight: 700; line-height: 1.4; color: var(--ink); }
  /* FINAL HOME TASTE PASS (2026-07-31) -- spacing/copy only, no logic
     changed. THE DAY's fact rows had even, checklist-flat spacing; a
     touch more air between rows and a real gap before "Next" (instead of
     nearly touching it) reads as Mayoom pausing to summarize, then
     pointing forward, rather than a form finishing itself. */
  .home-chat-card-facts { display: flex; flex-direction: column; gap: .32rem; }
  .home-chat-card-fact { font-size: .83rem; color: var(--ink-soft); line-height: 1.45; }
  .home-chat-card-next { font-size: .82rem; font-weight: 700; color: var(--ink); line-height: 1.45; margin-top: .5rem; }

  /* Reactivity pass (2026-07-27): press feedback is near-instant (40ms --
     faster than the ~100ms threshold where a tap starts reading as
     "laggy") and release springs back with a slight overshoot instead of
     just settling, the closest a screen gets to a haptic click. Ripple
     (below) adds the actual "impact" burst on top of the scale. */
  button, .tappable-row, .status-pill, .toggle-switch, .tab-btn {
    transition: transform .04s linear;
  }
  button:active, .tappable-row:active, .status-pill:active, .toggle-switch:active, .tab-btn:active {
    transform: scale(.94);
  }
  button:not(:active), .tappable-row:not(:active), .status-pill:not(:active) {
    transition: transform .32s cubic-bezier(.34, 1.75, .4, 1);
  }
  @media (prefers-reduced-motion: reduce) {
    button, .tappable-row, .status-pill, .toggle-switch, .tab-btn, #savings-total { transition: none !important; }
    button:active, .tappable-row:active, .status-pill:active, .toggle-switch:active, .tab-btn:active { transform: none; }
  }

  /* Tap ripple -- a quick radial burst from the touch point, added/removed
     by JS (rippleOn()) on every button/tappable-row via one delegated
     listener, so it applies to every current AND future one automatically. */
  .ripple-host { position: relative; overflow: hidden; }
  .ripple {
    position: absolute; width: 10px; height: 10px; border-radius: 50%;
    background: rgba(255,255,255,.55); transform: translate(-50%,-50%) scale(0);
    pointer-events: none; animation: ripple-burst .5s ease-out forwards;
  }
  .ripple.dark { background: rgba(0,0,0,.12); }
  @keyframes ripple-burst { to { transform: translate(-50%,-50%) scale(22); opacity: 0; } }

  /* Value pulse -- briefly scales+flashes an element right when its text
     changes (savings figure, counts, status pills), called via pulseEl()
     from wherever that value is set. */
  @keyframes value-pulse { 0% { transform: scale(1); } 35% { transform: scale(1.16); } 100% { transform: scale(1); } }
  .value-pulse { animation: value-pulse .38s cubic-bezier(.34, 1.75, .4, 1); }

  /* Savings count-up celebration (2026-07-28) -- fires only when the real
     savings total goes up (see updateSavingsTotal() in the page script),
     never on a merely-claimed-not-yet-verified request. Two pieces: a green shine
     that sweeps through the actual digit glyphs (background-clip:text,
     not an overlay box) right as the rapid count-up lands, plus a
     "+$X.XX" that pops out of the number's own top-right corner. */
  #savings-total.scan-sweep {
    background-image: linear-gradient(100deg, #fff 30%, #c6ffb0 45%, #2FA500 50%, #c6ffb0 55%, #fff 70%);
    background-size: 250% 100%;
    background-position: 150% 0;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: savings-scan .5s ease-out;
  }
  @keyframes savings-scan {
    from { background-position: 150% 0; }
    to { background-position: -50% 0; }
  }
  .savings-pop {
    position: absolute; top: -.1rem; right: -.5rem; font-size: 1.15rem; font-weight: 800;
    color: #c6ffb0; text-shadow: 0 1px 8px rgba(47,165,0,.65); pointer-events: none;
    animation: savings-pop-float .9s ease-out forwards;
  }
  @keyframes savings-pop-float {
    0% { opacity: 0; transform: translateY(6px) scale(.7); }
    20% { opacity: 1; transform: translateY(-6px) scale(1.15); }
    100% { opacity: 0; transform: translateY(-34px) scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    /* #savings-total.scan-sweep deliberately NOT included here (2026-07-29,
       direct instruction) -- it now also plays on every reload (see
       paintEconomicHeroValue()'s isFirstRender branch), not just on a live
       increase, so it joins the same standing exception this codebase
       already carries for the heartbeat/savings-pulse (the Auto-Plan
       glow this once also listed was removed 2026-07-29):
       those keep animating under reduced-motion too. .savings-pop (the
       "+$X.XX" float) stays suppressed -- that one's semantically a
       celebration of a live increase, which a first render isn't. */
    .savings-pop { animation: none !important; opacity: 0; }
  }

  /* "Copied" pop (2026-07-28) -- appears above the copy button on click,
     floats up and fades, same mechanic as .savings-pop. Anchors to the
     row (position:relative on the row itself), positioned above the
     button rather than the whole row so it reads as coming from the tap. */
  .copy-pop {
    position: absolute; top: -1.6rem; right: 0; font-size: .78rem; font-weight: 700;
    color: #fff; background: rgba(0,0,0,.55); padding: .25rem .6rem; border-radius: 999px;
    white-space: nowrap; pointer-events: none; animation: copy-pop-float .9s ease-out forwards;
  }
  @keyframes copy-pop-float {
    0% { opacity: 0; transform: translateY(6px) scale(.85); }
    20% { opacity: 1; transform: translateY(0) scale(1); }
    100% { opacity: 0; transform: translateY(-14px) scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .copy-pop { animation: none !important; opacity: 0; }
  }

  /* List stagger-in -- rows fade+rise in with a small incremental delay
     (set via inline --i on each row) instead of popping in all at once. */
  @keyframes row-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
  .row-in { animation: row-in .32s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--i, 0) * 35ms); }
  @media (prefers-reduced-motion: reduce) {
    .ripple, .row-in { animation: none !important; }
  }

  form { display: flex; flex-direction: column; gap: .8rem; }
  button { font-family: var(--font); cursor: pointer; }
  .btn-primary { background: var(--accent); color: var(--on-accent); border: none; border-radius: var(--radius-sm); padding: .85rem 1.1rem; font-weight: 700; font-size: .95rem; min-height: 48px; width: 100%; }
  .btn-primary:disabled { opacity: .5; }
  .btn-secondary { background: #fff; color: var(--accent); border: 1px solid var(--accent-light); border-radius: var(--radius-sm); padding: .75rem 1rem; font-weight: 700; font-size: .9rem; min-height: 46px; width: 100%; }
  .tertiary-link { background: none; border: none; color: var(--ink-soft); text-decoration: underline; font-size: .85rem; padding: .4em; align-self: center; }
  /* Shadow-based elevation (2026-07-27), not a heavy outline -- matches
     shopper.html's existing --shadow-card token instead of the flat
     "admin panel" look of a solid gray border on every surface. */
  .card { background: #fff; border: 1px solid rgba(26,14,16,.07); border-radius: var(--radius-lg); padding: 1.1rem; display: flex; flex-direction: column; gap: .75rem; box-shadow: var(--shadow-card); }

  /* For Sale catalog cards (2026-07-29) -- two-up where readable, one
     column on very narrow screens. 300px is the practical floor for a
     2-up grid to still read as two upright cards rather than two
     cramped slivers; below it the grid collapses to one column. */
  .catalog-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
  @media (max-width: 340px) { .catalog-grid { grid-template-columns: 1fr; } }
  .catalog-card { background: #fff; border: 1px solid rgba(26,14,16,.07); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-card); cursor: pointer; display: flex; flex-direction: column; }
  .catalog-card-photo { aspect-ratio: 1 / 1; width: 100%; background: linear-gradient(160deg, var(--surface) 0%, var(--line) 100%); display: flex; align-items: center; justify-content: center; }
  .catalog-card-photo span { font-family: var(--font); font-weight: 800; font-size: 2.4rem; color: var(--ink-faint); }
  .catalog-card-body { display: flex; flex-direction: column; gap: .4rem; padding: .7rem .8rem .85rem; flex: 1; }
  .catalog-card-name { font-weight: 700; font-size: .92rem; color: var(--ink); line-height: 1.25; }
  .catalog-card-price { font-weight: 800; font-size: 1rem; color: var(--ink); }
  .catalog-status-btn { border: none; border-radius: 999px; padding: .4rem .6rem; font-family: var(--font); font-size: .72rem; font-weight: 700; cursor: pointer; text-align: center; min-height: 34px; }
  .catalog-status-btn.for-sale { background: var(--success-wash); color: var(--success); }
  .catalog-status-btn.not-for-sale { background: var(--accent-wash); color: var(--ink-soft); }
  .catalog-status-btn.blocked { background: #fef3e2; color: #c2650a; }
  .catalog-detail-label { font-size: .74rem; font-weight: 700; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .03em; }
  .catalog-photo-slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: .45rem; }
  .catalog-photo-slot { aspect-ratio: 1 / 1; border: 1px dashed var(--line-strong); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: .72rem; font-weight: 700; background: var(--surface); text-align: center; padding:.35rem; }
  .order-alert-dot { display:inline-block; width:7px; height:7px; border-radius:50%; background:#00a200; margin-right:.45rem; vertical-align:middle; animation: order-alert-pulse 1s ease-in-out infinite; }
  @keyframes order-alert-pulse { 0%,100% { opacity:.35; transform:scale(.8); } 50% { opacity:1; transform:scale(1.15); } }
  @media (prefers-reduced-motion: reduce) { .order-alert-dot { animation: none !important; opacity: 1; } }
  .text-btn { background: none; border: none; cursor: pointer; padding: .5rem; color: var(--ink); font-family: var(--font); font-size: .85rem; font-weight: 600; min-height: 44px; }
  .btn-outline { background: #fff; color: var(--ink); border: 1px solid rgba(26,14,16,.12); border-radius: 999px; padding: 1rem 1.25rem; font-weight: 700; font-size: .95rem; min-height: 48px; width: 100%; box-shadow: 0 1px 2px rgba(26,14,16,.04); }
  .status { font-size: .85rem; color: var(--ink-soft); min-height: 1.1em; }
  .status.error { color: var(--danger); font-weight: 600; }

  /* "Plan something" action sheet options (2026-07-28) -- slim, no card
     shadow/stack, per direct instruction ("no new big card stack, no
     scroll-heavy UI"). Plain bordered rows, same weight as a form field. */
  .plan-option { display: flex; flex-direction: column; gap: .25rem; text-align: left; background: #fff; border: 1px solid rgba(26,14,16,.1); border-radius: var(--radius-sm); padding: .9rem 1rem; font-family: var(--font); cursor: pointer; }
  .plan-option-title { font-weight: 700; font-size: .95rem; color: var(--ink); }
  .plan-option-sub { font-size: .8rem; color: var(--ink-soft); line-height: 1.35; }

  /* First-run onboarding (2026-07-28) -- 3 slim full-screen slides, see
     screen-onboard-intro/role/identity. Deliberately plain: slide 1 is
     explicitly "no heavy cards," slide 2's role picker IS cards (per
     spec) but stays flat/bordered, not shadowed, to match. */
  .onboard-slide { padding: 1.5rem 1.25rem; display: flex; flex-direction: column; gap: 1.25rem; height: 100%; overflow-y: auto; }
  .onboard-slide h1 { font-size: 1.5rem; line-height: 1.25; margin: 0; }
  .onboard-feature-list { display: flex; flex-direction: column; gap: .6rem; margin-top: .3rem; }
  .onboard-feature-line { display: flex; align-items: center; gap: .6rem; font-size: .92rem; font-weight: 600; color: var(--ink); }
  .onboard-feature-dot { width: 6px; height: 6px; border-radius: 50%; background: #2FA500; flex-shrink: 0; }
  /* Route visual: Food -> Groceries -> Restock -> Delivery, 4 dots on one
     connecting line -- a single ::before line under the dots, not 4
     separate segments, so it reads as one continuous route. */
  .onboard-route { display: flex; align-items: flex-start; justify-content: space-between; position: relative; padding: 1.4rem .5rem .2rem; }
  .onboard-route::before { content: ''; position: absolute; top: 1.7rem; left: 8%; right: 8%; height: 2px; background: var(--line-strong); }
  .onboard-route-stop { display: flex; flex-direction: column; align-items: center; gap: .5rem; position: relative; z-index: 1; flex: 1; }
  .onboard-route-dot { width: 10px; height: 10px; border-radius: 50%; background: #2FA500; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(47,165,0,.3); }
  .onboard-route-label { font-size: .68rem; font-weight: 600; color: var(--ink-soft); text-align: center; }

  .role-card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
  .role-card { position: relative; text-align: left; background: rgba(255,255,255,.92); border: 1.5px solid var(--line-strong); border-radius: var(--radius-md); padding: .9rem .8rem; display: flex; flex-direction: column; gap: .35rem; cursor: pointer; font-family: var(--font); backdrop-filter: blur(5px); }
  .role-card.selected { border-color: #2FA500; background: #f4faf1; }
  .role-card-radio { position: absolute; top: .7rem; right: .7rem; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--line-strong); background: #fff; box-sizing: border-box; }
  .role-card.selected .role-card-radio { border-color: #2FA500; background: #2FA500; box-shadow: inset 0 0 0 3px #fff; }
  .role-card-name { font-weight: 700; font-size: .92rem; padding-right: 1.4rem; color: var(--ink); }
  .role-card-price { font-size: .74rem; font-weight: 600; color: var(--ink-soft); }
  .role-card-desc { font-size: .74rem; color: var(--ink-soft); line-height: 1.3; }
  .role-card-highlight { font-size: .72rem; font-weight: 700; color: #2FA500; margin-top: .1rem; }
  .onboard-helper { font-size: .78rem; color: var(--ink-soft); margin: 0; }

  .hero-stat { background: #fff; border: 1.5px solid #b8b8b8; border-radius: var(--radius-lg); padding: 1.4rem 1.1rem; color: var(--ink); }
  .hero-stat .big { font-size: 2rem; font-weight: 800; line-height: 1; }
  .hero-stat .label { font-size: .85rem; color: var(--ink-soft); margin-top: .3rem; }

  .item-row { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding: .45rem 0; border-bottom: 1px solid var(--line); }
  .item-row:last-child { border-bottom: none; }
  .item-name { font-weight: 700; font-size: .95rem; }
  .status-pill { font-size: .74rem; font-weight: 700; padding: .3rem .6rem; border-radius: 999px; border: none; cursor: pointer; white-space: nowrap; }
  .status-pill.good { background: var(--success-wash); color: var(--success); }
  .status-pill.running_low { background: #fef3e2; color: #c2650a; }
  .status-pill.out { background: var(--danger-wash); color: var(--danger); }
  .broadcast-btn { background: #2FA500; color: #fff; border: none; border-radius: var(--radius-sm); padding: .5rem .75rem; font-weight: 700; font-size: .75rem; min-height: 34px; white-space: nowrap; }
  .broadcast-btn:disabled { opacity: .35; }
  .item-actions { display: flex; gap: .4rem; flex-shrink: 0; }

  .activity-row { display: flex; flex-direction: column; gap: .3rem; padding: .9rem 0; border-bottom: 1px solid var(--line); }
  .activity-row:last-child { border-bottom: none; }
  .activity-head { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
  #screen-onboard-role {
    position: relative;
    isolation: isolate;
    overflow-x: hidden;
    background:
      linear-gradient(#f4f4f4 1px, transparent 1px),
      linear-gradient(90deg, #f4f4f4 1px, transparent 1px),
      #fff;
    background-size: 42px 42px;
  }
  #screen-onboard-role::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background:
      radial-gradient(circle at 50% 34%, transparent 0 28%, rgba(255,255,255,.7) 56%, #fff 100%),
      linear-gradient(180deg, rgba(255,255,255,.9), transparent 20%, transparent 78%, rgba(255,255,255,.94));
  }
  #screen-onboard-role > :not(.activity-city-bg) { position: relative; z-index: 1; }
  .activity-city-bg {
    position: absolute;
    inset: -10%;
    z-index: 0;
    pointer-events: none;
    perspective: 900px;
    transform-style: preserve-3d;
    overflow: hidden;
    opacity: .65;
  }
  .activity-bg-card {
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: var(--w);
    min-height: var(--h);
    border: 1px solid var(--line-color);
    border-radius: 18px;
    background: rgba(255,255,255,var(--paper));
    color: #222;
    padding: .68rem;
    box-shadow: 0 14px 38px rgba(0,0,0,.06);
    transform-style: preserve-3d;
    animation: activity-card-shuffle var(--dur) cubic-bezier(.42,0,.19,1) var(--delay) infinite;
    opacity: var(--opacity);
  }
  .activity-bg-card::after {
    content: "";
    position: absolute;
    inset: .42rem;
    border: 1px solid rgba(0,0,0,.045);
    border-radius: 12px;
    pointer-events: none;
  }
  .activity-bg-card strong {
    display: block;
    font-size: .72rem;
    line-height: 1.05;
    letter-spacing: -.015em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .activity-bg-card span {
    display: block;
    margin-top: .32rem;
    color: #8a8a8a;
    font-size: .58rem;
    line-height: 1.1;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: .045em;
  }
  .activity-bg-card.best {
    border-color: #111;
    border-width: 1.5px;
    background: #fff;
    opacity: .765;
    box-shadow: 0 18px 50px rgba(0,0,0,.1);
  }
  @keyframes activity-card-shuffle {
    0%, 100% {
      transform: translate3d(0, 0, var(--z0)) rotate(var(--r)) rotateY(0deg) scale(var(--s0));
    }
    38% {
      transform: translate3d(var(--dx), var(--dy), var(--z1)) rotate(calc(var(--r) + var(--rr))) rotateY(178deg) scale(var(--s1));
    }
    64% {
      transform: translate3d(calc(var(--dx) * -.3), calc(var(--dy) * .46), var(--z2)) rotate(calc(var(--r) - 9deg)) rotateY(360deg) scale(var(--s2));
    }
  }
  #tab-activity .activity-row {
    background: rgba(255,255,255,.9);
    border: 1px solid rgba(0,0,0,.08);
    border-radius: 18px;
    padding: .88rem .95rem;
    margin: .5rem 0;
    box-shadow: 0 8px 26px rgba(0,0,0,.066);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
  .activity-bottom-line {
    margin: auto 0 .55rem;
    color: #000;
    font-size: .92rem;
    font-weight: 900;
    letter-spacing: -.015em;
    text-align: center;
  }
  #tab-activity .empty-note {
    background: rgba(255,255,255,.62);
    border: 1px solid rgba(0,0,0,.05);
    border-radius: 18px;
    padding: 1rem;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
  .badge { display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px; padding: .25rem .6rem; font-size: .7rem; font-weight: 700; flex-shrink: 0; }
  .badge.broadcasting { background: var(--accent-wash); color: var(--accent); }
  .badge.claimed { background: var(--accent-ai-wash); color: var(--accent-ai); }
  .badge.awaiting_verification { background: var(--accent-wash); color: var(--accent); }
  .badge.completed { background: var(--success-wash); color: var(--success); }
  .badge.expired { background: #f1eaea; color: var(--ink-soft); }
  .delivery-ledger-group { display: flex; flex-direction: column; }
  .delivery-ledger-group.with-break { margin-top: 1rem; padding-top: .85rem; border-top: 1px solid var(--line); }
  .delivery-ledger-group .catalog-detail-label { margin: 0 0 .4rem; }
  .delivery-ledger-group.done .delivery-ledger-group-inner { opacity: .7; }
  .delivery-ledger-group.done .catalog-detail-label { color: var(--ink-faint); }
  .delivery-ledger-group.done .activity-row { border-bottom-color: rgba(26,14,16,.06); color: var(--ink-soft); }
  .delivery-ledger-group.done .activity-row strong { color: var(--ink-soft); }
  .delivery-ledger-group.done .activity-row .soft,
  .delivery-ledger-group.done .activity-row .math-note { color: var(--ink-faint) !important; }
  .delivery-ledger-group.done .badge {
    background: #f1f1f1 !important; color: var(--ink-faint) !important; border: 1px solid rgba(26,14,16,.04);
  }
  .delivery-ledger-group.done .icon-btn { color: var(--ink-faint); filter: grayscale(1); }
  .verify-photo { width: 100%; border-radius: var(--radius-sm); border: 1px solid var(--line); display: block; }
  .verify-photo-label { font-size: .8rem; color: var(--ink-soft); font-weight: 700; margin: 0 0 .3rem; }
  .verify-geo { font-size: .72rem; color: var(--ink-faint); margin: .2rem 0 0; }
  .math-note { font-size: .82rem; color: var(--success); font-weight: 600; }
  .empty-note { color: var(--ink-soft); font-size: .9rem; padding: 2.5rem 1rem; text-align: center; }
  /* Inner-page operating strip (2026-07-28, "time-to-value" pass) --
     one slim real-data line per inner page (Inventory, Inventory &
     Recipes, Today's Plan, Delivery): a status dot, the real signal, and
     the next real action. Never a card, never a second color accent
     beyond the dot -- Home is cinematic, inner pages are operating
     panels. Status text truncates rather than wrapping (micro-polish
     requirement: never break layout on a critical one-line status). */
  .operating-strip { display: flex; align-items: center; gap: .5rem; padding: .5rem 0; }
  .operating-strip-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
  .operating-strip-status { font-weight: 700; font-size: .84rem; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .operating-strip-action { font-size: .84rem; color: var(--ink-soft); margin-left: auto; white-space: nowrap; flex-shrink: 0; padding-left: .6rem; }


  .tab-bar { position: relative; flex-shrink: 0; display: flex; border-top: none; padding-bottom: env(safe-area-inset-bottom, 0); background: transparent; }

  /* Auto-Stock live glow removed (2026-07-29, global visual cleanup --
     "remove decorative green/red glow language, preserve semantic
     colors"). This used to be a purely decorative pink/green ombre wash
     behind the top bar keyed to Auto-Plan on/off -- not a real status
     pill, not tied to success/error/available/blocked, just mood
     lighting ("manufacturing discomfort" when off, "calm" when on). The
     real Auto-Plan status signal (.heartbeat-line, gated by the same
     body.auto-live class -- see setAutoLiveUI()) is untouched. */
  .top-bar { position: relative; z-index: 60; }

  /* Trending-down cost line (2026-07-28, revised again: direct
     instruction -- the first hand-drawn zigzag was replaced with a real
     4-frame flipbook built from the founder's own reference SVGs. Each
     of the 4 source files shares one identical anchor point
     (0.507812, 0.273438) despite having different auto-cropped
     viewBoxes -- proof they're all frames of ONE animation drawn on the
     same coordinate space, just exported separately. Using a single
     shared viewBox ("0 0 15 13") for all four path elements and their original,
     UNRESCALED coordinates is what lines them up correctly, so the shape
     visibly builds outward from that same fixed top point across frames
     instead of jumping around. #00A200 is the founder's exact requested
     hex. Frames 1-3 (thin stroke) reveal progressively; frame 4 (solid
     fill, the real arrowhead) is the settled resting state, shown for
     most of the loop. Class name/CSS hooks (.heartbeat-line, body.auto-
     live) intentionally left as-is -- purely internal, no migration
     risk. Shows next to each page heading and the Home savings number
     whenever Auto-Plan is on; hidden otherwise. */
  .heartbeat-line { display: none; flex-shrink: 0; transform: scaleY(-1); transform-origin: center; }
  body.auto-live .heartbeat-line { display: inline-flex; }
  .tv-f1, .tv-f2, .tv-f3 {
    fill: none; stroke: #00A200; stroke-width: 1.15; stroke-linecap: round; stroke-linejoin: round; opacity: 0;
  }
  .tv-f4 { fill: #00A200; stroke: none; opacity: 0; }
  /* One shared 2.4s cycle, split into 4 non-overlapping windows -- each
     frame's own @keyframes just says when IT is opaque; everything else
     is implicitly 0. The two keyframe stops a fraction of a percent apart
     (e.g. "8%, 8.4%") make each cut read as a clean swap, not a cross-
     fade, without needing steps() timing. */
  .tv-f1 { animation: tv-f1-seq 2.4s linear infinite; }
  .tv-f2 { animation: tv-f2-seq 2.4s linear infinite; }
  .tv-f3 { animation: tv-f3-seq 2.4s linear infinite; }
  .tv-f4 { animation: tv-f4-seq 2.4s linear infinite; }
  @keyframes tv-f1-seq { 0%, 8% { opacity: 1; } 8.4%, 100% { opacity: 0; } }
  @keyframes tv-f2-seq { 0%, 8% { opacity: 0; } 8.4%, 20% { opacity: 1; } 20.4%, 100% { opacity: 0; } }
  @keyframes tv-f3-seq { 0%, 20% { opacity: 0; } 20.4%, 32% { opacity: 1; } 32.4%, 100% { opacity: 0; } }
  @keyframes tv-f4-seq { 0%, 32% { opacity: 0; } 32.4%, 96% { opacity: 1; } 99%, 100% { opacity: 0; } }
  /* Deliberately NOT covered by the reduced-motion query below (founder,
     2026-07-28): the heartbeat/savings-pulse are the one exception --
     they must keep animating even with reduce-motion on. (The Auto-Stock
     glow this once also listed was removed 2026-07-29.) */

  /* Savings-figure trend arrow (2026-07-28, revised) -- used to force
     white+thicker here specifically since it used to be a heartbeat
     zigzag sitting directly on the green gradient; removed now that the
     direct instruction is a single, fixed #00A200 for every instance of
     this icon everywhere, Home included, no per-context override. */
  /* Mobile density pass (2026-07-30) -- less-chunky bottom nav: smaller
     min-height/padding/icon, all 5 tabs/labels/behavior unchanged. */
  .tab-btn { flex: 1; background: none; border: none; padding: .42rem 0 .46rem; display: flex; flex-direction: column; align-items: center; gap: .2rem; color: var(--ink-faint); font-family: var(--font); font-size: .64rem; font-weight: 700; min-height: 50px; }
  .tab-btn .tab-icon { font-size: 1.12rem; line-height: 1; }
  .tab-btn.active { color: var(--accent); }
  /* Activity always reads in the branded accent color -- not just when it's
     the selected tab -- so it keeps pulling the eye back, per the founder's
     own framing (2026-07-27): the place with live/claimable action should
     never look like a neutral, unvisited tab. */
  .tab-btn-brand { color: var(--accent); }
  /* Home nav floats directly on the map (2026-07-29, direct instruction):
     no frosted veil, no opacity plate, no top break line. The map is the
     background and the icons sit on it. */
  .tab-bar-on-map {
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-top: none;
  }
  .tab-label-row { display: inline-flex; align-items: center; gap: .3rem; }
  .tab-dot { width: 7px; height: 7px; border-radius: 50%; background: #2FA500; box-shadow: 0 0 0 0 rgba(0,0,0,.4); animation: tab-dot-pulse 1.6s ease-out infinite; }
  @keyframes tab-dot-pulse { 0% { box-shadow: 0 0 0 0 rgba(0,0,0,.4); } 70% { box-shadow: 0 0 0 7px rgba(0,0,0,0); } 100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } }
  @media (prefers-reduced-motion: reduce) { .tab-dot { animation: none; } }

  .sheet-backdrop { position: fixed; inset: 0; background: rgba(26,14,16,.35); z-index: 10; display: flex; align-items: flex-end; justify-content: center; }
  .sheet { width: 100%; max-width: 480px; background: #fff; border-radius: var(--radius-lg) var(--radius-lg) 0 0; padding: 1.3rem 1.1rem calc(1.3rem + env(safe-area-inset-bottom, 0)); display: flex; flex-direction: column; gap: .9rem; box-shadow: 0 -8px 32px rgba(26,14,16,.14); }
  .sheet-head { display: flex; justify-content: space-between; align-items: center; }
  /* Receipt-only Orders trigger: the sheet itself stays immediate, while
     the icon gives one small physical acknowledgement of the tap. */
  #orders-cart-btn { transform-origin: center; }
  #orders-cart-btn.orders-tapped { animation: orders-receipt-tap .14s ease-out both; }
  #orders-cart-btn.orders-open { color: var(--ink); }
  @keyframes orders-receipt-tap {
    0% { transform: scale(1); }
    42% { transform: scale(.86); }
    100% { transform: scale(1); }
  }
  #orders-sheet.orders-portal-backdrop {
    background: transparent; z-index: 70; align-items: flex-start; pointer-events: none;
  }
  #orders-sheet .orders-portal {
    pointer-events: auto; position: fixed; top: calc(max(.9rem, env(safe-area-inset-top)) + 52px);
    bottom: calc(56px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%);
    width: 100%; max-width: 480px; background: rgba(255,255,255,.98);
    border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
    border-radius: 0 0 28px 28px; padding: 1rem 1.1rem;
    display: flex; flex-direction: column; gap: .85rem; overflow-y: auto;
    box-shadow: 0 18px 42px rgba(26,14,16,.12);
    animation: orders-portal-in .22s cubic-bezier(.15,1.18,.25,1) both;
  }
  @keyframes orders-portal-in {
    from { opacity: 0; transform: translateX(-50%) translateY(-8px); clip-path: inset(0 0 100% 0); }
    to { opacity: 1; transform: translateX(-50%) translateY(0); clip-path: inset(0 0 0 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    #orders-sheet .orders-portal,
    #orders-cart-btn.orders-tapped { animation: none !important; }
  }
  .field-row { display: flex; gap: .7rem; }
  .field-row > label { flex: 1; }
  .share-box { display: flex; gap: .5rem; align-items: center; }
  .share-box input { font-size: .78rem; color: var(--ink-soft); }
