/* ==========================================================================
   GLOSS — mobile.css
   The small-screen layer for every page. Loaded LAST, after atelier.css
   (index / login) and style.css + story.css (story / heart), so equal
   specificity wins.

   EVERY rule in this file lives inside a media query. Desktop is untouched.

   Two query shapes are used, and the difference matters:

     A. (max-width: 820px), (max-width: 900px) and (max-height: 480px)
        and (orientation: landscape)
        The PATH PAGE frame. A landscape phone is 852x393 — too wide for
        820px, so it is caught by height instead.

     B. (max-width: 820px)
        Everything whose behaviour is INTENDED to be mirrored by a
        matchMedia() call in app.js / story.js (the whole-map modal, the
        story acts, the heat grid).
        NOTE (audit 2026-08): no such matchMedia exists yet. There is no
        MOBQ in app.js and no PHONE test in story.js. When they are added
        this string must stay byte-identical to them. Every rule that
        depended on that JS has been gated or neutralised below so the
        pages are correct without it.

   Then (max-width: 420px) for the narrow tier, and a reduced-motion block.

   SCOPING CONTRACT — mobile.css is loaded on four pages that share class
   names but not stylesheets. Nothing here starts with a bare .stage,
   .topbar, .brand, .main-nav, .theme-btn, .who-btn or .p-btn:
     index    -> .app-shell …, #stage, .workspace, #builder, #map, #job-tray
     story    -> .shell header.topbar, #act-spine, .act, .act-pin
     heart    -> .shell header.topbar, #heart
     login    -> .signin-wrap, .signin
   ========================================================================== */


/* ==========================================================================
   A · THE PATH PAGE  (index.html)
   Portrait phones and small tablets, plus short landscape phones.
   ========================================================================== */
@media (max-width: 820px),
       (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {

  :root {
    /* 0 until the .pane-tabs element actually exists — otherwise #job-tray
       floats 68px above nothing */
    --tabbar-h: 0px;
    /* both written by the visualViewport script in index.html; each has a
       working default, so every rule below still resolves without the JS */
    --kbd: 0px;
  }
  @supports selector(:has(*)) {
    :root:has(.pane-tabs) { --tabbar-h: calc(58px + env(safe-area-inset-bottom, 0px)); }
  }

  html { -webkit-text-size-adjust: 100%; }
  /* overflow-x lives on html ONLY. Putting it on body makes body its own
     scroll container, which stops window scroll events (story.js listens on
     window and measures document.body.scrollHeight) and breaks the
     position:sticky pinning of .act-pin. */
  html { overflow-x: hidden; }
  body { max-width: 100%; }

  /* ---------------------------------------------------------------- topbar
     Desktop's `minmax(240px,1fr) auto minmax(200px,1fr)` grid needs ~815px
     and is what pushes the whole document sideways on a phone. It becomes a
     wrapping flex: brand + account on row 1, nav pills on row 2, search on
     row 3. Scoped to .app-shell — story/heart have their own topbar in a
     different stylesheet, handled in section D. */
  .app-shell .topbar {
    height: auto; max-width: none; margin: 0;
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 8px 10px;
    padding: 8px 14px 10px;
    padding-top: max(8px, env(safe-area-inset-top, 0px));
    border-bottom: 1px solid var(--line);
    flex: none;
  }
  .app-shell .brand { flex: 1 1 auto; min-width: 0; gap: 8px; order: 0; }
  .app-shell .brand strong { font-size: 26px; }
  .app-shell .brand sup { font-size: 11px; }
  .app-shell .brand em { display: none; }   /* the tagline costs a whole row */

  /* .search-box is a CHILD of .top-right in the markup, not a sibling — the
     row-3 rule below only works if the wrapper stops being a box. Its children
     then become direct flex items of .topbar and keep document order
     (brand 0 · nav 2 · search 3 · who/theme implicit 0 → row 1). */
  .app-shell .top-right { display: contents; }
  .app-shell .who-btn,
  .app-shell .theme-btn { flex: none; width: 44px; height: 44px; }

  .app-shell .main-nav {
    order: 2; flex: 1 0 100%;
    justify-content: flex-start; gap: 4px;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .app-shell .main-nav::-webkit-scrollbar { display: none; }
  .app-shell .main-nav a {
    flex: 0 0 auto;
    min-height: 44px; padding: 10px 12px;
    font-size: 12.5px; gap: 6px; white-space: nowrap;
  }

  .app-shell .search-box {
    order: 3; flex: 1 0 100%;
    width: auto; height: 44px; padding: 0 14px;
  }
  /* anything under 16px makes iOS Safari zoom the page on focus */
  .app-shell .search-box input { font-size: 16px; }

  /* the search belongs to the unit list; reclaim its row on the other panes.
     Pure enhancement — without :has() the row is simply always there. */
  @supports selector(:has(*)) {
    .app-shell:has(.workspace[data-pane]:not([data-pane="units"])) .search-box { display: none; }
  }

  /* ------------------------------------------------------- the app frame
     The document does not scroll; each pane scrolls itself. Gated on
     body.page-path so heart/login keep normal page scroll, and --vvh comes
     from visualViewport because iOS does not shrink dvh for the keyboard.
     Each height declaration is a fallback for the one below it.
     NOT IN THE MARKUP YET — body carries no page-path class and nothing
     writes --vvh, so this whole block is inert and index.html still scrolls
     the document normally. Harmless, but the frame is not live. */
  /* Pinned, not merely clipped. position:fixed means the window has nothing
     to scroll, so iOS cannot shove the page up when the keyboard opens; the
     frame simply becomes shorter. --vvh/--vvtop come from visualViewport. */
  body.page-path {
    position: fixed;
    top: var(--vvtop, 0px); left: 0; right: 0;
    min-height: 0;
    height: 100vh;
    height: 100dvh;
    height: var(--vvh, 100dvh);
    overflow: hidden;
    overscroll-behavior: none;
  }
  body.page-path .app-shell {
    min-height: 0;
    height: 100vh;
    height: 100dvh;
    height: var(--vvh, 100dvh);
    height: var(--vvh, 100dvh);
    padding: 0;
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
    overflow: hidden;
  }

  /* ---------------------------------------- fallback until [data-pane] lands
     atelier only stacks the workspace at <=780px; between 781 and 820 it is
     still the 2-column grid with `.unit-card { display: none }`, so gates,
     sources and actions are unreachable in that band. This self-retires the
     moment the pane markup is added. */
  .workspace:not([data-pane]) {
    display: flex; flex-direction: column; gap: 14px;
    height: auto; max-width: none;
  }
  .workspace:not([data-pane]) > .library { max-height: 300px; }
  .workspace:not([data-pane]) > #stage { min-height: 480px; }
  .workspace:not([data-pane]) > .unit-card { display: block; }

  /* ------------------------------------------------- one pane at a time
     Gated on [data-pane]. Without the markup change the workspace keeps
     atelier's stacked layout instead of going blank. */
  .workspace[data-pane] {
    max-width: none; width: 100%; margin: 0;
    height: auto; min-height: 0; flex: 1 1 auto;
    display: flex; flex-direction: column; gap: 0;
    overflow: hidden;
  }
  .workspace[data-pane] > .library,
  .workspace[data-pane] > #stage,
  .workspace[data-pane] > .unit-card {
    display: none;
    border: 0; border-radius: 0; box-shadow: none;
    min-height: 0;
    max-height: none;                 /* atelier caps .library at 300px @780 */
  }
  .workspace[data-pane="units"]   > .library   { display: flex; flex: 1 1 auto; min-height: 0; }
  .workspace[data-pane="lesson"]  > #stage     { display: flex; flex: 1 1 auto; min-height: 0; }
  .workspace[data-pane="details"] > .unit-card { display: block; flex: 1 1 auto; min-height: 0; }

  /* -------------------------------------------------------- the tab bar
     NOT IN THE MARKUP YET — every rule in this block is currently inert.
     When added it must be a sibling of .workspace, so app.js's innerHTML
     rewrites of #stage and #unit-card never destroy it. The `hidden`
     attribute keeps it invisible at every width this file does not style;
     an author `display` beats the UA [hidden] rule, so it appears here and
     nowhere else. */
  .pane-tabs {
    flex: none;
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
    padding: 5px 10px;
    padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    background: var(--paper);
    -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  }
  .pane-tabs button {
    min-height: 48px;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
    border: 1px solid transparent; border-radius: 14px; background: transparent;
    color: var(--muted);
    font: 600 9.5px var(--sans); letter-spacing: .12em; text-transform: uppercase;
    transition: background .18s ease, color .18s ease;
    -webkit-tap-highlight-color: transparent;
  }
  .pane-tabs button i { font-style: normal; font-size: 16px; line-height: 1; }
  .pane-tabs button.on {
    color: var(--coral);
    background: rgba(235, 124, 107, .09);
    border-color: rgba(235, 124, 107, .22);
  }
  html[data-theme="dark"] .pane-tabs button.on {
    background: rgba(240, 144, 126, .14);
    border-color: rgba(240, 144, 126, .28);
  }
  /* the keyboard needs the room more than the tab bar does */
  html.kb-open .pane-tabs { display: none; }


  /* ==================================================== A1 · UNITS PANE */
  .workspace[data-pane] > .library { padding: 12px; }
  .lib-toggle { margin-bottom: 10px; }
  .lib-toggle button { min-height: 44px; font-size: 13px; }
  .library .panel-heading { padding: 0 4px 10px; font-size: 10.5px; }
  .unit-list {
    flex: 1 1 auto; min-height: 0; gap: 2px;
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  }
  .unit-item { min-height: 64px; padding: 9px 10px; grid-template-columns: 46px 1fr 22px; }
  .unit-item b { font-size: 16.5px; }
  .unit-item small { font-size: 10.5px; }
  .unit-item .u-state { font-size: 13px; }
  .new-unit-btn { flex: none; min-height: 48px; font-size: 13.5px; }
  .lib-empty { font-size: 14px; padding: 20px 6px; }


  /* =================================================== A2 · LESSON PANE */
  #stage { overflow: hidden; }
  #stage:after { border-radius: 0; }

  #stage .stage-cover {
    flex: 1 1 auto; min-height: 0;
    justify-content: flex-start;              /* centring clips long copy */
    padding: 22px 18px 80px;                  /* 80px clears caption + arrows */
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    /* overflow-y alone computes overflow-x to `auto`, and .em-label is
       absolutely positioned, nowrap and rotated — its transformed box adds
       scrollable width. Clip it instead of scrolling it. */
    overflow-x: hidden;
  }
  /* the only tappable thing in the cover is the prereq link in .stage-hint */
  #stage .stage-locked a {
    display: inline-block; padding: 13px 4px; margin: -13px 0;
    -webkit-tap-highlight-color: transparent;
  }
  #stage .empty-cover { padding: 26px 18px 80px; }
  #stage .empty-cta { min-height: 52px; width: 100%; padding: 0 20px; }
  #stage .stage-desc { font-size: 20px; line-height: 1.45; max-width: none; }
  #stage .stage-hint { margin-top: 20px; font-size: 13px; }

  /* the era strip: labels rotate further so they run down the page instead
     of off the right edge. The 26px lane step is written as an inline style
     by eraStripHTML(), so the strip height formula has to match it. */
  #stage .era-strip {
    max-width: none; margin-top: 22px;
    height: calc(138px + (var(--lanes, 1) - 1) * 26px);
  }
  #stage .era-seg { font-size: 8.5px; letter-spacing: .1em; }
  #stage .em-label { transform: rotate(52deg); font-size: 9.5px; }

  #stage .stage-caption { left: 16px; bottom: 14px; max-width: 58%; font-size: 8.5px; }
  #stage .stage-caption strong { font-size: 16px; }
  #stage .stage-nav { right: 12px; bottom: 12px; gap: 8px; }
  #stage .stage-nav a { width: 44px; height: 44px; border-radius: 13px; font-size: 20px; touch-action: manipulation; }

  /* ---- the tutor conversation ---- */
  #stage .stage-chat { padding: 12px 12px 58px; }   /* 58px clears caption + arrows */
  #stage .chat-mode { font-size: 11px; margin-bottom: 8px; }
  /* 44px hit area, 12px of layout — the negative margin cancels the padding */
  #stage .chat-mode a {
    display: inline-block; padding: 16px 12px; margin: -16px 0 -16px -8px;
    border-radius: 999px; -webkit-tap-highlight-color: transparent;
  }

  #chat {
    padding: 2px 0 6px; gap: 10px;
    border: 0; border-radius: 0; background: transparent;   /* 30px of line length back */
    overflow-x: hidden; overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  html[data-theme="dark"] #chat { background: transparent; }
  #chat::-webkit-scrollbar { width: 0; height: 0; }

  .msg {
    max-width: 88%; padding: 10px 14px; border-radius: 16px;
    font-size: 15px; line-height: 1.55;
    overflow-wrap: anywhere;         /* a long citation cannot widen the column */
  }
  .msg.tutor { border-bottom-left-radius: 7px; }
  .msg.user { border-bottom-right-radius: 7px; }
  .msg.verdict-msg { max-width: 100%; padding: 6px 10px; font-size: 11px; text-align: center; }
  /* .scribe-wrap is nested inside .msg, so their paddings double up */
  .msg.thinking.scribe-wrap { padding: 7px 13px 7px 9px; gap: 10px; }
  .dove { width: 50px; height: 32px; }
  .scribe-say { font-size: 12.5px; }

  #chat-input-row { flex: none; gap: 9px; margin-top: 8px; align-items: flex-end; }
  #chat-input {
    flex: 1 1 auto;
    min-width: 0;                    /* a textarea's intrinsic cols can overflow flex */
    /* no explicit height — that would disable field-sizing. rows="3" in
       tutorHTML() supplies the fallback height where field-sizing is absent. */
    /* fixed pixel bounds: a dvh max-height re-measures every time the browser
       chrome slides, which is what made the box jump mid-sentence */
    height: 46px; min-height: 46px; max-height: 132px;
    padding: 12px 14px; border-radius: 16px;
    font-size: 16px; line-height: 1.4;
    resize: none;                    /* the drag handle is unusable by thumb */
    overflow-y: auto;
  }
  /* the send control is the primary thumb target, so it reads primary */
  #chat-input-row .p-btn {
    flex: none; min-height: 48px; min-width: 72px; padding: 0 16px;
    border: 1px solid transparent; border-radius: 14px;
    background: linear-gradient(110deg, #ed8a72, #e87868); color: #fff;
    font-size: 14px; font-weight: 600;
    box-shadow: 0 6px 16px rgba(208, 90, 70, .22);
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
  }
  #chat-input-row .p-btn[disabled] { opacity: .45; box-shadow: none; }

  #stage .tutor-note { font-size: 13.5px; }
  #stage .mode-row { flex-wrap: wrap; gap: 10px; }
  #stage .mode-row button { min-height: 46px; padding: 0 18px; }

  /* while the tutor is running, the caption and the arrows only crowd the
     input row — drop them and give the padding back */
  @supports selector(:has(*)) {
    #stage:has(.stage-chat) .stage-caption,
    #stage:has(.stage-chat) .stage-nav { display: none; }
    #stage:has(.stage-chat) .stage-chat { padding-bottom: 12px; }
  }


  /* ================================================== A3 · DETAILS PANE */
  .workspace[data-pane] > .unit-card {
    padding: 18px 16px 32px;
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  }
  .unit-card .uc-meta { font-size: 10.5px; }
  .unit-card h1 { font-size: 30px; margin: 8px 0 5px; }
  .unit-card .uc-sub { font-size: 13.5px; margin-bottom: 16px; }
  .unit-card .gate-dots { margin: 0 0 16px; flex-wrap: wrap; gap: 8px; }
  .unit-card .gate-dot {
    display: inline-flex; align-items: center; min-height: 34px;
    padding: 0 14px; font-size: 12px;
  }
  .unit-card .lesson-button { min-height: 52px; border-radius: 14px; font-size: 13.5px; }
  .unit-card .action-grid { gap: 10px; margin-top: 10px; }
  .unit-card .action-grid button {
    min-height: 48px; border-radius: 12px;
    font-size: 12.5px; line-height: 1.25; padding: 0 8px;
  }
  .unit-card .blocked-go {
    display: flex; align-items: center; justify-content: center;
    min-height: 48px; font-size: 13.5px;
  }
  .unit-card .uc-locked { font-size: 14px; padding: 13px 14px; }
  .unit-card .uc-read { font-size: 14px; margin-bottom: 22px; }
  .unit-card .uc-fold summary { min-height: 48px; align-items: center; padding: 14px 0; font-size: 12px; }
  .unit-card .uc-body,
  .unit-card .chain-note { font-size: 13.5px; }
  .unit-card .chain-row { padding: 9px 0; }
  /* .chain-row is a baseline-aligned flex row: inline-block + padding would
     move the baseline, so the padding is cancelled by an equal negative
     margin. Same trick as .chat-mode a. */
  .unit-card .chain-row a {
    font-size: 15px;
    display: inline-block; padding: 13px 4px; margin: -13px -4px;
    -webkit-tap-highlight-color: transparent;
  }
  .unit-card .chain-row small { font-size: 12px; }
  .unit-card .chain-tick { width: 20px; height: 20px; font-size: 10.5px; }
  .unit-card .ev { font-size: 14px; padding-left: 18px; }
  .unit-card .align-row { padding: 11px 0 10px; }
  .unit-card .align-head b { font-size: 15px; }
  .unit-card .align-head em { font-size: 12px; }
  .unit-card .align-head a.align-link {
    display: inline-block; padding: 13px 4px; margin: -13px -4px;
    -webkit-tap-highlight-color: transparent;
  }
  /* atelier gives .free-link margin-top:5px — keep a positive top margin so
     the enlarged box cannot ride up over the source line above it */
  .unit-card .align-row .free-link {
    padding: 12px 4px; margin: -2px -4px 0;
    -webkit-tap-highlight-color: transparent;
  }
  .unit-card .align-detail { font-size: 12.5px; }
  .unit-card .align-chip { min-width: 34px; height: 22px; font-size: 11px; }
  .unit-card .steelman { font-size: 13.5px; }
  .unit-card .src-why { font-size: 13px; }
  .unit-card details.hist { font-size: 13px; padding: 10px 12px; }
  .unit-card details.hist summary { row-gap: 7px; }
  .unit-card .s-when { font-size: 12.5px; }
  .unit-card .s-resume { min-height: 44px; padding: 0 14px; font-size: 11.5px; }
  .unit-card .hist-chat { max-height: 46dvh; }


  /* ============================================ A4 · BUILD-A-UNIT MODAL
     A bottom sheet that lifts with the keyboard. Scoped to #builder so the
     whole-map modal — which shares .graph-panel / .graph-head / .graph-close
     — is untouched here and handled in section C. */
  body:has(#builder:not([hidden])) { overflow: hidden; }

  #builder.graph-modal {
    padding: 0 0 var(--kbd, 0px);
    place-items: end stretch;
    overscroll-behavior: contain;
  }
  #builder .graph-panel {
    width: 100%; height: auto;
    /* never taller than the space above the keyboard, always leaving 20px of
       backdrop so it reads as a sheet rather than a page */
    max-height: calc(100dvh - var(--kbd, 0px) - 20px);
    border-radius: 22px 22px 0 0; border-bottom: 0;
    background: #fdf8f0;             /* opaque: the sticky foot sits on it */
    box-shadow: 0 -18px 60px rgba(63, 42, 28, .30);
  }
  html[data-theme="dark"] #builder .graph-panel { background: #201d1a; }

  #builder .graph-head { align-items: flex-start; gap: 12px; padding: 16px 16px 14px; }
  #builder .graph-head h2 { font-size: 20px; line-height: 1.15; margin-top: 5px; }
  #builder .graph-close {
    width: 44px; height: 44px; margin: -5px -5px 0 0; font-size: 15px;
    touch-action: manipulation;
  }

  /* the body is the scroller — without it .graph-panel's overflow:hidden
     clips .builder-foot away and the Build button is unreachable */
  #builder-body {
    flex: 1 1 auto; min-height: 0; gap: 12px;
    padding: 16px 16px 0;
    overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  }
  #builder .kind-hint { font-size: 10.5px; margin-bottom: -6px; }
  /* all four options visible at once; wrapping pills cost ~4 ragged rows */
  #builder .kind-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  #builder .kind-btn {
    display: grid; place-items: center;
    min-height: 46px; padding: 8px 10px; border-radius: 14px;
    font-size: 12.5px; line-height: 1.25; text-align: center;
    touch-action: manipulation;
  }
  #builder .kind-btn[data-kind="auto"] { grid-column: 1 / -1; }
  #builder-input {
    min-height: 150px; max-height: 40dvh;
    padding: 12px 14px; border-radius: 14px;
    font-size: 16px; line-height: 1.5; resize: none;
  }
  /* Build stays on screen no matter how far the body scrolls.
     No negative side margins: #builder-body is a scroll container (overflow-y
     auto makes overflow-x auto), so a -16px bleed would push the foot into the
     unreachable left padding and add a horizontal scrollbar. The foot's
     background is the panel's own background in both themes, so an inset
     bar has no visible seam. */
  #builder .builder-foot {
    position: sticky; bottom: 0; z-index: 1;
    flex-wrap: wrap; align-items: stretch; gap: 10px;
    margin: 0;
    padding: 12px 0 calc(14px + env(safe-area-inset-bottom, 0px));
    background: #fdf8f0;
    box-shadow: 0 -1px 0 var(--line);
  }
  html[data-theme="dark"] #builder .builder-foot { background: #201d1a; }
  #builder .builder-note { flex: 1 1 100%; order: 2; font-size: 12px; line-height: 1.45; text-align: center; }
  #builder .builder-foot .lesson-button {
    order: 1; width: 100%; min-height: 52px; padding: 0 18px;
    justify-content: center; gap: 10px; border-radius: 14px; font-size: 13px;
    touch-action: manipulation;
  }


  /* ================================================= A5 · BUILD-JOB TRAY
     Desktop's 320px card at right:22px/bottom:22px lands squarely on the
     composer. It becomes a full-width strip above the tab bar, and yields
     entirely to the keyboard and to the builder sheet. */
  #job-tray {
    left: 10px; right: 10px; width: auto;
    bottom: calc(var(--tabbar-h) + 10px);
    max-height: 45dvh; overflow-y: auto; overscroll-behavior: contain;
  }
  .job { padding: 12px 14px; border-radius: 15px; }
  .job-head { gap: 7px; }
  .job-head b { font-size: 10px; }
  .job-label { font-size: 12.5px; }
  .job-steps { margin-top: 7px; gap: 0; }
  .job-steps li:not(.now) { display: none; }   /* one live step, not the whole list */
  .job-steps li.now { font-size: 12px; }
  .job-note { margin-top: 7px; font-size: 11.5px; line-height: 1.45; }
  .job-actions { gap: 8px; margin-top: 10px; }
  .job-actions .p-btn { flex: 1 1 0; min-height: 44px; border-radius: 12px; font-size: 13px; }

  /* the tray (z 70) would otherwise land on the builder sheet's head (z 60);
     the sheet shows its own build state, so it wins while open */
  html.kb-open #job-tray,
  body:has(#builder:not([hidden])) #job-tray { display: none; }
  /* html.kb-open needs JS that does not exist yet; :has() covers the case that
     actually matters — the tray must never sit on the composer being typed in */
  body:has(#chat-input:focus) #job-tray { display: none; }
}


/* ==========================================================================
   B · SHORT LANDSCAPE PHONE — overrides on top of section A
   iPhone 15 = 852x393, SE = 667x375. iPad mini landscape (1133x744) is
   excluded and keeps the full desktop three-column layout.
   ========================================================================== */
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
  .app-shell .topbar { flex-wrap: nowrap; padding: 6px 14px; gap: 10px; }
  .app-shell .brand { flex: 0 1 auto; }
  .app-shell .brand strong { font-size: 22px; }
  .app-shell .main-nav { order: 0; flex: 1 1 auto; justify-content: center; }
  .app-shell .main-nav a { min-height: 44px; padding: 8px 10px; font-size: 12px; }
  .app-shell .search-box { display: none; }     /* portrait keeps the search row */

  .pane-tabs { padding: 3px 10px; padding-bottom: calc(3px + env(safe-area-inset-bottom, 0px)); }
  .pane-tabs button { min-height: 44px; flex-direction: row; gap: 7px; }
  .pane-tabs button i { font-size: 14px; }

  #stage .stage-cover { padding: 14px 18px 62px; }
  #stage .stage-desc { font-size: 17px; }
  #stage .stage-chat { padding: 10px 12px 52px; }
  #chat-input { max-height: 120px; }
  .workspace[data-pane] > .unit-card { padding: 14px 16px 24px; }
  .unit-card h1 { font-size: 24px; }
}


/* ==========================================================================
   C · THE WHOLE-MAP MODAL  (#map on index.html)

   Plain (max-width: 820px) — this string is meant to be mirrored by a MOBQ
   in app.js that switches drawGraph() to a portrait cluster layout, adds the
   48px .hit circles and the tap-to-inspect sheet. That JS DOES NOT EXIST
   YET: `circle.hit` and `.tip-open` are never generated, so those two rules
   are inert and graph nodes are still ~8-20px touch targets. Everything else
   here stands on its own.

   Everything is scoped to #map so #builder — the other .graph-modal, which
   shares .graph-panel / .graph-head / .graph-close — keeps section A4.
   ========================================================================== */
@media (max-width: 820px) {

  /* ---- panel: full-bleed, no chrome ---- */
  #map.graph-modal { padding: 0; overscroll-behavior: contain; }
  #map .graph-panel {
    width: 100%; height: 100%;
    border: 0; border-radius: 0;
    box-shadow: none;                /* a lift reads as noise when it IS the screen */
  }

  /* ---- head: title row, stats row; the ✕ is a fixed cell that can never
     be squeezed out by the wrapping title or the three stats ---- */
  #map .graph-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 44px;
    align-items: start;
    gap: 7px 12px;
    padding: calc(12px + env(safe-area-inset-top, 0px)) calc(14px + env(safe-area-inset-right, 0px))
             11px calc(14px + env(safe-area-inset-left, 0px));
  }
  #map .graph-head > div:first-child { grid-column: 1; grid-row: 1; min-width: 0; }
  #map .graph-head h2 {
    font-size: 19px; line-height: 1.16; margin-top: 3px;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  }
  #map .graph-close {
    grid-column: 2; grid-row: 1; align-self: start;
    width: 44px; height: 44px; margin: -5px -5px 0 0; font-size: 16px;
    touch-action: manipulation;
  }
  #map .graph-stats {
    grid-column: 1 / -1; grid-row: 2;
    margin-left: 0; gap: 16px; flex-wrap: wrap; font-size: 10.5px;
  }
  #map .graph-stats b { font-size: 16px; margin-right: 4px; }

  /* ---- stage: the legend leaves the canvas and becomes a band ---- */
  #map .graph-stage { display: flex; flex-direction: column; overscroll-behavior: contain; }
  #map #legend {
    order: -1; flex: none;           /* its own row above the graph */
    position: static;
    display: flex; flex-wrap: nowrap; gap: 0 14px;
    overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
    padding: 8px calc(14px + env(safe-area-inset-right, 0px))
             8px calc(14px + env(safe-area-inset-left, 0px));
    background: transparent; border: 0; border-bottom: 1px solid var(--line);
    border-radius: 0; -webkit-backdrop-filter: none; backdrop-filter: none;
    font-size: 9.5px;
  }
  html[data-theme="dark"] #map #legend { background: transparent; }
  #map #legend::-webkit-scrollbar { display: none; }
  #map #legend > span { flex: none; white-space: nowrap; }

  #map #graph {
    flex: 1 1 0%; height: auto; min-height: 0;
    cursor: default;
    touch-action: none;              /* d3.zoom owns pinch and one-finger drag */
    -webkit-tap-highlight-color: transparent;
  }

  /* ---- nodes: finger-sized, legible at the initial zoom ---- */
  #map .node circle.hit { fill: transparent; stroke: none; pointer-events: all; }
  #map .node text { font-size: 12px; }
  #map .node.selected circle.core { stroke-width: 3px; }
  #map .subject-label { font-size: 12px; letter-spacing: .16em; }

  /* ---- tooltip: a hover preview becomes a tap sheet ----
     !important beats the inline left/top showTip() writes on desktop, so the
     sheet docks correctly even if the JS branch has not been applied. */
  #map #tooltip {
    left: 10px !important; right: 10px !important;
    top: auto !important; bottom: calc(10px + env(safe-area-inset-bottom, 0px)) !important;
    width: auto; max-width: none;
    padding: 13px 15px 14px; border-radius: 18px;
    box-shadow: 0 -10px 34px rgba(83, 59, 42, .18);
    animation: gm-in .2s cubic-bezier(.2, .85, .3, 1);
  }
  html[data-theme="dark"] #map #tooltip { box-shadow: 0 -10px 34px rgba(0, 0, 0, .55); }
  #map #tooltip .t-name { font-size: 19px; line-height: 1.2; }
  #map #tooltip .t-sub { font-size: 9.5px; }
  #map #tooltip .t-mastery { font-size: 12px; margin-top: 5px; }
  /* atelier's #tooltip is pointer-events:none. Only take pointer events once
     showTip() actually renders the .tip-open button — otherwise the docked
     sheet just swallows taps meant for the graph underneath it. */
  #map #tooltip:has(.tip-open) { pointer-events: auto; }
  #map .tip-open {
    display: block; width: 100%; min-height: 46px; margin-top: 11px;
    border: 0; border-radius: 12px;
    background: linear-gradient(110deg, #ed8a72, #e87868); color: #fff;
    font: 600 13px var(--sans);
    box-shadow: 0 9px 20px rgba(208, 90, 70, .18);
    touch-action: manipulation;
  }

  /* ---- hint: phone verbs, and it yields to the sheet ---- */
  #map .graph-hint {
    left: 50%; right: auto; transform: translateX(-50%);
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    white-space: nowrap;
    font-size: 0;                    /* suppress the desktop wording */
    transition: opacity .18s ease;
  }
  #map .graph-hint::after {
    content: "drag to pan · pinch to zoom · tap a topic";
    font-size: 9.5px; letter-spacing: .08em;
  }
  #map #tooltip:not([hidden]) ~ .graph-hint { opacity: 0; pointer-events: none; }


  /* ======================================================================
     D · SHARED TOPBAR for story.html and heart.html
     A different stylesheet from index (story.css), so it needs its own
     rules. Same shape: brand + account on row one, nav on row two.
     ====================================================================== */
  .shell {
    padding: 14px 16px 0;
    padding-left: max(16px, env(safe-area-inset-left, 0px));
    padding-right: max(16px, env(safe-area-inset-right, 0px));
    padding-top: max(14px, env(safe-area-inset-top, 0px));
  }
  .shell header.topbar {
    height: auto;
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "brand right"
      "nav   nav";
    gap: 10px 12px;
  }
  .shell header.topbar .brand { grid-area: brand; gap: 6px; }
  .shell header.topbar .brand strong { font-size: 26px; }
  .shell header.topbar .brand em { display: none; }
  .shell header.topbar .top-right { grid-area: right; gap: 8px; justify-self: end; }
  .shell header.topbar .main-nav {
    grid-area: nav; justify-content: flex-start; gap: 4px;
    overflow-x: auto; overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    margin: 0 -16px; padding: 0 16px 2px;
  }
  .shell header.topbar .main-nav::-webkit-scrollbar { display: none; }
  .shell header.topbar .main-nav a {
    flex: none; min-height: 44px; padding: 0 14px;
    align-items: center; white-space: nowrap;
  }
  .shell header.topbar .theme-btn,
  .shell header.topbar .who-btn {
    width: 44px; height: 44px; flex: none; margin: 0; font-size: 15px;
  }


  /* ======================================================================
     E · THE STORY PAGE  (story.html)
     Beat 1 (the wall of lives) is a real chart and survives once story.js
     retunes the geometry. Beat 2 (the family tree) cannot: .river-why is a
     single ~400px non-wrapping SVG <text> line, so every fork reason
     renders off the left edge. The plan is for story.js to build the same
     eleven forks as a DOM list (.tree-list) in the existing .layer-html
     overlay. IT DOES NOT DO THAT YET — every .tree-* / .tr-* rule below is
     inert, and the river-hiding rule is gated on .tree-list existing so
     beat 2 is never blanked out. --bg/--dim/--g2/--line2/--mono all exist
     in style.css, so the block is ready as written.
     ====================================================================== */

  /* 100vh on iOS is the URL-bar-hidden height, so the bottom of the pinned
     stage — the axis and the event baseline — sits under the browser chrome */
  .act { height: calc(var(--act-vh, 4) * 100dvh); }
  .act-pin { height: 100dvh; }
  #act-spine { margin-top: 0; }

  /* a 3px bar is not a touch target; a mis-tap opens a random topic */
  #act-spine .life { pointer-events: none; }

  /* era names run on two alternating rows (story.js), so they need a small
     size and a paper halo over the bars */
  #act-spine .era-head {
    font-size: 9px; letter-spacing: .16em;
    paint-order: stroke; stroke: var(--bg); stroke-width: 3px;
  }
  #act-spine .era-sub { display: none; }
  #act-spine .sp-axis text { font-size: 8.5px; letter-spacing: .5px; }
  #act-spine .sp-axis line { stroke-opacity: .7; }
  #act-spine .life-label { font-size: 8px; stroke-width: 2.5px; letter-spacing: .04em; }
  #act-spine .sp-today-label { font-size: 8.5px; letter-spacing: .12em; }

  /* the four turning-point labels are 110px+ of rotated, non-wrapping text
     and every one of them flips at 393px. Bare diamonds read as noise, so
     the whole group goes; the axis ticks and .river-year carry the dates. */
  #act-spine .evt { display: none; }

  /* The SVG river is still BUILT — progress() calls getTotalLength() on the
     paths, and geometry APIs on a display:none SVG element are unreliable —
     so it is hidden with visibility, not display.
     GATED on the replacement actually being in the DOM. story.js does not
     build .tree-list yet; hiding the river unconditionally would leave beat 2
     of the spine completely blank on every phone. No :has() support means the
     river simply stays visible, which is the correct fallback. */
  #act-spine:has(.tree-list) .river-path,
  #act-spine:has(.tree-list) .river-label,
  #act-spine:has(.tree-list) .river-why,
  #act-spine:has(.tree-list) .river-year,
  #act-spine:has(.tree-list) .river-dot { visibility: hidden; }

  /* ...and replaced by a DOM list, where text wraps */
  #act-spine .tree-list {
    position: absolute; inset: 0; pointer-events: none;
    display: flex; flex-direction: column; justify-content: center;
    padding:
      calc(env(safe-area-inset-top, 0px) + 16px)
      max(18px, env(safe-area-inset-right, 0px))
      calc(env(safe-area-inset-bottom, 0px) + 16px)
      max(20px, env(safe-area-inset-left, 0px));
  }
  #act-spine .tree-head {
    font: 600 20px/1.15 var(--serif); letter-spacing: -.01em; color: var(--ink);
    margin: 0 0 13px; max-width: 22ch;
    opacity: 0; transform: translateY(8px);
    transition: opacity .45s ease, transform .45s ease;
  }
  #act-spine .tree-list.is-live .tree-head { opacity: 1; transform: none; }
  #act-spine .tree-rows {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-direction: column;
    /* svh, not dvh: a stable smallest-viewport gap can never grow the list
       past a 667px SE while the URL bar is showing */
    gap: clamp(3px, 1svh, 10px);
  }
  #act-spine .tree-row {
    position: relative;
    display: grid; grid-template-columns: 34px 1fr; column-gap: 10px; align-items: start;
    padding-left: 13px;
    opacity: 0; transform: translateY(6px);
    transition: opacity .5s ease, transform .5s ease;
  }
  #act-spine .tree-row.is-in { opacity: 1; transform: none; }
  /* one hairline trunk down the left, a node at every fork */
  #act-spine .tree-row::before {
    content: ""; position: absolute; left: 0; top: -6px; bottom: -6px;
    width: 1px; background: var(--line2);
  }
  #act-spine .tree-row:last-child::before { bottom: 50%; }
  #act-spine .tree-row::after {
    content: ""; position: absolute; left: -2px; top: 5px;
    width: 5px; height: 5px; border-radius: 50%; background: var(--g2);
  }
  #act-spine .tree-row.is-trunk::after {
    left: -3px; top: 4px; width: 7px; height: 7px; background: var(--ink);
  }
  #act-spine .tr-year {
    font: 700 10px/1.5 var(--mono); letter-spacing: .06em; color: var(--g2);
    text-align: right; padding-top: 1px; font-variant-numeric: tabular-nums;
  }
  #act-spine .tr-body b {
    display: block; font: 700 10.5px/1.45 var(--mono); letter-spacing: .07em; color: var(--ink);
  }
  #act-spine .tr-body i {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    margin-top: 2px; font: italic 400 12px/1.35 var(--serif); color: var(--dim);
  }


  /* ======================================================================
     F · THE PRACTICES PAGE  (heart.html) — one column
     ====================================================================== */
  #heart {
    max-width: none;
    padding: 24px 16px 80px;
    padding-left: max(16px, env(safe-area-inset-left, 0px));
    padding-right: max(16px, env(safe-area-inset-right, 0px));
    padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  }
  #heart .section-head { gap: 12px; margin-bottom: 22px; }
  #heart .section-head h2 { font-size: 21px; letter-spacing: 2.5px; margin-bottom: 6px; }
  #heart .section-head p { font-size: 13px; line-height: 1.6; }
  #heart .sec-num { align-self: flex-start; }

  /* renderHeat() always draws 26 weeks (there is no phone branch), but it
     already clamps the svg width to #heart.clientWidth - 40, so the grid
     scales down rather than overflowing. The scroller is belt-and-braces. */
  #heart #heat-wrap {
    margin-bottom: 26px;
    overflow-x: auto; overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  #heart #heat-wrap::-webkit-scrollbar { display: none; }
  #heart #heat { max-width: 100%; height: auto; }
  #heart .viz-caption {
    font-size: 9px; letter-spacing: .8px; line-height: 1.7;
    margin-top: 12px; max-width: 32ch;
  }

  #heart #practices { grid-template-columns: 1fr; gap: 11px; }
  #heart .practice { padding: 15px 16px; border-radius: 14px; }
  #heart .practice.future { opacity: .6; }   /* .45 does not read outdoors */
  #heart .practice h4 { font-size: 15.5px; }
  #heart .practice .p-what { font-size: 13px; line-height: 1.55; }
  #heart .practice .p-start { font-size: 12px; line-height: 1.5; }
  #heart .practice .p-foot { flex-wrap: wrap; gap: 10px 12px; margin-top: 14px; }
  #heart .practice .p-btn {
    min-height: 44px; padding: 0 18px; border-radius: 10px; font-size: 11px;
    touch-action: manipulation;
  }
  #heart .practice .p-note { font-size: 12px; line-height: 1.45; }
  #heart .practice .streak { font-size: 11.5px; }


  /* ======================================================================
     G · SIGN IN  (login.html)
     ====================================================================== */
  /* atelier pins body to 100vh; on iOS that is taller than the visible page */
  body:has(.signin-wrap) { min-height: 100dvh; }
  .signin-wrap {
    min-height: 100dvh;
    /* top-aligned, not centred: a centred card is re-centred when the
       software keyboard opens, throwing the focused field off screen */
    align-items: start; justify-items: center;
    padding: calc(env(safe-area-inset-top, 0px) + 7dvh)
             max(16px, env(safe-area-inset-right, 0px))
             calc(env(safe-area-inset-bottom, 0px) + 24px)
             max(16px, env(safe-area-inset-left, 0px));
  }
  .signin {
    width: 100%; max-width: 420px; gap: 13px;
    padding: 28px 20px 24px; border-radius: 22px;
  }
  .signin .brand strong { font-size: 34px; }
  .signin-tag { font-size: 13px; margin-bottom: 6px; }
  .signin label { font-size: 10px; gap: 7px; }
  .signin input {
    font-size: 16px;                 /* load-bearing: iOS zooms below 16px */
    min-height: 46px; padding: 0 14px; border-radius: 12px;
  }
  .signin .lesson-button { min-height: 50px; margin-top: 10px; font-size: 13px; border-radius: 12px; }
  .signin-err { font-size: 12.5px; }
  .signin-alt { font-size: 13.5px; margin-top: 4px; }
  .signin-alt a { display: inline-block; padding: 14px 8px; margin: -6px 0; }
}


/* ==========================================================================
   H · NARROW TIER  ·  <= 420px   (iPhone 15 = 393, iPhone SE = 375)
   ========================================================================== */
@media (max-width: 420px) {

  /* ---- path page ---- */
  .app-shell .topbar { padding-left: 12px; padding-right: 12px; gap: 6px 8px; }
  .app-shell .brand strong { font-size: 23px; }
  .app-shell .main-nav a { padding: 10px; font-size: 12px; gap: 5px; }

  .workspace[data-pane] > .library { padding: 10px; }
  .unit-item { grid-template-columns: 42px 1fr 20px; gap: 8px; padding: 8px; }
  .unit-glyph { width: 38px; height: 38px; font-size: 14px; }
  .unit-item b { font-size: 15.5px; }

  #stage .stage-cover { padding: 18px 14px 78px; }
  #stage .stage-desc { font-size: 18.5px; }
  #stage .era-strip { height: calc(132px + (var(--lanes, 1) - 1) * 26px); }
  #stage .em-label { font-size: 9px; }
  #stage .stage-caption { left: 14px; max-width: 54%; }
  #stage .stage-chat { padding: 10px 10px 58px; }
  .msg { font-size: 14.5px; max-width: 92%; padding: 9px 13px; }
  #chat-input { height: 44px; min-height: 44px; max-height: 108px; padding: 11px 13px; }
  #chat-input-row { gap: 7px; }
  #chat-input-row .p-btn { min-width: 62px; min-height: 46px; padding: 0 12px; font-size: 13px; }

  .workspace[data-pane] > .unit-card { padding: 16px 14px 30px; }
  .unit-card h1 { font-size: 27px; }
  .unit-card .action-grid { gap: 8px; }
  .unit-card .action-grid button { font-size: 12px; padding: 0 6px; }

  .pane-tabs { padding-left: 8px; padding-right: 8px; gap: 3px; }
  .pane-tabs button { font-size: 9px; letter-spacing: .1em; }

  #builder .graph-head h2 { font-size: 18px; }
  #builder .kind-row { grid-template-columns: 1fr; }   /* two-up truncates these labels */
  #builder .kind-btn { min-height: 44px; }
  #builder-input { min-height: 120px; max-height: 32dvh; }
  .job-label { font-size: 12px; }

  /* ---- whole-map modal ---- */
  #map .graph-head {
    gap: 6px 10px;
    padding-right: calc(12px + env(safe-area-inset-right, 0px));
    padding-left: calc(12px + env(safe-area-inset-left, 0px));
  }
  #map .graph-head h2 { font-size: 17px; }
  #map .graph-head .info-kicker { font-size: 8.5px; letter-spacing: .11em; gap: 5px; }
  #map .graph-stats { gap: 13px; font-size: 10px; }
  #map .graph-stats b { font-size: 15px; }
  #map #legend {
    font-size: 9px; gap: 0 12px;
    padding: 7px calc(12px + env(safe-area-inset-right, 0px))
             7px calc(12px + env(safe-area-inset-left, 0px));
  }
  #map .dot { width: 8px; height: 8px; margin-right: 4px; }
  #map #tooltip { padding: 12px 14px 13px; }
  #map #tooltip .t-name { font-size: 18px; }
  #map .node text { font-size: 12.5px; }

  /* ---- story / heart / login ---- */
  .shell header.topbar .brand strong { font-size: 23px; }
  .shell header.topbar .main-nav a { padding: 0 11px; font-size: 12.5px; }

  /* the type budget that fits eleven forks in one pinned 667px viewport */
  #act-spine .tree-head { font-size: 18px; margin-bottom: 11px; }
  #act-spine .tree-row { grid-template-columns: 31px 1fr; column-gap: 8px; }
  #act-spine .tr-year { font-size: 9.5px; }
  #act-spine .tr-body b { font-size: 10px; letter-spacing: .05em; }
  #act-spine .tr-body i { font-size: 11.5px; line-height: 1.3; }

  #heart {
    padding-left: max(14px, env(safe-area-inset-left, 0px));
    padding-right: max(14px, env(safe-area-inset-right, 0px));
  }
  #heart .section-head { gap: 10px; }
  #heart .section-head h2 { font-size: 19px; letter-spacing: 2px; }
  #heart .practice { padding: 14px 15px; }

  .signin { padding: 26px 18px 22px; }
  .signin .brand strong { font-size: 31px; }
}


/* ==========================================================================
   I · REDUCED MOTION — mobile only, so desktop behaviour is untouched
   ========================================================================== */
@media (max-width: 820px) and (prefers-reduced-motion: reduce),
       (max-width: 900px) and (max-height: 480px) and (orientation: landscape)
         and (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .pane-tabs button { transition: none; }
  #builder .graph-panel,
  #map .graph-panel,
  #map #tooltip { animation: none; }
  #map .node.next circle.frontier-ring { animation: none; stroke-opacity: .95; }
  #map .graph-hint { transition: none; }
  .unit-list, .unit-card, #stage .stage-cover, #chat { scroll-behavior: auto; }
  .prose-step { transition: none; }
  #act-spine .tree-head,
  #act-spine .tree-row { transition: none; transform: none; }
  #act-spine .tree-row { opacity: 0; }
  #act-spine .tree-row.is-in { opacity: 1; }
  .scroll-hint span { animation: none; }
}

/* ---------------------------------------------------------------------------
   CHAT, SIMPLIFIED — a calmer conversation on a phone
   --------------------------------------------------------------------------- */
@media (max-width: 820px) {
  /* the composer is the only thing docked; give it a quiet edge instead of a card */
  #chat-input-row {
    padding-top: 8px;
    border-top: 1px solid var(--line);
    background: var(--paper);
  }
  html[data-theme="dark"] #chat-input-row { background: #201d1a; }
  #chat-input {
    background: color-mix(in srgb, var(--ink) 5%, transparent);
    border-color: transparent;
    transition: none;                 /* no animated resize while typing */
  }
  #chat-input:focus { background: color-mix(in srgb, var(--ink) 7%, transparent); border-color: var(--coral); }

  /* roomier bubbles, less chrome */
  #chat { gap: 12px; padding: 14px 2px 6px; }
  .msg { max-width: 86%; padding: 10px 14px; border-radius: 16px; line-height: 1.5; }
  .msg.tutor { border: 0; background: color-mix(in srgb, var(--ink) 5%, transparent); box-shadow: none; }
  html[data-theme="dark"] .msg.tutor { background: rgba(255,255,255,.06); }
  .msg.user { border-bottom-right-radius: 6px; }
  .msg.tutor { border-bottom-left-radius: 6px; }
  .msg.verdict-msg { max-width: 100%; padding: 2px; }

  /* mode line: small and out of the way */
  #stage .chat-mode { font-size: 10px; letter-spacing: .1em; margin-bottom: 6px; opacity: .8; }
}

/* ---------------------------------------------------------------------------
   iMESSAGE BEHAVIOUR — the composer rides the keyboard, nothing else moves
   --------------------------------------------------------------------------- */
@media (max-width: 820px) {
  /* the lesson pane is a column: conversation scrolls, composer is pinned last */
  #stage .stage-chat { display: flex; flex-direction: column; min-height: 0; }
  #chat { flex: 1 1 auto; min-height: 0; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
  #chat-input-row { flex: 0 0 auto; }

  /* while the keyboard is up the conversation owns the screen: the tab bar,
     the caption and the paging arrows all step aside rather than being
     pushed around by it */
  html.kb-open .stage-caption,
  html.kb-open .stage-nav,
  html.kb-open .topbar { display: none; }
  html.kb-open #stage .stage-chat { padding-top: 8px; padding-bottom: 8px; }

  /* nothing inside the frame may bounce; only #chat scrolls */
  body.page-path .workspace,
  body.page-path .library,
  body.page-path .unit-card { overscroll-behavior: contain; }
}

/* the gate meter reads horizontally on a phone: taller rows are wasted space */
@media (max-width: 820px) {
  .unit-item { grid-template-columns: 44px 1fr 32px; }
  .pips { flex-direction: row; gap: 4px; }
  .pips i { width: 8px; height: 8px; }
  .pips.is-held { box-shadow: 0 7px 0 -6px var(--sage); }
}
