/* ═══════════════════════════════════════════════════════════════════════
   Dough — Application shell
   ─────────────────────────────────────────────────────────────────────
   The chrome every signed-in page wears: theme variables, the sticky top
   nav, the left rail, the touch drawer and its scrim, the More sheet, the
   profile menu, the bottom tab bar and the SPA transition bar.

   Extracted verbatim from the <style> block that lived in base.html, and
   linked from the position that block occupied — after design-system.css,
   dough.css and categorizing.css, before any page stylesheet (those are
   linked from the content block, which renders later in the document).
   That order is load-bearing: rules here deliberately override the design
   system, and page stylesheets deliberately override these.
   ═══════════════════════════════════════════════════════════════════════ */

  /* ═══════════════════════════════════════════════════════════════════════
     Check App — Global Theme Variables & CSS Overrides
     Themes mirror Odysseus's THEMES object. Switching any theme updates
     --bg / --fg / --panel / --border / --red on :root; every themed
     element reads from those variables.
     ═══════════════════════════════════════════════════════════════════════ */

  :root {
    /* THEMES['light'] — the warm paper look, shown in the picker as "Dough".
       A visitor with no saved preference never runs applyTheme(), so this
       block *is* what they see; keep it in sync with THEMES['light'] and with
       the fallback literal in the <head> init script above.

       Note the keys and the labels do not line up, and deliberately so: the
       key `light` is labelled "Dough" and the key `default` is labelled
       "Light". The labels were renamed; the keys were not, because a key is
       what `localStorage['check-theme']` stores, and changing one would reset
       the theme of every existing visitor. Match on the key, never the
       label. */
    --bg:     #f0ebe3;
    --fg:     #3a332b;
    --panel:  #faf6f0;
    --border: #d4cdc2;
    --red:    #c47d5a;   /* "accent" — copper (red in Odysseus) */
  }

  /* ── Smooth transitions on theme switch ──
     Colour only, and deliberately. Collapsing the rail changes <body>'s left
     padding, and animating that would be the obvious flourish — it was tried
     here, in this rule, because the rule carries `!important` and a separate
     one would have to out-shout it (which then out-shouts the
     `transition: none` test_theme_sweep.py injects to measure end states).

     What it actually did was animate every *other* padding change these
     selectors see, and one of them is not a change a user makes. A page loads
     with `.app-main`'s 1rem, and chat.css — linked from the content block,
     like every page stylesheet here — lands afterwards with
     `padding: 0 !important`. Instant, that is invisible. Over .18s it is a
     page that slides sideways on every load, and CI caught it as
     test_the_sidebar_is_a_drawer_on_a_phone measuring #thread mid-flight at
     2.36px. The rail's own width transition went with it: the rail sliding
     while the content it offsets jumps is worse than both moving at once.

     So the collapse is instant. If it is ever worth animating, animate
     something that is not layout.

     ── Why the duration is a variable and not a value ──
     This transition exists for one event: the moment somebody picks a theme.
     It used to be unconditional and `!important`, which meant every one of
     these selectors carried a 280ms background-color transition for the whole
     session — so a table row lit up a third of a second after the pointer
     reached it, and `!important` left no component able to say otherwise
     (.ds-btn's own 90ms hover was being overridden by this rule).

     Gating it on a custom property rather than on a `html.theme-changing`
     descendant selector keeps the specificity exactly where it was. That
     matters: test_theme_sweep.py injects a matching `transition: none
     !important` after the app's stylesheets to read end states rather than
     mid-transition colours, and it wins on source order at equal specificity.
     Prefixing these selectors with a class would out-specify that injection
     and the sweep would start reading colours mid-fade again.

     Reduced motion needs no extra handling here: the universal
     `transition-duration: .001ms !important` in design-system.css outranks
     this rule's non-important duration. */
  body, nav, main, header, footer,
  input, textarea, select, button,
  table, th, td {
    transition: background-color var(--theme-swap, 0s) ease,
                color            var(--theme-swap, 0s) ease,
                border-color     var(--theme-swap, 0s) ease;
  }
  /* Set by applyTheme() for the length of the swap, then removed. */
  html.theme-changing { --theme-swap: var(--dur-3); }
  /* Don't animate canvas, images, or JS animations */
  canvas, img, video, svg,
  [style*="animation"] { transition: none !important; }

  /* ── Canvas ──
     `body` alone used to carry this, and CSS background propagation meant the
     canvas took its colour from there — but only once this stylesheet had been
     fetched and body existed. Before that the canvas is the user agent's, and
     inside an installed PWA on a phone in dark mode the user agent's is pure
     black. That is the black screen testers photograph on a cold launch: the
     launch image (`_head_splash.html`) hands over, and on a slow connection
     there is a gap before our first paint that we did not own.

     Naming `html` explicitly closes it. The `--bg` it reads is stamped inline
     on the root element by the <head> init script in base.html, which runs
     ahead of this file, so the canvas is the person's own theme rather than a
     default that has to be corrected a moment later. */
  html { background-color: var(--bg); }

  /* ── Body ── */
  body { background-color: var(--bg); color: var(--fg); min-height: 100vh; }

  /* ── Navigation ──
     Sticky and translucent: the page scrolls under it rather than pushing it
     away, which keeps the primary destinations reachable from anywhere on a
     long dashboard. A hairline replaces the drop shadow — at this weight a
     shadow reads as a seam, not as elevation. */
  nav {
    background-color: color-mix(in srgb, var(--panel) 86%, transparent) !important;
    position: sticky;
    top: 0;
    z-index: 100;
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid var(--hairline, var(--border));
  }
  /* Browsers without backdrop-filter get an opaque bar rather than
     unreadable text over scrolling content. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    nav { background-color: var(--panel) !important; }
  }
  /* ── App shell ─────────────────────────────────────────────────────────
     The nav row, its links, the content column and the toast anchor. These
     were Tailwind utility classes until the CDN came out; the widths and
     paddings below are the same values those utilities resolved to, so the
     shell is pixel-identical either side of the change. */

  .nav-inner { max-width: 80rem; margin-inline: auto; padding-inline: 1rem; }
  .nav-row   { display: flex; justify-content: space-between; height: 4rem; }
  /* min-width:0 so the page title can ellipsize rather than push the avatar
     off the right edge — a flex item's default min-width is its content. */
  .nav-left  { display: flex; align-items: center; min-width: 0; }
  .nav-brand-box { flex-shrink: 0; display: flex; align-items: center; }
  .nav-brand {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: 1.25rem;
    font-weight: 650;
    letter-spacing: -.022em;
    color: var(--fg);
  }
  .nav-right {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-shrink: 0;
    margin-left: .75rem;
    white-space: nowrap;
  }

  /* ── The rail ──────────────────────────────────────────────────────────
     The seven primary destinations, standing in a column down the left edge
     rather than lying in a row across the top.

     Why the change: a row is a queue for horizontal space, and this one had
     already lost that argument twice — it could not be revealed until 1024px,
     and the pages it could not fit (Anomalies, Recurring) were folded into
     Insights to buy the width back. A column is paid for in vertical space,
     which a page this tall has to spare, so a destination added next costs a
     row of pixels instead of a seat at the table.

     What it buys beyond room: the label sits beside an icon rather than alone,
     the active destination is a filled plate rather than a 2px underline, and
     the whole thing stays put while <main> is swapped underneath it — which is
     what the SPA layer has always done and what this layout finally shows.

     Fixed rather than in flow, so it does not scroll away on a long dashboard,
     and above the nav's z-index so the sticky bar's translucent panel and its
     hairline stop at the rail's edge instead of running underneath it. */
  :root { --rail-w: 15rem; }
  /* Collapsed: a strip wide enough for a 19px glyph and its plate, and
     nothing else. Set on <html> rather than on the rail because the width is
     read in three places — the rail, the body's offset and the SPA bar — and
     only the root is an ancestor of all of them. `html.rail-collapsed` also
     out-specifies the `:root` above, which is the same element. */
  html.rail-collapsed { --rail-w: 4.5rem; }

  /* All three are revealed by the min-width block below, so they are hidden
     here and nowhere else: a second `display: none` written after that block
     would win on source order and hide them for good. */
  #app-rail { display: none; }
  #primary-nav { display: none; }
  .nav-title { display: none; }

  /* 1024px, unchanged and for the same reason: it is the exact complement of
     the touch block below, which offers the tab bar instead. The rail would
     fit sooner than the row did, but the two navigations must stay
     complements — a width with both, or with neither, is the bug the
     breakpoint move fixed. tests/browser/test_pages.py holds this. */
  @media (min-width: 1024px) {
    #app-rail {
      position: fixed;
      top: 0; left: 0; bottom: 0;
      width: var(--rail-w);
      z-index: 110;
      display: flex;
      flex-direction: column;
      padding: 0 .75rem 1rem;
      background: var(--panel);
      border-right: 1px solid var(--hairline, var(--border));
      overflow-y: auto;
      overscroll-behavior: contain;
    }

    /* The shift, made once on <body> and inherited by everything in normal
       flow — the sticky nav, the content column, and any page that takes the
       column apart around itself.

       That last clause is the whole reason it lives here rather than on the
       nav and <main> individually, which is where it was first written. Two
       pages deliberately undo <main>'s padding to go full-bleed
       (`main.is-chat` in chat.css, `main:has(> .lp)` in landing.css), and both
       do it with `padding: 0 !important` — so a left offset expressed as
       padding on <main> is discarded by exactly the pages that most need it.
       Chat slid under the rail, and five browser tests failed clicking a
       composer the rail was sitting on top of.

       Keyed to the rail's presence rather than set unconditionally: three
       templates replace `chrome_top` with a header of their own — landing,
       privacy, terms — and none of them has a rail. `:has()` is already load-
       bearing in this codebase for the same kind of question (see landing.css).

       #spa-bar is `position: fixed`, so it is laid out against the viewport and
       does not inherit this; it is offset by hand. */
    body:has(#app-rail) { padding-left: var(--rail-w); }
    body:has(#app-rail) #spa-bar { left: var(--rail-w); }

    /* The brand moved into the rail; the bar carries the page's name instead. */
    .nav-brand-box { display: none; }
    .nav-title { display: block; }

    #primary-nav { display: flex; flex-direction: column; gap: 2px; }
  }

  .rail-brand {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-shrink: 0;
    /* 4rem — the nav row's height, so the wordmark and the page title beside
       it sit on one line rather than half a step apart. */
    height: 4rem;
    padding-inline: .5rem;
    font-size: 1.25rem;
    font-weight: 650;
    letter-spacing: -.022em;
    color: var(--fg);
  }

  .nav-title {
    align-self: center;
    font-size: 1.05rem;
    font-weight: 600;
    letter-spacing: -.015em;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .nav-link {
    display: flex;
    align-items: center;
    gap: .7rem;
    white-space: nowrap;
    padding: .55rem .65rem;
    border-radius: var(--r-sm);
    font-size: .9rem;
    font-weight: 500;
    color: color-mix(in srgb, var(--fg) 62%, transparent);
    transition: background-color var(--dur-2) ease, color var(--dur-2) ease;
  }
  .nav-link svg { flex-shrink: 0; }
  .nav-link:hover {
    background: color-mix(in srgb, var(--fg) 7%, transparent);
    color: var(--fg);
  }
  /* The rail is the keyboard path through the app, and it was relying on the
     UA's default ring -- which is a thin dark outline, invisible on the darker
     half of the sixteen themes. --focus-ring carries its own inner band in
     --panel, so it separates from whatever the rail is painted in.
     :focus-visible, so a mouse click does not paint it. */
  .nav-link:focus-visible,
  .rail-brand:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--r-sm);
  }
  /* Ask Dough carries the accent at rest — it is the one destination that is
     an invitation rather than a place. --accent-ink is the accent corrected
     for contrast against the panel, which is what the rail is painted in. */
  .nav-link--accent { color: var(--accent-ink, var(--red)); }
  .nav-link--accent:hover { background: color-mix(in srgb, var(--red) 12%, transparent); }
  /* Set by _updateNav() on the link matching the current path. A tinted plate
     rather than a rule: in a column there is no baseline for an underline to
     sit on, and the plate reads at a glance from the corner of the eye. */
  .nav-link.active {
    background: color-mix(in srgb, var(--red) 15%, transparent);
    color: var(--accent-ink, var(--red));
    font-weight: 650;
  }
  /* Dough is full colour and cannot dim by inheriting currentColor the way the
     outline glyphs beside him do — the same trick the tab bar plays. */
  .nav-link .dough-avatar { --dough-size: 22px; opacity: .75; transition: opacity var(--dur-2) ease; }
  .nav-link:hover .dough-avatar,
  .nav-link.active .dough-avatar { opacity: 1; }

  /* ── "See more" ───────────────────────────────────────────────────────
     The rail's second tier. Its rows are .nav-link like the seven above, so
     an expanded rail reads as one column rather than as a panel that opened
     inside it — the grouping is carried by the summary and the chevron, not
     by giving the members a different shape.

     The marker is removed twice on purpose: `display: flex` from .nav-link
     already drops the disclosure triangle in Chrome and Safari, and Firefox
     needs ::marker cleared as well. Both, or the triangle survives in one
     browser and doubles up with the chevron. */
  #rail-more { margin-top: 2px; }
  #rail-more > summary { cursor: pointer; list-style: none; }
  #rail-more > summary::-webkit-details-marker { display: none; }
  #rail-more > summary::marker { content: ''; }
  #rail-more > summary:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
  .rail-more__chevron { margin-left: auto; opacity: .6; transition: transform var(--dur-2) ease; }
  #rail-more[open] > summary .rail-more__chevron { transform: rotate(180deg); }
  .rail-more__items { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
  /* Padded in from the summary rather than merely listed under it — an
     indent is what says "these belong to that" without a rule or a box. */
  .rail-more__items .nav-link { padding-left: 1.4rem; }

  /* ── The collapse control ──
     A .nav-link like everything above it, so it aligns with the column
     without a second set of paddings to keep in step. margin-top:auto against
     the rail's flex column pushes it to the foot; the margin above it keeps
     it off the last destination when the list is short enough that they would
     otherwise meet. */
  #rail-toggle {
    margin-top: auto;
    padding-top: .55rem;
    width: 100%;
    background: none;
    border: 0;
    font: inherit;
    cursor: pointer;
  }
  #rail-toggle svg { transition: transform var(--dur-2) ease; }

  /* ── Collapsed ─────────────────────────────────────────────────────────
     Same rows, same order, same one click each — the label moves into the
     tooltip that _syncRail() writes, because a glyph alone is a rebus.
     Everything here is a reversal of a horizontal decision: the gap, the
     indent and the trailing chevron all exist to place a label that is no
     longer rendered. */
  html.rail-collapsed #app-rail { padding-inline: .5rem; align-items: stretch; }
  html.rail-collapsed .rail-label { display: none; }
  html.rail-collapsed .rail-brand,
  html.rail-collapsed .nav-link {
    justify-content: center;
    gap: 0;
    padding-inline: 0;
  }
  html.rail-collapsed .rail-more__items .nav-link { padding-left: 0; }
  html.rail-collapsed .rail-more__chevron { display: none; }
  /* Mirrored: the panel it draws is now the one that would open. */
  html.rail-collapsed #rail-toggle svg { transform: scaleX(-1); }

  /* The content column. py-6 px-4, then sm:px-6 and lg:px-8, as it was. */
  .app-main { max-width: 80rem; margin-inline: auto; padding: 1.5rem 1rem; }
  @media (min-width: 640px)  { .app-main { padding-inline: 1.5rem; } }
  @media (min-width: 1024px) { .app-main { padding-inline: 2rem; } }

  /* The toast anchor. Bottom-right on desktop; the touch block below re-pins
     it above the tab bar. */
  #toast-container {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 50;
    display: flex;
    flex-direction: column;
    gap: .5rem;
    pointer-events: none;
    max-width: 20rem;
  }

  /* ════════════════════════════════════════════════════════════════════
     Profile / Settings Dropdown  (account, theme, connections, rules, …)
     ════════════════════════════════════════════════════════════════════ */

  #profile-menu-wrap { position: relative; }

  #profile-btn {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--red);
    /* --accent-on is stamped by CheckScheme for exactly this: text sitting
       directly on the accent, which several themes make too pale for white. */
    color: var(--accent-on, #fff);
    font-weight: 700;
    font-size: .8rem;
    letter-spacing: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    cursor: pointer;
    flex-shrink: 0;
    transition: transform var(--dur-1), box-shadow var(--dur-2);
  }
  #profile-btn:hover { transform: scale(1.07); box-shadow: var(--e-2); }

  #profile-menu {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    width: 280px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--e-4);
    z-index: 9999;
    padding: 6px 0;
    display: none;
    animation: tp-enter var(--dur-2) ease-out both;
    /* Bounded and scrollable.  [Phase 10.5] This menu carries five links, a
       16-swatch theme grid and Sign out, and it had no height limit — so on a
       short viewport the last item sat below the fold with no way to reach it.
       Adding "Account & security" is what pushed it over, and
       tests/browser/test_auth_journey.py::test_signing_out_ends_the_session
       caught it as "element is outside of the viewport": the sign-out button
       was unreachable, which means there was no way to sign out at all.

       The cap is the space between the nav and the bottom of the window. The
       menu opens at `100% + 10px` below a 4rem nav, so 84px is that offset;
       anything taller scrolls inside itself rather than off the screen.

       `overscroll-behavior: contain` stops a scroll that reaches the end of the
       menu from continuing into the page behind it, which on a touch device
       reads as the menu dragging the whole page around. */
    max-height: calc(100vh - 84px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  #profile-menu.open { display: block; }
  @keyframes tp-enter {
    from { opacity:0; transform:translateY(8px) scale(.97); }
    to   { opacity:1; transform:translateY(0) scale(1); }
  }

  /* The header names the account the way the rest of the world does: the mark
     you clicked, repeated at rest beside who it belongs to, so the menu
     confirms *whose* settings these are before offering to change any. */
  .pm-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px 10px; }
  .pm-avatar {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--red);
    color: var(--accent-on, #fff);
    font-weight: 700;
    font-size: .8rem;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  /* min-width:0 so a long address ellipsizes instead of widening the menu. */
  .pm-ident  { min-width: 0; }
  .pm-name   { font-weight: 600; font-size: .92rem; color: var(--fg); }
  .pm-sub    {
    font-size: .72rem;
    color: color-mix(in srgb, var(--fg) 45%, transparent);
    margin-top: 1px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .pm-divider { border-top: 1px solid var(--border); margin: 6px 0; }
  .pm-section-title {
    padding: 6px 16px 6px;
    font-size: .66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: color-mix(in srgb, var(--fg) 45%, transparent);
  }
  .pm-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 16px;
    font-size: .86rem;
    font-weight: 500;
    color: color-mix(in srgb, var(--fg) 82%, transparent);
    text-decoration: none;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
  }
  .pm-item:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); }
  .pm-item.danger { color: var(--danger-ink, #dc2626); }
  .pm-item svg { flex-shrink: 0; opacity: .7; }

  /* ── The theme group ──
     This was the Settings disclosure: one entry that opened onto Account &
     security, Household and the theme. The first two moved down to the rail's
     "See more" — a page belongs in the column of pages — and what is left is
     not worth hiding behind a summary, so the `#pm-settings` <details> went
     with them along with its marker-removal rules and chevron.

     The indent stays. It reads as a titled section of a short menu rather
     than as a second list of items at the same level as Sign out. */
  .pm-group { padding-bottom: 6px; }
  .pm-group .pm-section-title { padding-left: 30px; }
  .pm-group #theme-grid { padding-left: 24px; }

  /* Theme swatch grid */
  #theme-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
    padding: 0 10px 8px;
  }

  .t-swatch {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 5px 3px;
    border-radius: var(--r-sm);
    transition: background var(--dur-2);
  }
  .t-swatch:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); }

  .t-swatch-preview {
    width: 42px;
    height: 42px;
    border-radius: var(--r-sm);
    border: 2px solid;
    overflow: hidden;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    transition: box-shadow var(--dur-2);
    position: relative;
  }
  .t-swatch.active .t-swatch-preview {
    box-shadow: var(--focus-ring);
  }

  .t-sw-bg     { grid-column: 1 / -1; grid-row: 1; }
  .t-sw-accent { grid-column: 1; grid-row: 2; }
  .t-sw-fg     { grid-column: 2; grid-row: 2; opacity: .55; }

  /* Active check */
  .t-swatch.active .t-swatch-preview::after {
    content: '✓';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: .75rem;
    font-weight: 800;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0,0,0,.7);
    background: rgba(0,0,0,.18);
  }

  .t-swatch-name {
    font-size: .6rem;
    font-family: ui-monospace, 'Courier New', monospace;
    color: var(--fg);
    opacity: .6;
    text-align: center;
    white-space: nowrap;
  }

  /* ── The touch drawer (the rail, for a phone) ─────────────────────────
     What used to be a "More" bottom sheet hanging off a fifth tab. It is a
     left drawer now, opened by the hamburger at the head of the nav row —
     the shape every phone app with more destinations than tab slots has
     settled on, and the one the tab bar was fighting.

     Why the sheet had to go rather than merely be restyled: a fifth tab
     labelled "More" spends a permanent slot on a place that does not exist,
     and it spends it in the row where the *real* destinations live, so the
     bar's own vocabulary is four places and one filing cabinet. Moving the
     cabinet to the top-left corner gives the row back to places and gives
     the overflow a panel with room to name its groups.

     Still shown and hidden with the native [hidden] attribute (design-
     system.css makes it win), so the closed drawer is closed to a screen
     reader too; .mm-shown is what slides it, one frame later — see
     openMobileMenu(). The min-width rule keeps it off desktop, where the
     rail already carries all of this, even if a resize catches it open. */
  #mobile-menu { background-color: var(--panel); }
  @media (min-width: 1024px) {
    #mobile-menu, #mobile-scrim, #nav-menu-btn { display: none !important; }
  }
  .mm-body  { padding: 12px 12px 8px; }

  /* The panel takes focus when it opens, so a screen reader lands inside it
     and the next Tab walks the destinations. It is a container, not a
     control, and design-system.css draws its one focus ring on anything
     focusable — so the ring is turned off here rather than being allowed to
     draw a box around the whole drawer. Nothing is lost: the thing focus is
     announcing is the panel that just appeared. */
  #mobile-menu:focus, #mobile-menu:focus-visible { outline: none; box-shadow: none; }

  /* The drawer's head: the same brand mark as the rail's, so the panel that
     replaces the rail on a phone opens with the thing the rail opens with.

     It stops short of the close button rather than running under it. As a
     flex container it is block-level, so at full width its hit area sat
     beneath the ✕ — two tap targets in one place, and the wrong one easier
     to hit with a thumb.

     The margin below it is the head's separation from the first group. There
     is no rule under it and there should not be: the drawer already carries
     one above "Manage", and a second would divide a seven-row panel into
     three parts. */
  .mm-brand {
    display: flex;
    align-items: center;
    gap: .5rem;
    height: 3.25rem;
    margin-right: 46px;
    margin-bottom: 14px;
    padding-inline: 12px;
    font-size: 1.2rem;
    font-weight: 650;
    letter-spacing: -.022em;
    color: var(--fg);
    text-decoration: none;
  }
  .mm-close {
    position: absolute;
    /* Centred on the brand row beside it: 12px of body padding plus half of
       the row's 3.25rem, less half the button's own height. */
    top: calc(12px + 1.625rem - 19px + env(safe-area-inset-top));
    right: 10px;
    width: 38px;
    height: 38px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: none;
    border: 0;
    cursor: pointer;
    color: color-mix(in srgb, var(--fg) 55%, transparent);
  }
  .mm-close:active { background: color-mix(in srgb, var(--fg) 10%, transparent); }

  .mm-title {
    padding: 0 12px 4px;
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: color-mix(in srgb, var(--fg) 38%, transparent);
  }
  .mm-title--rule { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 12px; }
  .mm-link {
    display: flex;
    align-items: center;
    gap: .75rem;
    width: 100%;
    text-align: left;
    padding: 8px 12px;
    border-radius: var(--r-sm);
    font-size: 1rem;
    font-weight: 500;
    color: color-mix(in srgb, var(--fg) 72%, transparent);
    background: none;
    border: 0;
    cursor: pointer;
    text-decoration: none;
  }
  .mm-link svg { flex-shrink: 0; opacity: .72; }
  .mm-link:active { background: color-mix(in srgb, var(--fg) 8%, transparent); }
  /* The same tinted plate the rail uses for the current page, written by
     _updateNav(). A drawer that does not say where you are is a list of
     places you might be. */
  .mm-link.active {
    background: color-mix(in srgb, var(--red) 15%, transparent);
    color: var(--accent-ink, var(--red));
    font-weight: 650;
  }
  .mm-link.active svg { opacity: 1; }

  /* ── The hamburger ────────────────────────────────────────────────────
     Head of the nav row, left of the brand, at every touch width. It is the
     only control in the chrome that opens a *place* rather than a setting,
     which is why it sits opposite the avatar rather than beside it.

     It takes the accent when the page you are on lives inside the drawer —
     otherwise Investments, Goals and Insights light nothing at all now that
     the "More" tab is gone, and the chrome claims you are nowhere. */
  #nav-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-left: -8px;
    margin-right: 2px;
    flex-shrink: 0;
    border: 0;
    border-radius: var(--r-sm);
    background: none;
    cursor: pointer;
    color: color-mix(in srgb, var(--fg) 70%, transparent);
  }
  #nav-menu-btn:active { background: color-mix(in srgb, var(--fg) 8%, transparent); }
  #nav-menu-btn.active { color: var(--accent-ink, var(--red)); }

  /* The scrim. A sibling element rather than a shadow on the drawer because
     it is the tap target that closes it — the largest one on the screen, and
     the gesture people try first. */
  #mobile-scrim {
    position: fixed;
    inset: 0;
    z-index: 9994;
    background: rgba(0,0,0,.45);
    opacity: 0;
    transition: opacity var(--dur-3) ease;
    -webkit-tap-highlight-color: transparent;
  }
  #mobile-scrim.mm-shown { opacity: 1; }

  /* ── Bottom tab bar (phones only) ── */
  #tab-bar { display: none; }

  /* ── Touch / narrow layout ────────────────────────────────────────────────
     1023.98px, and the number is not arbitrary: it is the exact complement of
     Tailwind's `lg:` (min-width 1024px), which is what reveals the desktop nav
     row below. The two have to be complements or there is a band of widths
     with no navigation that fits.

     It used to be 640px, and that band existed: from 641px to about 880px the
     seven-link desktop nav was shown but did not fit, so *every page in the
     product* scrolled sideways at iPad-portrait width. The bottom tab bar and
     the "More" sheet cover the same destinations and fit at any width, so the
     fix is to keep using them until the desktop row genuinely has room.

     chat.css's PHONE / TOUCH block carries the same number and must keep
     carrying it — it reserves --chat-tabbar-h for the bar this block shows.
     When they disagreed, 641-860px reserved 54px for a bar that was not there.
     tests/test_ui_invariants.py::test_the_touch_breakpoint_agrees_across_files
     holds them together. */
  @media (max-width: 1023.98px) {
    * { -webkit-tap-highlight-color: transparent; }

    /* ## The other blue rectangle  [UAT round 3]

       The rule above takes off the flash iOS paints on a tap. It does not
       touch the two things a *slow* tap gets, and those are what a reader
       actually reports: hold a control a moment too long and iOS starts a
       text selection in it -- a blue block over the label -- and a firmer
       press raises the callout menu over the top. Reported as "the Apple
       blue highlight thing appears; if I press down a little too hard,
       that's a problem".

       Neither is a tap highlight and neither is `touch-action`'s to prevent;
       they are separate opt-outs, and the reason all three want stating
       together is that they are one thing to a person holding the phone: the
       control answered a press with an OS artifact instead of with itself.

       Controls only, never a blanket `*`. Selecting text is how somebody
       copies a balance out of this app, and a rule that took it off the
       document would be the same mistake one level up: fixing a press by
       breaking a read. A `<button>`'s label is not text anybody copies. */
    button,
    summary,
    [role="button"],
    .ds-btn,
    .ds-pressable {
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      user-select: none;
    }

    /* The tab bar's height was written out as a literal in four places, so
       growing Dough's tab by a pixel silently desynced the bottom sheet and
       the scroll padding from the bar they sit above. One number now. */
    :root { --tabbar-h: 58px; }

    /* 16px inputs stop iOS Safari/WebView from auto-zooming on focus */
    input[type="text"], input[type="number"], input[type="email"],
    input[type="password"], input[type="date"], input[type="search"],
    select, textarea { font-size: 16px !important; }

    /* These three were written when this block stopped at 640px, where
       "edge to edge" and "phone width" were the same thing. They are not the
       same thing at 1000px, so each one now caps its width and stays on the
       side it was already on. On a phone the cap is wider than the screen, so
       nothing about the phone layout changes. */
    #profile-menu {
      position: fixed;
      top: 64px;
      right: 8px;
      left: 8px;
      width: auto;
      max-width: 380px;
      margin-left: auto;   /* left+right+max-width is over-constrained; pin it right */
      /* Restated because `top` differs here: fixed at 64px rather than offset
         from the nav, so the cap has to be measured from that instead. `dvh`
         because this is the viewport where the browser's own chrome shows and
         hides — `vh` on a phone is the *largest* viewport, so a menu sized with
         it is exactly one URL-bar taller than the screen it is on. */
      max-height: calc(100dvh - 76px);
    }
    #toast-container {
      left: 1rem;
      right: 1rem;
      max-width: 420px !important;
      margin-left: auto;
      bottom: calc(var(--tabbar-h) + 14px + env(safe-area-inset-bottom)) !important;
    }
    /* Keep wide content scrollable instead of stretching the page */
    main {
      overflow-x: hidden;
      /* room for the fixed tab bar (!important beats .app-main's padding) */
      padding-bottom: calc(var(--tabbar-h) + 30px + env(safe-area-inset-bottom)) !important;
    }

    /* Fixed bottom tab bar — primary nav within thumb reach */
    #tab-bar {
      display: flex;
      justify-content: center;
      position: fixed;
      left: 0; right: 0; bottom: 0;
      z-index: 9990;
      background: var(--panel);
      border-top: 1px solid var(--border);
      padding-bottom: env(safe-area-inset-bottom);
      /* Not on the --e-* scale, and cannot be: every rung casts downward
         (positive y) because that is what elevation means. This bar sits at
         the bottom of the viewport and lifts the page off itself upward. */
      box-shadow: 0 -2px 16px rgba(0,0,0,.10);
    }
    .tab-item {
      /* Capped so five tabs do not stretch to 200px each on a tablet; the
         group then centres itself in the bar. On a phone each tab is well
         under the cap, so this is inert there. */
      flex: 1 1 0;
      max-width: 132px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 3px;
      padding: 8px 0 6px;
      min-height: var(--tabbar-h);
      font-size: .62rem;
      font-weight: 600;
      letter-spacing: .02em;
      color: color-mix(in srgb, var(--fg) 45%, transparent);
      text-decoration: none;
      background: none;
      border: none;
      cursor: pointer;
    }
    .tab-item.active { color: var(--red); }
    .tab-item svg { flex-shrink: 0; }

    /* [Phase 5] The most-tapped control in the product had no press state at
       all. It is not a .ds-btn, so none of the design system's press rules
       ever reached it, and it is only ever shown on touch -- where there is no
       :hover to have covered for that. Tapping Home and waiting for a fetch
       gave back nothing, on the one surface with no haptic to fall back on
       (ADR-0016 §6).

       Scaling the glyph and label rather than the tab: the tabs are laid out
       `flex: 1 1 0` in a fixed bar, so scaling the item itself would move its
       neighbours' rendering within a row that must stay rock-steady under a
       thumb. */
    .tab-item > * {
      transition: transform var(--dur-1) var(--ease-out);
    }
    .tab-item:active > * { transform: scale(.9); }

    /* Dough's tab can't dim by inheriting currentColor the way the outline
       glyphs do — he is full colour. Fading the whole avatar gives him the
       same inactive/active reading as its neighbours.

       He runs a few pixels larger than the 21px outline glyphs beside him on
       purpose: he is the "get help" affordance and the only full-colour mark
       on the bar, and at 21px the ears stop resolving. --tabbar-h is sized to
       hold him. */
    /* Both transitions, because `transition` is a shorthand and this rule is
       more specific than the `.tab-item > *` press rule above -- declaring
       only opacity here would reset the transform's transition to 0s and make
       Dough's tab the one tab in the bar that snaps under a thumb instead of
       easing. */
    .tab-dough .dough-avatar {
      --dough-size: 28px;
      opacity: .6;
      transition: opacity var(--dur-2) ease,
                  transform var(--dur-1) var(--ease-out);
    }
    .tab-dough.active .dough-avatar { opacity: 1; }

    /* While the keyboard is up, hide the tab bar (ChatGPT-style focused compose) */
    html.kb-open #tab-bar { display: none; }
    html.kb-open main { padding-bottom: 16px !important; }

    /* ── The drawer, slid in from the left edge ──
       Above the tab bar rather than docked on top of it (9995 > 9990): it is
       full-height now, so the bar is one of the things it covers. The scrim
       sits between them at 9994 and dims both.

       Width is a fraction of the screen with a cap, so a sliver of the page
       stays visible behind it — that sliver is what tells a first-time user
       the panel is a layer over the page rather than a new page, and it is
       the second thing they tap to dismiss.

       `dvh`, not `vh`: on a phone the browser's own chrome shows and hides,
       and a drawer sized in `vh` is exactly one URL-bar taller than the
       screen it is on — the last row would sit below the fold with nothing
       to scroll. Same reasoning as #profile-menu's cap above. */
    #mobile-menu {
      position: fixed;
      top: 0;
      left: 0;
      height: 100dvh;
      width: min(82vw, 320px);
      z-index: 9995;
      border-right: 1px solid var(--border);
      /* Sideways, for the same reason as the tab bar's: the drawer enters
         from the left edge and throws its shadow across the page, which no
         downward-casting --e-* rung expresses. */
      box-shadow: 6px 0 40px rgba(0,0,0,.30);
      overflow-y: auto;
      overscroll-behavior: contain;
      padding-top: env(safe-area-inset-top);
      padding-bottom: calc(16px + env(safe-area-inset-bottom));
      transform: translateX(-101%);
      transition: transform var(--dur-3) var(--ease-drawer);
    }
    #mobile-menu.mm-shown { transform: none; }
    #mobile-menu .mm-link { min-height: 44px; }

    /* The page behind a modal layer must not scroll — on a phone a drawer
       that drags the page around underneath it reads as a bug. Scoped to
       the touch block rather than set on <html> globally, so a resize to
       desktop while it is open releases the lock without any JS to do it. */
    html.mm-open, html.mm-open body { overflow: hidden; }
  }

  @media (prefers-reduced-motion: reduce) {
    #mobile-menu { transition: none; }
    #mobile-scrim { transition: none; }
    /* Removed, not shortened. Zeroing the duration leaves the tab glyph
       scaled -- it just gets there in one frame, which is the jump this
       setting exists to prevent. */
    .tab-item:active > * { transform: none; }
  }

  /* SPA transition bar */
  /* Scaled, not widened. `width` is a layout property, so the old rule ran
     layout on every frame of a bar that appears on every navigation -- the one
     animation in the app guaranteed to be on screen while the main thread is
     busiest. `right: 0` replaces the `width: 0` the transform now does, and
     keeps the rail offset below working: left moves in, right stays put. */
  #spa-bar { position:fixed;top:0;left:0;right:0;height:2px;background:var(--red);transform:scaleX(0);transform-origin:left;opacity:0;transition:transform var(--dur-3) ease,opacity var(--dur-3);z-index:9999;pointer-events:none }
  /* Opacity only. This used to lift the whole page 6px on every SPA
     navigation, which is movement that communicates nothing: the visitor asked
     to go somewhere, and the answer arriving *and then sliding into place* puts
     motion between them and what they asked for. The cross-fade stays because
     it does say something -- the content under the same chrome is now different
     content -- but the translate was decoration, and it was also the one thing
     on the page that shifted layout after paint. */
  @keyframes fadeSlideIn { from{opacity:0} to{opacity:1} }
  .spa-loaded { animation:fadeSlideIn var(--dur-2) ease-out }

  /* ── While a View Transition is running  [Phase 5] ──────────────────────
     The bar and the transition describe the same event, and running both
     says it twice: the bar's job is "something is happening you cannot see
     yet", and once the cross-fade starts the visitor can see it. Worse, the
     bar was painting *over* the swap it was describing.

     `:active-view-transition` matches <html> for exactly the span of the
     transition and stops on its own, so nothing has to remember to undo
     this -- which matters, because the transition can also be skipped, and
     a JS-toggled class would have to handle that case too.

     Baseline newly available (October 2025). A browser without it keeps
     today's behaviour, which is also what a browser without
     startViewTransition() gets: no transition to collide with. */
  html:active-view-transition #spa-bar { opacity: 0; }

  /* Same reasoning, other direction. .spa-loaded fades the new <main> in --
     but inside a transition the cross-fade is already doing that, and two
     opacity ramps over the same pixels read as a stutter rather than as one
     movement. */
  html:active-view-transition .spa-loaded { animation: none; }
