/* ==========================================================================
   mobile.css — phone layout for the agent-facing app.

   Loaded AFTER tailwind.css and after each page's own <style> block, so it
   wins on specificity ties without !important.

   RULES FOR THIS FILE
   -------------------
   1. Every rule lives inside `@media (max-width: 768px)` or defines a custom
      property. Desktop rendering must stay byte-for-byte identical — this
      file is additive, never a refactor of the desktop layout.
   2. 768px is the only breakpoint. Tablets get the desktop layout: the
      three-column workspace is usable at 768px+ and a second breakpoint
      doubles the QA matrix for no user we have.
   3. Anything JS needs to branch on reads `documentElement.dataset` set by
      mobile.js — never a width check in JS, which would drift from here.

   Milestone 0: foundation only. No layout is moved yet; that is M1.
   ========================================================================== */

:root {
  /* Safe areas. env() resolves to 0px on every browser that has no notch, and
     on notched devices only once the page opts in with viewport-fit=cover —
     which is why the meta tag and these vars have to ship together. Reading
     them through a custom property means a rule can use them unconditionally
     without repeating the env() fallback everywhere. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  /* Minimum comfortable touch target. Used as a floor, not a fixed size:
     controls that are already larger keep their size. */
  --tap: 44px;

  /* Resting height of the chat sheet in the workspace: grab handle, one line
     of the agent's last message, and the composer. The document reserves this
     much bottom padding so its final page is never permanently covered. */
  --sheet-peek: calc(132px + var(--safe-bottom));

  /* Talking to the agent. Deliberately not taller: past about 55% the document
     stops being visible enough to be the thing you are checking against, and
     the sheet may as well have been a full screen. */
  --sheet-half: 55dvh;
}

@media (max-width: 768px) {
  /* ------------------------------------------------------------------
     Viewport height

     The workspace is a 100vh shell with `overflow: hidden` on <body>, which
     is correct on desktop and wrong on mobile Safari: 100vh there means the
     viewport with the URL bar RETRACTED, so a full-height shell is ~60-100px
     taller than what you can see, and the bottom row — the tab bar today,
     the chat composer under Option A — sits below the fold until you scroll
     a page that cannot scroll.

     dvh tracks the visible viewport as the bar shows and hides. Guarded by
     @supports so a browser without dvh keeps the 100vh it has always had
     rather than collapsing to auto height.
     ------------------------------------------------------------------ */
  @supports (height: 100dvh) {
    .main-container {
      height: 100dvh;
    }
  }

  /* ------------------------------------------------------------------
     iOS zoom-on-focus

     Mobile Safari zooms the whole page when a focused control's font-size is
     under 16px, and does not zoom back out. The app's inputs are 0.875rem
     (.form-input-compact) and 0.85rem (.doc-modal__search), so today every
     tap into a field throws the layout off. 16px is the floor, not a
     redesign — the control's padding and height are untouched.

     Marked !important because this is a platform floor, not a design value,
     and it does not win on load order alone: `.form-input-compact` and
     Tailwind's `.text-sm` are class selectors, so they beat a bare `select`
     or `textarea`. The input rule happened to win by accident — its three
     `:not([type=…])` clauses score as three attribute selectors — which is
     why inputs measured 16px on a phone while every select and textarea in
     the same sheet was still 14px, and still zoomed iOS on focus.

     Scoped to real form controls so it cannot leak into the type scale.
     ------------------------------------------------------------------ */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select,
  textarea {
    font-size: 16px !important;
  }

  /* Native controls — the select popup, date pickers, scrollbars — are painted
     by the browser in the OS colour scheme unless the page declares one. The
     app is light-only, so on a phone set to dark mode Chromium was drawing the
     role popup as dark-on-dark over a white sheet. Declaring the scheme is the
     only way to reach a widget that is not in the DOM.

     Desktop has the same latent issue; left alone here because this file's
     contract is that it changes nothing outside the breakpoint. */
  :root,
  select,
  input,
  textarea {
    color-scheme: light;
  }

  /* The PDF overlay inputs are the deliberate exception: their font-size is
     computed from currentPdfState.scale to match the text baked into the
     page image, so forcing 16px would make a manually placed value render at
     a different size from every AI-filled one next to it. Touch editing is
     switched off on phones anyway (M2), so these are never focused there. */
  #pdfContainer input,
  #pdfContainer textarea {
    font-size: inherit !important;
  }

  /* ------------------------------------------------------------------
     Touch targets

     A floor for the controls that are demonstrably under 44px today — the
     rail's remove button (0.15rem padding), the rail collapse toggle
     (0.2rem) and the doc-modal close button. Applied as min-width/height
     with centred content so the visual icon size does not change; only the
     hit area grows.
     ------------------------------------------------------------------ */
  .document-rail__toggle,
  .doc-row__remove,
  .doc-modal__close {
    min-width: var(--tap);
    min-height: var(--tap);
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* The remove button is revealed by :hover on desktop, which never fires on
     a touch screen — so on a phone it would be permanently invisible but
     occupying 44px of the row. Kept hidden here; removing a document from a
     phone goes through the document sheet in M3. */
  .doc-row__remove {
    display: none;
  }

  /* ------------------------------------------------------------------
     Scroll behaviour

     Momentum scrolling for the panes that scroll, and overscroll containment
     so dragging past the end of the chat log or a modal does not rubber-band
     the whole shell — which on iOS also triggers pull-to-refresh.
     ------------------------------------------------------------------ */
  .chat-window,
  .document-rail__list,
  .doc-modal__list,
  .modal-content {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* Hairline scrollbars are a pointer affordance; on touch they only eat
     6px of a 390px width. The scroll position is still obvious from the
     content moving. */
  .chat-window::-webkit-scrollbar,
  .document-rail__list::-webkit-scrollbar,
  .doc-modal__list::-webkit-scrollbar {
    width: 0;
    height: 0;
  }

  /* ==================================================================
     M1 — Workspace shell

     Desktop is a three-column grid: chat | 234px rail | pdf. On a phone the
     document owns the screen and chat sits over it as a sheet, so the grid
     is dropped entirely and the two surviving columns are positioned by
     hand. The rail is not shown at all here — it returns as a chip strip in
     M3.

     No DOM was reordered to do this. `.chat-column` still precedes
     `.pdf-column` in source order, which keeps every getElementById in
     app.js working and means the desktop path is untouched.

     `.main-container[data-mobile-view]` is the switch, set by
     setMobileWorkspace() in app.js:
       (absent) / "tabs"  — the tabbed screens; chat-column fills the phone
       "workspace"        — a draft is open; pdf fills it, chat is a sheet
     ================================================================== */
  .main-container {
    display: block;
    position: relative;
    width: 100%;
    overflow: hidden;
  }

  /* The 234px middle column. Hidden rather than collapsed to the 60px icon
     strip: at 390px even 60px is 15% of the width, and the strip's only
     affordances are hover-revealed. */
  .document-rail {
    display: none;
  }

  .chat-column,
  .pdf-column {
    position: absolute;
    inset: 0;
    height: auto;
    border-right: none;
  }

  /* ---- Tabs view: chat-column is the whole phone ---- */
  .pdf-column {
    /* Hidden by visibility rather than display so the PDF canvases keep their
       layout boxes. PDF.js renders into sized canvases and the overlay text is
       absolutely positioned against them; display:none would zero those boxes
       and every overlay would stack at the origin when the workspace reopens. */
    visibility: hidden;
    pointer-events: none;
  }

  /* ---- Workspace view: pdf fills the phone, chat becomes a sheet ---- */
  .main-container[data-mobile-view="workspace"] .pdf-column {
    visibility: visible;
    pointer-events: auto;
    /* Room for the chat sheet at its resting height, so the last page is not
       permanently covered by it. */
    padding-bottom: var(--sheet-peek);
  }

  .main-container[data-mobile-view="workspace"] .chat-column {
    top: auto;
    bottom: 0;
    height: var(--sheet-peek);
    border-radius: 20px 20px 0 0;
    box-shadow: 0 -8px 26px rgba(15, 23, 42, 0.1);
    border-top: 1px solid #e2e8f0;
    /* M4 makes this draggable between peek and half. Until then the height is
       fixed, and the transition is already here so the drag has something to
       animate against rather than snapping. */
    transition: height 0.24s cubic-bezier(0.32, 0.72, 0, 1);
    z-index: 60;
  }

  /* A sheet at 132px has room for the conversation and the composer, and
     nothing else. Three things are dropped from it:

       div:first-child  the app header (logo, title, bell). The document bar
                        above already says where you are, and left in place it
                        filled the entire sheet — the conversation never
                        appeared at all.
       div:last-child   the tab bar. The workspace is a pushed screen; you
                        leave it with the back chevron, not by switching tabs.
       #mlsTab etc.     the non-chat tabs, in case one was selected when the
                        draft opened. */
  .main-container[data-mobile-view="workspace"] #chatHeader,
  .main-container[data-mobile-view="workspace"] #mobileTabBar,
  .main-container[data-mobile-view="workspace"] .chat-window,
  .main-container[data-mobile-view="workspace"] #mlsTab,
  .main-container[data-mobile-view="workspace"] #contractsTab,
  .main-container[data-mobile-view="workspace"] #settingsTab {
    display: none;
  }

  /* With the header gone the tab-content wrapper is the sheet's only child, so
     it takes the height and the conversation scrolls above a pinned composer.
     The chat log gets tighter padding here than in the tabs view: at 132px,
     1rem of vertical padding is a sixth of the sheet. */
  .main-container[data-mobile-view="workspace"] .chat-window {
    padding: 0.5rem 0.875rem 0.25rem;
  }

  /* ==================================================================
     Peek summary row

     Only at peek. Expanding the sheet (M4) swaps it for the conversation.
     ================================================================== */
  .main-container[data-mobile-view="workspace"] .peek-summary {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 4px 14px 0;
    background: transparent;
    border: none;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    flex-shrink: 0;
  }

  .peek-summary[hidden] {
    display: none !important;
  }

  .peek-summary__avatar {
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    border-radius: 8px;
    background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
  }

  .peek-summary__text {
    flex-grow: 1;
    min-width: 0;
    font-size: 12px;
    color: #475569;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The value the agent just changed is the point of the line, so it takes the
     ink the rest of the sentence does not. */
  .peek-summary__text strong {
    color: #0f172a;
    font-weight: 700;
  }

  .peek-summary__count {
    flex-shrink: 0;
    font-size: 10px;
    font-weight: 800;
    color: #6366f1;
    background: #eef2ff;
    padding: 3px 7px;
    border-radius: 6px;
  }

  .peek-summary__count[hidden] {
    display: none;
  }

  /* The composer wrapper keeps its desktop p-6 — 24px top and bottom around a
     53px input, which is 106px of a 132px sheet, leaving the row above it about
     two pixels. The sheet supplies its own vertical rhythm. */
  .main-container[data-mobile-view="workspace"] #chatTab > div:last-child {
    padding: 8px 0 0;
  }

  /* Chat bubbles: the design sets 13px/1.55. The desktop text-sm is 14px with a
     22.75px line-height, which at 390px costs roughly one extra line per turn
     in a sheet that only has a few to give. */
  .chat-window .message-bubble p,
  .chat-window .message-bubble div {
    font-size: 13px;
    line-height: 1.55;
  }

  /* The composer's mic and send are absolutely positioned inside the input with
     `top-3 bottom-3`, which on a ~53px field leaves them 29px tall — under the
     44px the design states as a hard rule. Centred and given a real height
     instead; the horizontal offsets they already carry are unchanged. */
  #voiceButton,
  #chatTab button[onclick="sendMessage()"] {
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    height: 44px;
    min-width: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* The composer's mic and send buttons are absolutely positioned over the
     input, which reserves 112px (pr-28) for them. That is enough at desktop
     width where the send button's px-5 label area is the only thing there, but
     at 390px the two together measure ~132px and sat on top of the placeholder
     — it read "Ask me anything or describ". Widened to clear both, and the
     placeholder shortened since 390px cannot show the long one either way. */
  #userInput {
    padding-right: 8.75rem;
  }

  /* ==================================================================
     Field jump card

     Main.dc.html. Sits inside the agent's turn, so it reads as part of what the
     agent just said rather than as a separate message.
     ================================================================== */
  /* Aligned under the bubble, clear of the 40px avatar and its 16px gap. */
  .jump-card--indent,
  .jump-card-group.jump-card--indent {
    margin-left: 56px;
    max-width: calc(100% - 56px);
  }

  .jump-card {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin-top: 7px;
    padding: 9px 11px;
    background: #eef2ff;
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }

  .jump-card:active {
    background: #e0e7ff;
  }

  .jump-card__check {
    flex-shrink: 0;
    background: #e0e7ff;
    color: #4f46e5;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .jump-card__body {
    flex-grow: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
  }

  .jump-card__title {
    font-size: 11px;
    font-weight: 700;
    color: #3730a3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .jump-card__sub {
    font-size: 10px;
    color: #6366f1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .jump-card__chevron {
    flex-shrink: 0;
    color: #6366f1;
  }

  /* Multi-document form: one row per document, on white so a packet-wide write
     reads as a list rather than as one very tall tinted card. */
  .jump-card-group {
    margin-top: 7px;
    background: #ffffff;
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 6px 18px rgba(79, 70, 229, 0.1);
  }

  .jump-card-group__row {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 46px;
    padding: 9px 11px;
    background: transparent;
    border: none;
    border-bottom: 1px solid #eef2ff;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }

  .jump-card-group__row:last-child {
    border-bottom: none;
  }

  .jump-card-group__row:active {
    background: #f8fafc;
  }

  .jump-card-group__row .jump-card__title {
    font-size: 12px;
  }

  /* ==================================================================
     Sheet detents

     "peek" is the resting state; "half" is the conversation. The attribute
     lives on .main-container next to data-mobile-view so one element carries
     the whole shell's state.
     ================================================================== */
  .main-container[data-mobile-view="workspace"] .sheet-grabber {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 20px;
    flex-shrink: 0;
    /* The bar is 4px; the target around it is the whole 20px strip plus the
       row below, which is what a thumb actually lands on. */
    touch-action: none;
    cursor: grab;
  }

  .sheet-grabber__bar {
    width: 38px;
    height: 4px;
    border-radius: 999px;
    background-color: #cbd5e1;
  }

  .main-container[data-mobile-view="workspace"][data-sheet="half"] .chat-column {
    height: var(--sheet-half);
  }

  /* At peek the conversation is replaced by the one-line summary; at half the
     summary gives way to the conversation itself. Only one is ever on screen. */
  .main-container[data-mobile-view="workspace"][data-sheet="half"] .peek-summary {
    display: none !important;
  }

  .main-container[data-mobile-view="workspace"][data-sheet="half"] .chat-window {
    display: block;
  }

  /* While a finger is down the height follows it directly, so the transition
     has to be off or every frame fights the animation. */
  .main-container[data-sheet-dragging="true"] .chat-column {
    transition: none !important;
  }

  .main-container[data-mobile-view="workspace"] #chatTab {
    padding-top: 0.75rem;
  }

  /* ---- Chrome inside the chat column ---- */

  /* 2.5rem of padding on a 390px screen leaves 230px of usable line. */
  .chat-window {
    padding: 1rem 1rem 0.5rem;
  }

  /* The desktop header carries a bell, "New Contract" and "Logout" beside the
     title. At this width the two text buttons wrap under the logo; they move
     to the document bar's overflow menu instead. */
  #chatHeader {
    padding: 0.75rem 0.5rem 0.75rem 1rem;
    padding-top: calc(0.75rem + var(--safe-top));
  }

  #chatHeader button[onclick="resetSession()"],
  #chatHeader button[onclick="logout()"] {
    display: none;
  }

  /* The notification dropdown is a fixed 384px (w-96) panel anchored to the
     bell — wider than the screen it is anchored on. Pinned to the viewport
     gutters instead so it cannot overflow. */
  #notificationDropdown {
    position: fixed;
    top: calc(var(--safe-top) + 4rem);
    left: 0.75rem;
    right: 0.75rem;
    width: auto;
    max-height: 60dvh;
  }

  /* Bottom tab bar: 96px (h-24) is a lot of a 780px viewport, and it needs to
     clear the home indicator. */
  #mobileTabBar {
    height: auto;
    min-height: 60px;
    padding-bottom: calc(0.5rem + var(--safe-bottom));
  }

  /* ------------------------------------------------------------------
     The document surface

     3rem of padding either side is 96px of a 390px screen — a quarter of the
     width, spent on margin, before the page is even drawn. Reduced to a
     gutter, which is also what resolvePdfScale() measures against when it
     works out the fit.

     overflow-x matters as much as the padding: the container scrolled only
     vertically, so an oversized page was clipped with no way to reach the rest
     of it. Left scrollable so the page stays pannable once M2b's zoom can make
     it wider than the screen again.
     ------------------------------------------------------------------ */
  #pdfContainer {
    padding: 0.5rem 0.5rem 1rem;
    overflow: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;

    /* Centring an overflowing child is a trap, and this container did it twice:
       text-align:center on an inline-block page, plus margin:0 auto on the page
       itself. Once zoom makes the page wider than the container, both centre it
       by pushing it equally off BOTH edges — and a scroll container cannot
       scroll to negative offsets, so the left portion of the page becomes
       permanently unreachable. Measured: scrollWidth 742 against clientWidth
       384, and scrollLeft refusing to move off 0.

       `safe` alignment is the fix the spec provides: centre while the item
       fits, fall back to flex-start the moment it would overflow, so the left
       edge always stays at scroll offset 0. */
    display: flex;
    flex-direction: column;
    align-items: safe center;
    text-align: initial;
  }

  /* 20px top / 30px bottom per page, plus the sheet, leaves little of an 844px
     screen for the document itself. Horizontal margin goes to 0 — the flex
     alignment above centres the page now, and an `auto` margin here would
     reintroduce exactly the overflow it is there to avoid. */
  .pdf-page-container {
    margin: 0 0 0.75rem;
    flex-shrink: 0;
  }

  /* The wavy animated border is drawn 18px outside the page on its own canvas,
     redrawn on every animation frame, once per page — fourteen simultaneous
     rAF loops on a phone GPU, to decorate an edge that at this width has no
     room to be seen. */
  .wave-border-canvas {
    display: none;
  }

  /* ------------------------------------------------------------------
     The editing toolbar

     #editorToolbar is `position: absolute; top: 1rem; z-index: 100` and spans
     the column's full width. At 390px it lands directly on top of the document
     app bar and, being the higher layer, swallows every tap meant for it —
     found by a browser run where 60 consecutive clicks on the overflow button
     were intercepted by #autocompleteBtn.

     It is hidden rather than repositioned because its four editing tools
     (add text, add checkbox, clear, save) are the manual PDF placement that
     phones deliberately do not get: placing a text box on a 612pt page through
     a finger is not a thing this product should ship. Its five NON-editing
     actions — send for e-signature, send for review, validate, autocomplete,
     download — are not dropped: they are mirrored into the app bar's overflow
     menu, which clicks through to these same buttons.

     An ID selector (1,0,0) beats the `.hidden` utility (0,1,0) that
     applyDraftStageUI toggles, so this holds whatever that function decides.
     ------------------------------------------------------------------ */
  #editorToolbar {
    display: none;
  }

  /* ==================================================================
     Mobile document app bar
     ================================================================== */
  .mobile-doc-bar {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0 0.5rem;
    padding-top: var(--safe-top);
    height: calc(52px + var(--safe-top));
    flex-shrink: 0;
    background-color: #eef2ff;
    border-bottom: 1px solid #e2e8f0;
    position: relative;
    z-index: 50;
  }

  /* With the chip rail present the bar and the rail are one continuous band of
     #eef2ff, and the rail carries the hairline that closes it. Keeping the
     bar's own border too stacks two 1px lines with a 54px gap between them. */
  .main-container[data-mobile-view="workspace"] .mobile-doc-bar:has(+ .mobile-chips:not([hidden])) {
    border-bottom: none;
  }

  .mobile-doc-bar__icon {
    flex-shrink: 0;
    width: var(--tap);
    height: var(--tap);
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    color: #475569;
    cursor: pointer;
    border-radius: 0.75rem;
  }

  .mobile-doc-bar__icon:active {
    background-color: rgba(99, 102, 241, 0.1);
  }

  .mobile-doc-bar__body {
    flex: 1;
    min-width: 0;
  }

  .mobile-doc-bar__title {
    font-size: 0.875rem;
    font-weight: 700;
    color: #0f172a;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .mobile-doc-bar__subtitle {
    font-size: 0.6875rem;
    color: #64748b;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Empty subtitle would still claim a line box and push the title off centre. */
  .mobile-doc-bar__subtitle:empty {
    display: none;
  }

  .mobile-doc-bar__stage {
    flex-shrink: 0;
    font-size: 0.5625rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    color: #6366f1;
    background: #ffffff;
    border: 1px solid #c7d2fe;
    padding: 0.25rem 0.4375rem;
    border-radius: 0.375rem;
  }

  /* The stage badge turns red once the packet is locked, matching the locked
     banner rather than sitting indigo next to a red warning. */
  .mobile-doc-bar__stage[data-locked="true"] {
    color: #dc2626;
    border-color: #fecaca;
  }

  /* Tab labels: 10px/700 sentence case, per the design. The desktop classes
     render 12px uppercase with wide tracking, which at 390px cannot fit
     "Contracts" on one line once the bar's padding and gaps are taken. */
  .tab-label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    line-height: 1.2;
  }

  /* ==================================================================
     M5 — Tabbed screens

     Contracts, MLS and Settings are desktop pages inside the phone shell:
     p-10 gutters (40px, a fifth of the screen each side), header rows that put
     a title and a button side by side, and grids sized for a 600px column.
     None of them had a single phone rule.

     Structural first — gutters, stacking, grids — then the per-screen pieces
     the artboards call for.
     ================================================================== */

  /* The scroll pane inside each tab. 40px each side leaves 310px of a 390px
     screen; the artboards use a 16px gutter throughout. */
  #contractsDashboard,
  #contractsDraftsList,
  #contractsSentList,
  #contractsReceivedList,
  #mlsTab > div,
  #settingsTab > div {
    padding: 16px !important;
  }

  /* Settings centres a max-w-4xl column with a 40px top margin — on a phone the
     column IS the screen and the margin is a hole under the header. */
  #settingsTab .max-w-4xl {
    max-width: none;
    margin-top: 0;
  }

  /* The sub-tab pill above already names the view, so repeating it as a heading
     and hanging a Refresh bar under it costs ~130px — a third of the first
     screenful — to say "Active Drafts" a second time. The lists reload on open
     and on every agent turn; a manual refresh is a desk affordance. */
  #contractsDraftsList > .flex.justify-between,
  #contractsSentList > .flex.justify-between,
  #contractsReceivedList > .flex.justify-between {
    display: none;
  }

  /* Header rows: "Dashboard" beside a Refresh button, "MLS Documents" beside an
     Upload button. At 390px the button wraps to two lines and shoves the title.
     The artboards stack them, with the action full-width beneath. */
  #contractsDashboard > .flex.justify-between {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    margin-bottom: 16px;
  }

  /* MLS keeps its header on one row: the upload control is a 36px icon here,
     not a labelled bar, so there is nothing to stack. */
  #mlsTab .flex.justify-between.items-center.mb-10 {
    margin-bottom: 14px;
    align-items: center;
  }

  /* "PROCESSED LISTINGS" duplicates the count the list already prints, and the
     screen title already says these are listings. */
  #mlsTab .text-sm.font-semibold.text-slate-500.uppercase {
    display: none;
  }

  /* The action in those rows becomes the full-width bar the artboards draw. */
  #contractsDashboard > .flex.justify-between button {
    width: 100%;
    justify-content: center;
    height: 48px;
    border-radius: 13px;
  }

  /* Screen titles: the artboards set Outfit 22px. text-3xl is 30px, which wraps
     "Sent for signature" onto two lines before the button is even considered. */
  #contractsTab h2.font-outfit,
  #mlsTab h2.font-outfit,
  #settingsTab h1,
  #settingsTab h2.font-outfit {
    font-size: 22px;
    line-height: 1.2;
  }

  /* ---- Contracts sub-tab pills ----
     A horizontally scrolling rail. Left inset only, so the last pill bleeds off
     the edge and the rail reads as scrollable — the same rule the document chip
     rail follows. */
  #contractsTab > div:first-child {
    padding: 12px 0 12px 16px !important;
    margin-top: 0 !important;
    background: #eef2ff;
    border-bottom: 1px solid #e2e8f0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  #contractsTab > div:first-child::-webkit-scrollbar {
    display: none;
  }

  #contractsTab > div:first-child > .flex {
    flex-wrap: nowrap;
  }

  #contractsTab > div:first-child button {
    flex-shrink: 0;
    height: 36px;
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }

  /* ---- Stat tiles ----
     The dashboard groups render as multi-column grids sized for the desktop
     column. Two up is the artboard's layout and the most a 390px screen holds
     without the number and its label competing. */
  #dashboardContent .grid {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 10px !important;
  }

  /* ---- Table rows become cards ----
     Every list row on these screens is a flex row with the meta on the left and
     an action on the right, which at 390px collapses into an unreadable column
     of truncated text. Stacked, with the action full-width beneath its content. */
  #contractsDraftsList .flex.items-center.gap-2.ml-4,
  #contractsSentList .flex.items-center.gap-2.ml-4,
  #draftsList .flex.items-center.gap-2.ml-4 {
    margin-left: 0;
    width: 100%;
  }

  #draftsList .flex.items-center.gap-2.ml-4 button,
  #contractsSentList .flex.items-center.gap-2.ml-4 button {
    flex: 1;
    justify-content: center;
    min-height: 40px;
  }

  /* ---- MLS ---- */
  #mlsDocumentList > div,
  #mlsProcessingList > div {
    padding: 13px 14px;
    border-radius: 16px;
  }

  /* ==================================================================
     Contracts — C2 progress cards
     ================================================================== */
  .deal-list {
    display: flex;
    flex-direction: column;
    gap: 11px;
  }

  .deal-card {
    background: #ffffff;
    border: 1px solid #c7d2fe;
    border-radius: 18px;
    padding: 14px 15px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  /* A draft with no address yet. Dimmed rather than hidden — it is still real
     work, it just has not become a deal. */
  .deal-card--unnamed {
    border-color: #e2e8f0;
  }

  .deal-card--unnamed .deal-card__title {
    color: #94a3b8;
  }

  .deal-card__head {
    display: flex;
    align-items: flex-start;
    gap: 10px;
  }

  .deal-card__body {
    flex-grow: 1;
    min-width: 0;
  }

  .deal-card__title {
    font-size: 16px;
    font-weight: 700;
    color: #0f172a;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .deal-card__sub {
    font-size: 12px;
    color: #64748b;
    margin-top: 2px;
  }

  .deal-card__stage {
    flex-shrink: 0;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 0.08em;
    padding: 5px 8px;
    border-radius: 7px;
  }

  .deal-card__progress {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .deal-card__progress-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }

  .deal-card__count {
    font-size: 12px;
    font-weight: 700;
    color: #3730a3;
    font-variant-numeric: tabular-nums;
  }

  .deal-card--unnamed .deal-card__count {
    color: #64748b;
  }

  .deal-card__remaining {
    font-size: 11px;
    font-weight: 600;
    color: #6366f1;
  }

  .deal-card--unnamed .deal-card__remaining {
    color: #94a3b8;
  }

  .deal-card__track {
    height: 6px;
    border-radius: 999px;
    background: #e0e7ff;
    overflow: hidden;
    display: flex;
  }

  .deal-card--unnamed .deal-card__track {
    background: #f1f5f9;
  }

  .deal-card__fill {
    background: linear-gradient(90deg, #6366f1, #4f46e5);
    border-radius: 999px;
  }

  .deal-card__foot {
    display: flex;
    align-items: center;
    gap: 9px;
  }

  .deal-card__meta {
    flex-grow: 1;
    font-size: 11px;
    color: #94a3b8;
  }

  .deal-card__action {
    flex-shrink: 0;
    height: 36px;
    padding: 0 15px;
    border-radius: 11px;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    color: #475569;
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
  }

  .deal-card__action--primary {
    background: #4f46e5;
    border-color: #4f46e5;
    color: #ffffff;
  }

  .deal-empty {
    padding: 28px 16px;
    text-align: center;
    font-size: 13px;
    color: #94a3b8;
    background: #ffffff;
    border: 1px dashed #cbd5e1;
    border-radius: 16px;
  }

  .deal-more {
    padding: 14px 0 4px;
    text-align: center;
    font-size: 11px;
    color: #94a3b8;
  }

  /* ==================================================================
     MLS — M1 search-first list
     ================================================================== */
  .mls-search {
    height: 44px;
    background: #ffffff;
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 0 13px;
    margin-bottom: 2px;
  }

  .mls-search input {
    flex-grow: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-size: 16px;
    color: #0f172a;
  }

  .mls-count {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.1em;
    color: #94a3b8;
    padding: 0 2px 2px;
  }

  .mls-list {
    display: flex;
    flex-direction: column;
    gap: 7px;
  }

  .mls-row {
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 14px;
    padding: 12px 13px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }

  .mls-row:active {
    background: #f8fafc;
  }

  /* An author `display: flex` outranks the UA rule that [hidden] relies on, so
     filtered-out rows stayed on screen while the count said otherwise. */
  .mls-row[hidden] {
    display: none;
  }

  .mls-row__top {
    display: flex;
    align-items: baseline;
    gap: 8px;
  }

  .mls-row__addr {
    flex-grow: 1;
    min-width: 0;
    font-size: 15px;
    font-weight: 700;
    color: #0f172a;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .mls-row__price {
    flex-shrink: 0;
    font-family: "Outfit", sans-serif;
    font-size: 15px;
    font-weight: 700;
    color: #0f172a;
    font-variant-numeric: tabular-nums;
  }

  .mls-row__meta {
    font-size: 12px;
    color: #64748b;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Upload is demoted to a 36px button beside the title — this screen is for
     finding a listing, not for adding one. */
  #mlsTab #openMlsUploadModalBtn.mls-upload-compact {
    width: 36px;
    height: 36px;
    min-width: 36px;
    padding: 0;
    border-radius: 11px;
    gap: 0;
    font-size: 0;
  }

  /* ==================================================================
     Settings — account actions

     Settings.dc.html: two full-width 48px rows at the foot of the tab. The
     log-out row is the only exit from the account on a phone, so it is not
     allowed to live anywhere conditional.
     ================================================================== */
  .mobile-account-actions {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 18px 16px 8px;
  }

  .mobile-account-row {
    height: 48px;
    border-radius: 13px;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    color: #475569;
    cursor: pointer;
  }

  .mobile-account-row:active {
    background: #f8fafc;
  }

  .mobile-account-row--danger {
    border-color: #fecaca;
    color: #ef4444;
  }

  .mobile-account-row--danger:active {
    background: #fef2f2;
  }

  /* ==================================================================
     Doc tab

     The fifth tab the design's tabbed screens carry. Reveals what the
     index.html base rule hides, so it exists only at this width.
     ================================================================== */
  .tab-doc-mobile {
    display: flex;
    position: relative;
  }

  /* Indigo dot for a document the agent filled while you were somewhere else —
     the same signal the chip rail and the desktop rail's row badge carry, at
     the one place you can see it from any tab. */
  .tab-doc-unseen {
    position: absolute;
    top: 12px;
    right: 22px;
    width: 8px;
    height: 8px;
    border-radius: 999px;
    background: #6366f1;
    animation: unseenPulse 2s ease-in-out infinite;
  }

  .tab-doc-unseen[hidden] {
    display: none;
  }

  /* No draft yet: the tab has nothing to open. Dimmed rather than removed so
     the bar does not change width the moment a contract is started. */
  .tab-doc-mobile:disabled {
    opacity: 0.4;
  }

  /* ==================================================================
     Document chip rail (A1)

     The desktop rail is 234px of vertical list. Here it is a 44px horizontal
     strip: a chip per document, scrolling sideways, always visible. It costs
     ~54px of height permanently, which is the trade the chosen design makes in
     exchange for keeping fill status and the unseen dot on screen rather than
     a tap away.
     ================================================================== */
  .mobile-chips {
    display: flex;
    gap: 7px;
    align-items: center;
    flex-shrink: 0;
    padding: 0 0.875rem 10px;
    background-color: #eef2ff;
    border-bottom: 1px solid #e2e8f0;
    overflow-x: auto;
    overflow-y: hidden;
    /* Stops a sideways flick through the chips from turning into a back-swipe
       or a bounce of the whole shell. */
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    position: relative;
    z-index: 45;
  }

  .mobile-chips[hidden] {
    display: none;
  }

  .mobile-chips::-webkit-scrollbar {
    display: none;
  }

  .mobile-chip {
    flex-shrink: 0;
    height: var(--tap);
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 0 13px;
    border-radius: 12px;
    border: 1px solid transparent;
    background: rgba(255, 255, 255, 0.55);
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    color: #475569;
    cursor: pointer;
    /* Long addendum names would otherwise push every other chip off screen. */
    max-width: 11rem;
  }

  .mobile-chip__label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .mobile-chip--active {
    background: #ffffff;
    border-color: #c7d2fe;
    color: #3730a3;
    font-weight: 700;
    box-shadow: 0 2px 8px rgba(79, 70, 229, 0.1);
  }

  /* Same three colours the rail's .doc-status-dot uses, so "amber means
     partly filled" means the same thing on both layouts. */
  .mobile-chip__dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex-shrink: 0;
  }

  .mobile-chip__dot--filled {
    background-color: #22c55e;
  }

  .mobile-chip__dot--partial {
    background-color: #f59e0b;
  }

  .mobile-chip__dot--empty {
    background-color: #cbd5e1;
  }

  /* The agent wrote into this document while you were looking at another one.
     Same indigo pulse as the rail's ● badge. */
  .mobile-chip__unseen {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background-color: #6366f1;
    flex-shrink: 0;
    animation: unseenPulse 2s ease-in-out infinite;
  }

  /* A document added a moment ago, still having its values copied across from
     the contract by the worker. */
  .mobile-chip__spinner {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    flex-shrink: 0;
    border: 1.5px solid #e2e8f0;
    border-top-color: #6366f1;
    animation: doc-modal-spin 0.7s linear infinite;
  }

  .mobile-chip--seeding .mobile-chip__label {
    color: #6366f1;
  }

  .mobile-chip__failed {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    flex-shrink: 0;
    background-color: #fee2e2;
    color: #dc2626;
    font-size: 9px;
    font-weight: 800;
    line-height: 12px;
    text-align: center;
  }

  .mobile-chip--failed .mobile-chip__label {
    color: #dc2626;
  }

  /* Trailing add chip. Dashed to read as an affordance rather than a document
     that happens to be called "Add". */
  .mobile-chip--add {
    border: 1.5px dashed #cbd5e1;
    background: transparent;
    color: #6366f1;
    font-weight: 700;
    padding: 0 12px;
  }

  .mobile-chip--add:disabled {
    opacity: 0.4;
    cursor: default;
  }

  /* ==================================================================
     Page pager + zoom

     Sits above the chat sheet, centred. Dark and translucent so it reads as
     chrome floating over the document rather than part of it — the page
     underneath is white and any light control would look like a form field
     printed on the contract.
     ================================================================== */
  .mobile-pager {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--sheet-peek) + 12px);
    z-index: 55;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 4px;
    border-radius: 999px;
    background: rgba(15, 23, 42, 0.86);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.28);
    color: #ffffff;
  }

  .mobile-pager[hidden] {
    display: none;
  }

  .mobile-pager__btn {
    width: var(--tap);
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    color: #ffffff;
    cursor: pointer;
    border-radius: 999px;
  }

  .mobile-pager__btn:disabled {
    color: rgba(255, 255, 255, 0.3);
    cursor: default;
  }

  .mobile-pager__btn:not(:disabled):active {
    background: rgba(255, 255, 255, 0.14);
  }

  .mobile-pager__label {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    min-width: 3.25rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
  }

  .mobile-pager__sep {
    opacity: 0.5;
    margin: 0 1px;
  }

  

  



  .mobile-doc-menu {
    position: absolute;
    top: calc(52px + var(--safe-top) + 0.25rem);
    right: 0.5rem;
    z-index: 70;
    min-width: 11rem;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 0.875rem;
    box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  .mobile-doc-menu[hidden] {
    display: none;
  }

  .mobile-doc-menu__item {
    min-height: var(--tap);
    padding: 0 1rem;
    background: transparent;
    border: none;
    border-bottom: 1px solid #f1f5f9;
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    color: #334155;
    text-align: left;
    cursor: pointer;
  }

  .mobile-doc-menu__item:last-child {
    border-bottom: none;
  }

  .mobile-doc-menu__item:active {
    background-color: #f8fafc;
  }

  .mobile-doc-menu__item--danger {
    color: #ef4444;
  }

  /* A source button that is present but disabled — a locked draft's
     autocomplete, say. Shown rather than hidden so the action's absence reads
     as "not right now" instead of the menu silently changing shape. */
  .mobile-doc-menu__item--disabled {
    color: #cbd5e1;
    cursor: default;
  }

  .mobile-doc-menu__item[hidden] {
    display: none;
  }

  /* ------------------------------------------------------------------
     Modals

     Desktop centres a 900px card with 2.5rem padding. Until each modal gets
     its designed sheet treatment (M3 for add-document, M5 for the MLS
     property card), this keeps them usable rather than clipped: full width,
     gutters that clear the notch, and a height that respects the safe area.
     ------------------------------------------------------------------ */
  /* ==================================================================
     Modals become bottom sheets

     Every desktop modal here is a centred card — 900px for .modal-content,
     min(560px, 92vw) for .doc-modal. Widening them to fit 390px made them
     usable but not right: a centred card on a phone puts its controls in the
     middle of the screen and its dismiss out of thumb reach, and it animates
     from nowhere.

     Anchored to the bottom instead, with the action pinned and the body
     scrolling under it — so "Send Request" is where a thumb already is, and a
     long form cannot bury it below the fold.
     ================================================================== */
  .modal-overlay {
    align-items: flex-end;
  }

  .modal-content,
  .doc-modal {
    width: 100%;
    max-width: none;
    max-height: 88dvh;
    margin: 0;
    padding: 0 1.125rem;
    /* No bottom padding: a sheet with a pinned footer gets its own clearance
       from the footer itself (below). Reserving 22px for a 79px footer left the
       last field permanently hidden behind it. */
    padding-bottom: 0;
    border-radius: 22px 22px 0 0;
    border-bottom: none;
    box-shadow: 0 -12px 40px rgba(15, 23, 42, 0.3);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* The scrollbar was taking 6px out of the content box on the right, so
       every block in the sheet sat at left 19 / right 25 — a visible skew that
       reads as "the text is not aligned". Phones render an overlay scrollbar
       that takes no space; matching that here makes the gutters symmetrical. */
    scrollbar-width: none;
  }

  .modal-content::-webkit-scrollbar,
  .doc-modal::-webkit-scrollbar {
    width: 0;
    height: 0;
  }

  /* The grab handle. Drawn, not draggable — these sheets dismiss by their own
     Cancel and by tapping the scrim, and a handle that looks draggable but is
     not is worse than none. Sticky so it stays put as the body scrolls. */
  .modal-content:not(:has(.sheet-footer)),
  .doc-modal:not(:has(.sheet-footer)) {
    padding-bottom: calc(1.375rem + var(--safe-bottom));
  }

  /* Painted as a full-width band rather than a bare pill: sticky with a
     transparent background, the header scrolled underneath it and the two
     overlapped. The band carries the sheet's own white so content disappears
     cleanly under it, with the pill drawn into it by the gradient layer. */
  .modal-content::before {
    content: "";
    position: sticky;
    top: 0;
    z-index: 3;
    align-self: stretch;
    flex-shrink: 0;
    height: 20px;
    margin: 0 -1.125rem 0.5rem;
    background:
      linear-gradient(#cbd5e1, #cbd5e1) no-repeat center 8px / 38px 4px,
      #ffffff;
  }

  /* A sheet rises; it does not zoom in from the middle of the screen. */
  .modal-content,
  .doc-modal {
    transform: translateY(100%);
  }

  .modal-overlay.active .modal-content,
  .doc-modal-overlay.is-open .doc-modal {
    transform: translateY(0);
  }

  /* ==================================================================
     M6 — Auth (login, register, invite)

     Per Login.dc.html and Entry.dc.html. The dark violet identity is the
     point of these screens and is deliberately NOT normalised toward the
     app's light chrome: what changes here is size, spacing and the shape of
     the scroll, not the palette.

     The artboard paints the card over a flat violet gradient; the real page
     paints it over the particle canvas (#background-canvas, fixed, z-index
     -1) with a translucent card on top. The gradient in the drawing is a
     still of that canvas, so the translucency stays and only the side-panel
     treatment — the right border and the 10px lateral shadow that made sense
     beside a canvas — comes off.

     Hooked on `.login-card`, which is the shell on both login.html and
     invite.html.
     ================================================================== */
  .login-card {
    /* h-screen is the same 100vh trap M0 fixed for the workspace: on mobile
       Safari the card would be taller than the visible viewport, and the
       sign-in button would sit under the URL bar. */
    height: 100dvh;

    /* The markup asks for `overflow-y-auto` and does not get it: login.css
       sets `.login-card { overflow: hidden }` at equal specificity and loads
       after tailwind.css, so the card is a clipping box, not a scrolling one.
       An element with overflow:hidden still scrolls PROGRAMMATICALLY, which
       is how this survived a check — `scrollTop = 120` moves it, a finger
       does not. Anything past the fold was unreachable: on a 375x667 phone
       the register form is ~83px taller than the card, and login plus its
       "account pending approval" banner is ~623px in a 553px card, clipped
       at BOTH ends because the content is centred. */
    overflow-y: auto;

    /* And centring is only safe while the content fits. Past that, a centred
       flex child overflows equally in both directions and no scroll container
       can reach a negative offset — trap 7, on the axis M2 hit it on. `safe`
       keeps the centring and drops it the moment it would cost reachability. */
    justify-content: safe center;
    padding: calc(28px + var(--safe-top)) 28px calc(28px + var(--safe-bottom));
    border-right: none;
    box-shadow: none;
  }

  /* The bot. 120px square today; the artboard draws a 96x80 mark, which is
     what leaves room for the heading and both fields above the fold. */
  .bot-login-canvas {
    width: 96px;
    height: 80px;
  }

  .login-card h2 {
    font-size: 27px;
    margin-bottom: 5px;
  }

  /* The line under the heading — "Sign in to continue to Xrep.AI". */
  .login-card h2 + p {
    margin-bottom: 26px;
  }

  /* Field labels. The page shouts them (12px bold uppercase, wide tracking);
     the artboard says them (13px medium, sentence case, one step lighter
     against the card). Four labels stacked in caps is a lot of noise for
     "Email" and "Password". */
  .login-card label {
    font-size: 13px;
    font-weight: 500;
    color: #cbd5e1;
    text-transform: none;
    letter-spacing: normal;
    margin-bottom: 7px;
  }

  /* 50px and a 10px radius, per the redline. The 16px that stops iOS zooming
     comes from the floor at the top of this file. The monospace face goes:
     it is a hand-me-down from the app's field overlays and an email address
     is not code. */
  .login-card input {
    height: 50px;
    padding: 0 16px;
    border-radius: 10px;
    font-family: inherit;
  }

  .login-card button[type="submit"],
  .login-card #accept-button {
    height: 54px;
    border-radius: 10px;
  }

  /* 16px between fields, 14px on register where there is one more of them.

     `:is()` is here to win, not to shorten: Tailwind's own
     `.space-y-5 > :not([hidden]) ~ :not([hidden])` scores (0,3,0) because
     each `:not([hidden])` carries attribute weight, so the obvious
     `.login-card form.space-y-5 > * + *` (0,2,1) loses and this rule did
     nothing at all — login rendered at Tailwind's 20px while the redline and
     the status doc both said 16. `:is()` takes the specificity of its most
     specific argument, so repeating the class lifts this to (0,3,1). */
  :is(.login-card.login-card) form:is(.space-y-5, .space-y-4) > * + *:not(.auth-actions) {
    margin-top: 16px;
  }

  :is(.login-card.login-card) #signup-form form.space-y-4 > * + *:not(.auth-actions) {
    margin-top: 14px;
  }

  /* First and last name share a row. `gap-4` is 16px against the drawing's
     10px, which is the difference between two comfortable fields and two
     that each lose a character. */
  .login-card .grid-cols-2,
  .login-card .flex.gap-3 {
    gap: 10px;
  }

  /* The switch between "I have an account" and "I don't". 12px is a footnote;
     it is the only other thing you can do on this screen. `mt-6` is the same
     row on invite.html. */
  .login-card .mt-8 {
    /* Zeroed because `.auth-actions` supplies the 16px gap instead. */
    margin-top: 0;
    font-size: 14px;
  }

  /* Invite has no `.auth-actions` wrapper — its switch-link is a bare sibling
     of the field column, so `mt-6` was its ONLY spacing and zeroing it put the
     link flush against the button, hit area touching hit area. It keeps a gap
     of its own; the drawing says 16px. */
  .login-card .mt-6 {
    margin-top: 16px;
    font-size: 14px;
  }

  .login-card .mt-8 a,
  .login-card .mt-6 a {
    font-size: 14px;
  }

  /* `text-slate-500` (#64748b) against the card is about 3.4:1 — under the
     4.5:1 floor for body text, on the only other action these screens offer.
     The drawing's #94a3b8 clears it. */
  .login-card .mt-8,
  .login-card .mt-6 {
    color: #94a3b8;
  }

  /* Invite is drawn on the same dark violet card as login, but it never loads
     magnetic_background.js — so the translucent card had nothing behind it
     but the white body and read as flat slate. Where the canvas is absent the
     card paints the gradient itself; where it is present this does not match
     and the canvas still shows through. */
  body:not(:has(#background-canvas)) .login-card {
    background: radial-gradient(
      120% 80% at 20% 0%,
      #2e1065 0%,
      #0f172a 55%,
      #020617 100%
    );
  }

  /* ------------------------------------------------------------------
     The action block, pinned on register only

     Login is four elements and fits; the artboard centres it and leaves the
     button in flow. Register is seven fields and scrolls, and the drawing
     pins its footer over a scrim so "Sign up" is on screen the whole way
     down — including with the keyboard up, which is when the button would
     otherwise be furthest away.

     `:has()` reads the same class the page's own toggleForms() flips, so
     there is no second source of truth for which form is showing.
     ------------------------------------------------------------------ */
  /* The action block is excluded from the field-gap rules above and carries its
     own top margin. It has to be: those rules are `:is()`-boosted to beat
     Tailwind's `space-y-*`, which also made them beat `margin-top: auto` on
     this element — so the register CTA quietly stopped being pushed to the
     card's bottom edge and sat wherever the fields ended. Caught by the WebKit
     pass, present in both engines. */
  .auth-actions {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-top: 16px;
  }

  /* The footer supplies the card's bottom padding in this mode, so the card
     must not also supply it — otherwise the pinned block floats 28px above
     the bottom edge with card showing underneath it. */
  body:has(#signup-form:not(.hidden)) .login-card {
    padding-bottom: 0;
    /* Register always starts at the top rather than relying on `safe center`
       to give up: the form is the screen, and a short one drifting to the
       middle under a bottom-pinned footer looks like a rendering fault. */
    justify-content: flex-start;
  }

  /* Sticky alone is not enough: it only pins something that would otherwise
     scroll, so on a tall phone where the seven fields happen to fit, the
     block landed wherever the content ended and its scrim read as a dark bar
     across the middle of the card. The form is stretched to the card's full
     height and the block pushed to the bottom of it, so the footer is at the
     bottom edge whether the content scrolls or not. */
  body:has(#signup-form:not(.hidden)) #signup-form,
  body:has(#signup-form:not(.hidden)) #signup-form > form {
    display: flex;
    flex-direction: column;
    flex: 1 0 auto;
  }

  body:has(#signup-form:not(.hidden)) .auth-actions {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin-top: auto;
    /* Bleeds to the card's edges so the scrim covers the full width, and
       carries the card's bottom padding so the last field scrolls out from
       under the button rather than being trapped behind it. */
    margin-left: -28px;
    margin-right: -28px;
    /* 20px, or the home indicator's height where there is one — the drawing's
       34px is that indicator on the phone it was drawn at. */
    padding: 16px 28px max(20px, var(--safe-bottom));
    /* The drawing stops at 0.9, which is enough over a flat mock and not
       enough over the real thing: a field's placeholder read straight through
       the band beside "Already have an account?". It carries on to nearly
       solid at the bottom edge. */
    background: linear-gradient(
      180deg,
      rgba(2, 6, 23, 0) 0%,
      rgba(2, 6, 23, 0.92) 40%,
      rgba(2, 6, 23, 0.99) 100%
    );
  }

  /* On register the mark stops being the hero and becomes a header row: the
     screen's job there is the form, and 80px of bot above seven fields is
     80px the fields do not get. */
  body:has(#signup-form:not(.hidden)) .bot-login-canvas {
    width: 44px;
    height: 37px;
  }

  body:has(#signup-form:not(.hidden)) .login-card > .flex.justify-center {
    margin-bottom: 14px;
  }

  body:has(#signup-form:not(.hidden)) .login-card h2 {
    font-size: 25px;
  }

  /* ==================================================================
     M6 — Legal pages (terms, privacy, esign-consent)

     Per Entry.dc.html's "Legal page template": one template for all three,
     in the warm landing system rather than the app's cool one. The colours
     below are the drawing's, reached through the variables the landing pages
     already define — #f7f7f5 is --canvas, #e6e6e3 is --border, #1a1a1a is
     --text, #5c5c57 is --text-sec.

     The substance of this screen is reading, so the only real work is the
     measure and the type: `.legal` sets 15px for both paragraphs and list
     items, which is a desktop size read at desktop distance. The artboard
     lifts body copy to 17px and darkens it to --text, and leaves list items
     a step down at 16px/--text-sec, so the two stop being the same thing.
     ================================================================== */
  .legal p {
    font-size: 17px;
    line-height: 1.65;
    color: var(--text);
  }

  .legal li {
    font-size: 16px;
    line-height: 1.6;
  }

  .legal h2 {
    font-size: 20px;
    margin-top: 36px;
  }

  .legal h3 {
    font-size: 16px;
    margin-top: 24px;
  }

  /* These three pages and landing.html share `.xrep`, so anything hung on
     that class alone lands on the marketing page too — which is how the
     landing hero, drawn at 44px, first came out at 28px. `:has(.legal)`
     names the actual thing: a page whose body is a legal document. */

  /* 44px of title is a hero on a desktop page and a wall on a phone. */
  body:has(.legal) main h1 {
    font-size: 28px;
    line-height: 1.12;
  }

  /* 80px of air above the title costs a third of the first screen. */
  body:has(.legal) main .section-y {
    padding-block: 30px 40px;
  }

  /* 54px, per both Entry.dc.html and Landing.dc.html — the marketing header
     is the same header. Both artboards also reserve a 44px status-bar block
     above it, which is what `--safe-top` is: every one of these pages sets
     `viewport-fit=cover`, so without the inset a sticky `top: 0` header
     renders UNDER the notch and the wordmark and demo pill are clipped. */
  .xrep header[data-nav] {
    padding-top: var(--safe-top);
  }

  .xrep header nav {
    height: 54px;
    /* The drawn header is `padding: 0 8px 0 24px` — 8px on the right because
       the 44px control carries its own padding. `.container-x` gives 24px
       both sides, which pushed the control in from the edge. */
    padding-right: 8px;
  }

  /* The menu control, built by mobile-nav.js. 44x44 with a 20px glyph, last
     in the row after the demo pill, exactly as both artboards draw it. */
  .nav-menu-btn {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: none;
    background: none;
    color: var(--text);
  }

  .nav-menu-btn svg {
    width: 20px;
    height: 20px;
  }

  /* The drawn phone header is wordmark + demo pill + control. "Log in" is not
     in it; as built it is a 21px tap target squeezed beside the pill. It gets
     a full row in the menu instead. */
  .xrep header nav > div > a[href="/login_page"] {
    display: none;
  }

  /* ------------------------------------------------------------------
     Tap targets on the marketing chrome

     M0 set a 44px floor and applied it to the app's controls; these pages
     were linked to this file without that pass. "Log in" is a 21px target,
     the footer's legal links are 16-20px and stacked 10px apart, which is a
     mis-tap between Privacy and Terms.

     The floor is applied as hit area, not as size: the artboard deliberately
     draws the demo pill at 34px, so the pill keeps its drawn height and gains
     reach around it.
     ------------------------------------------------------------------ */
  .xrep header nav a,
  .xrep footer a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  /* ------------------------------------------------------------------
     The section bar

     Eighteen numbered sections on terms.html, and on a phone the only way to
     the one you want is a long scroll past seventeen others. The drawing
     pins a bar: the sections themselves, and a way back to the top.

     Built by mobile-legal.js from the page's own <h2>s, so a section added
     to the document appears here with no second list to maintain.
     ------------------------------------------------------------------ */
  .legal-bar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    display: flex;
    gap: 10px;
    padding: 14px 24px max(20px, var(--safe-bottom));
    background: linear-gradient(
      180deg,
      rgba(247, 247, 245, 0) 0%,
      var(--canvas) 40%
    );
  }

  .legal-bar__sections,
  .legal-bar__top {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    height: 46px;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--surface);
    font-size: 14px;
    font-weight: 500;
    color: var(--text);
  }

  .legal-bar__sections {
    flex: 1;
  }

  .legal-bar__top {
    width: 46px;
    flex-shrink: 0;
  }

  .legal-bar svg {
    width: 17px;
    height: 17px;
    flex-shrink: 0;
  }

  /* The bar floats over the document, so the document has to end above it —
     otherwise the last paragraph of a legal agreement is the one you cannot
     finish reading. */
  /* Derived from the bar, not guessed: 14px top pad + 46px button + the
     bottom pad, which is itself `max(20px, --safe-bottom)`. The magic 92px it
     replaces was 2px short of the bar's real height on a notched phone — the
     one number in this file that the comment above it justifies as "otherwise
     the last paragraph of a legal agreement is the one you cannot finish
     reading". */
  body.xrep:has(.legal) {
    padding-bottom: calc(60px + max(20px, var(--safe-bottom)) + 12px);
  }

  /* The warm-palette list sheet. Same bottom-sheet shape as the app's, in the
     marketing system's colours. Shared by two things that are the same thing:
     the legal pages' section list (mobile-legal.js) and the header nav menu
     (mobile-nav.js) — both are "pick a destination from a list". */
  .xrep-sheet-overlay {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: flex-end;
    background: rgba(26, 26, 26, 0.4);
    opacity: 0;
    transition: opacity 0.2s ease;
  }

  .xrep-sheet-overlay.is-open {
    opacity: 1;
  }

  .xrep-sheet {
    width: 100%;
    max-height: 72dvh;
    display: flex;
    flex-direction: column;
    padding: 0 20px max(20px, var(--safe-bottom));
    background: var(--canvas);
    border-radius: 18px 18px 0 0;
    transform: translateY(100%);
    transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
  }

  .xrep-sheet-overlay.is-open .xrep-sheet {
    transform: translateY(0);
  }

  .xrep-sheet::before {
    content: "";
    align-self: center;
    flex-shrink: 0;
    width: 38px;
    height: 4px;
    margin: 8px 0 12px;
    border-radius: 2px;
    background: var(--border-strong);
  }

  .xrep-sheet__title {
    flex-shrink: 0;
    padding: 2px 4px 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-mut);
  }

  .xrep-sheet__list {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }

  .xrep-sheet__list::-webkit-scrollbar {
    width: 0;
  }

  .xrep-sheet__row {
    display: block;
    width: 100%;
    min-height: 48px;
    padding: 12px 4px;
    border: none;
    border-bottom: 1px solid var(--border);
    background: none;
    font-size: 16px;
    line-height: 1.35;
    text-align: left;
    color: var(--text);
  }

  .xrep-sheet__row:last-child {
    border-bottom: none;
  }

  .xrep-sheet:focus {
    outline: none;
  }

  /* ==================================================================
     M6 — Landing

     Per Landing.dc.html. Most of this page already answers the phone: the
     hero is one column, the benefit list stacks, the header is the shared
     54px bar handled above. What is left is the lead paragraph and the two
     tables.
     ================================================================== */

  /* 19px against the drawing's 17px. The hero h1 is already clamp()'d to 44px
     at this width and is left alone. */
  body.xrep:not(:has(.legal)) main h1 + p {
    font-size: 17px;
  }

  /* "Phone floor of 44px, tighter block padding", per the artboard's own
     note. 80px above and below every block is desktop air: it puts a third
     of a screen between the header and the headline, and again between every
     section, so the page reads as mostly empty. The drawing sets 40px around
     the hero and 36px on the blocks under it. */
  /* The hero. Split by `:has(h1)` because the page's first section holds TWO
     `.container-x` blocks — the hero and KEY BENEFITS — and the artboard gives
     them different values: 40px around the hero, 36px above the benefits and
     nothing below. A plain descendant selector gave both 40/40. */
  body.xrep:not(:has(.legal)) main > section:first-child .container-x:has(h1) {
    padding-top: 40px;
    padding-bottom: 40px;
  }

  body.xrep:not(:has(.legal))
    main
    > section:first-child
    .container-x:not(:has(h1)) {
    padding-top: 36px;
    padding-bottom: 0;
  }

  /* The single largest block of desktop air on the phone page: the closing CTA
     is `py-24` (96px top and bottom at this width) on a section that carries
     no `.section-y`, so the rule below never reached it. */
  body.xrep:not(:has(.legal)) main .container-x.py-24 {
    padding-top: 40px;
    padding-bottom: 40px;
  }

  body.xrep:not(:has(.legal)) main .section-y {
    padding-block: 36px;
  }

  /* ------------------------------------------------------------------
     The status tables

     Both are fixed-width tables in a horizontally scrolling box: the review
     table is `min-w-[680px]` and the brokerage table `min-w-[480px]`, inside
     a 340px column. On a phone that is a sideways scroll to read a row, with
     the header scrolled out of sight by the time you reach the last column —
     and the artboard rejects it outright: "stacked rows, one per deal".

     Rebuilt as rows without touching the markup. The status cell is the only
     cell holding an element rather than text, so `:has(> span)` names it
     without a nth-child count that would be wrong for the second table,
     where the pill is last rather than second.

     DEVIATION, deliberate: the drawing's row is name + pill and nothing
     else. Followed literally that drops Counterparty, Signers and Updated
     from one table and Role and Templates from the other — the drawing's
     mock has three columns where the page has five. They are kept as a
     muted second line instead, so the phone shows the same facts as the
     desktop table.
     ------------------------------------------------------------------ */
  body.xrep:not(:has(.legal)) .overflow-x-auto:has(> table) {
    overflow-x: visible;
  }

  body.xrep:not(:has(.legal)) .overflow-x-auto > table {
    display: block;
    width: 100%;
    min-width: 0;
  }

  /* The column names go with the columns; each value carries its own meaning
     in a row this short. */
  body.xrep:not(:has(.legal)) .overflow-x-auto > table thead {
    display: none;
  }

  /* No border and no radius of its own: both tables already sit inside a
     browser-chrome frame that draws `rounded-[var(--r-lg)] border border-border`
     with no gap, so a card here produced a doubled hairline and two concentric
     12px corners. The frame IS the card the artboard draws. */
  body.xrep:not(:has(.legal)) .overflow-x-auto > table tbody {
    display: block;
  }

  body.xrep:not(:has(.legal)) .overflow-x-auto > table tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 10px;
    min-height: 52px;
    padding: 11px 14px;
  }

  body.xrep:not(:has(.legal)) .overflow-x-auto > table td {
    display: inline-block;
    padding: 0;
  }

  /* The row's subject, first and at full contrast. */
  body.xrep:not(:has(.legal)) .overflow-x-auto > table td:first-child {
    order: 1;
    flex: 1;
    font-size: 14px;
  }

  /* The pill closes the line, hard right. */
  body.xrep:not(:has(.legal)) .overflow-x-auto > table td:has(> span) {
    order: 2;
    flex: 0 0 auto;
  }

  /* Everything else goes, which is what the artboard draws: a row is a name
     and a status.

     This reverses an earlier call. Keeping the other cells as a muted second
     line looked like the conservative choice — it dropped no facts — but with
     `thead` hidden those cells lose the only thing that explained them, and
     the line rendered as `Coastline Realty  4 / 4  09:14`, or just
     `—  0 / 2  14:47`. A bare "4 / 4" under a property address is not a fact
     the reader can use; it is a number with its label deleted. The desktop
     table, which has its header row, still carries all five columns. */
  body.xrep:not(:has(.legal)) .overflow-x-auto > table td:not(:first-child):not(:has(> span)) {
    display: none;
  }

  body.xrep:not(:has(.legal)) .overflow-x-auto > table tbody tr:last-child {
    border-bottom: none;
  }

  /* ==================================================================
     Settings — the CONSOLES section

     Per Settings.dc.html. Rendered by mobile-shell.js from the same
     eligibility function the desktop console rail uses.
     ================================================================== */
  /* The desktop "Template Builder" card is dropped on the phone. Two reasons,
     both of them the CONSOLES section's doing: it is now a duplicate of the
     Templates row — which says DESKTOP before you tap, where this card's
     button walks you into the M7 gate with no warning — and its
     `justify-between` row does not fit 390px, so its button hung 10px past
     the edge and gave the whole settings pane a sideways scroll.
     Settings.dc.html does not draw it. */
  #builderPanelCard,
  #orgPanelCard {
    display: none !important;
  }

  /* And a guard for the cards that remain. Every settings card is a
     `justify-between` row of "text | button", which does not fit 390px: the
     two above each hung their button ~10px past the edge and gave the whole
     settings pane a sideways scroll. Wrapping puts the action under the text
     instead, so a card added later cannot reintroduce this. */
  #settingsTab .flex.items-center.justify-between {
    flex-wrap: wrap;
    gap: 12px;
  }

  #settingsTab .flex.items-center.justify-between > a,
  #settingsTab .flex.items-center.justify-between > button {
    justify-content: center;
    width: 100%;
  }

  .mobile-consoles {
    display: flex;
    flex-direction: column;
    gap: 9px;
    margin: 18px 0 0;
  }

  /* The drawn section label: caps, tracked, with a hairline running out to
     the edge. Reused by any other settings group that needs one. */
  .mobile-section-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.1em;
    color: #94a3b8;
  }

  .mobile-section-head i {
    flex: 1;
    height: 1px;
    background: #e2e8f0;
  }

  .mobile-console-card {
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 16px;
    overflow: hidden;
  }

  .mobile-console-row {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 56px;
    padding: 11px 14px;
    border-bottom: 1px solid #f1f5f9;
    text-decoration: none;
  }

  .mobile-console-row:last-child {
    border-bottom: none;
  }

  .mobile-console-row__text {
    flex: 1;
    min-width: 0;
  }

  .mobile-console-row__label {
    font-size: 14px;
    font-weight: 700;
    color: #475569;
  }

  .mobile-console-row__desc {
    font-size: 11px;
    color: #94a3b8;
  }

  /* The badge is a promise, not decoration: these rows lead to the M7 gate,
     and saying so before the tap is the difference between "not for this
     screen" and "broken". */
  .mobile-console-row__badge {
    flex-shrink: 0;
    padding: 5px 8px;
    border: 1px solid #e2e8f0;
    border-radius: 7px;
    background: #f1f5f9;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.06em;
    color: #64748b;
  }

  /* ==================================================================
     M7 — The desktop gate

     Per DesktopGate.dc.html. Built by mobile-gate.js on the four console
     pages. A full-bleed cover rather than a banner: the point is that the
     console underneath is not usable at this width, and a dismissible strip
     over a broken layout still shows the broken layout.
     ================================================================== */
  .desktop-gate {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: flex;
    flex-direction: column;
    background: #f1f5f9;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The console under the gate is not hidden for tidiness — it is hidden
     because leaving it in the layout breaks the gate itself. These consoles
     are ~960px wide, and a page wider than the viewport expands the LAYOUT
     viewport to match: `window.innerWidth` reported 962 while
     `documentElement.clientWidth` and the media queries said 390. The gate is
     `position: fixed; inset: 0`, which resolves against that expanded
     viewport, so it rendered 962px wide with its content off-screen right.

     Removing the console from the layout collapses the viewport back to the
     device width. `!important` because these pages style their own shells
     with whatever they like, sometimes inline. "Open desktop layout anyway"
     drops the class and gives the console straight back. */
  body.has-desktop-gate {
    overflow: hidden;
  }

  body.has-desktop-gate > *:not(.desktop-gate) {
    display: none !important;
  }

  .desktop-gate:focus {
    outline: none;
  }

  .desktop-gate__head {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: 8px;
    height: 52px;
    padding: var(--safe-top) 8px 0;
    background: #eef2ff;
    border-bottom: 1px solid #e2e8f0;
    box-sizing: content-box;
  }

  .desktop-gate__back {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: none;
    background: none;
    color: #475569;
  }

  .desktop-gate__back svg {
    width: 20px;
    height: 20px;
  }

  /* The page you tried to open, named. The gate has to read as "this screen
     needs more room", not as "the app is broken". */
  .desktop-gate__title {
    font-size: 15px;
    font-weight: 700;
    color: #0f172a;
  }

  .desktop-gate__body {
    display: flex;
    flex-direction: column;
    gap: 22px;
    padding: 28px 20px calc(28px + var(--safe-bottom));
  }

  .desktop-gate__hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    text-align: center;
  }

  .desktop-gate__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 68px;
    height: 68px;
    border: 1px solid #e2e8f0;
    border-radius: 22px;
    background: #ffffff;
    box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
    color: #94a3b8;
  }

  .desktop-gate__mark svg {
    width: 30px;
    height: 30px;
  }

  .desktop-gate__headline {
    font-size: 22px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.02em;
    color: #0f172a;
  }

  .desktop-gate__reason {
    font-size: 14px;
    line-height: 1.6;
    color: #64748b;
  }

  .desktop-gate__actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .desktop-gate__primary,
  .desktop-gate__secondary {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50px;
    border-radius: 13px;
    font-size: 14px;
    font-weight: 600;
  }

  .desktop-gate__primary {
    border: none;
    background: #4f46e5;
    box-shadow: 0 8px 18px rgba(79, 70, 229, 0.24);
    color: #ffffff;
  }

  .desktop-gate__secondary {
    border: 1px solid #e2e8f0;
    background: #ffffff;
    color: #475569;
  }

  .desktop-gate__note {
    font-size: 12px;
    line-height: 1.6;
    color: #94a3b8;
  }

  /* ==================================================================
     Select picker sheet

     Drawn by mobile-shell.js in place of the native <select> popup, which is
     an OS widget the page cannot style or position — see the comment there.
     Above the modal layer (.modal-overlay is z-index 1000 in index.html)
     because it opens ON TOP of an already-open sheet.
     ================================================================== */
  .select-sheet-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    align-items: flex-end;
    background: rgba(15, 23, 42, 0.45);
    opacity: 0;
    transition: opacity 0.2s ease;
  }

  .select-sheet-overlay.is-open {
    opacity: 1;
  }

  .select-sheet {
    width: 100%;
    max-height: 80dvh;
    display: flex;
    flex-direction: column;
    padding: 0 1.125rem calc(1.125rem + var(--safe-bottom));
    background: #ffffff;
    border-radius: 22px 22px 0 0;
    box-shadow: 0 -12px 40px rgba(15, 23, 42, 0.3);
    transform: translateY(100%);
    transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
  }

  .select-sheet-overlay.is-open .select-sheet {
    transform: translateY(0);
  }

  /* Same drawn-not-draggable handle as every other sheet. */
  .select-sheet::before {
    content: "";
    align-self: center;
    flex-shrink: 0;
    width: 38px;
    height: 4px;
    margin: 8px 0 12px;
    border-radius: 2px;
    background: #cbd5e1;
  }

  /* The field's own question, so a list of four roles is not four bare words
     with no idea what is being asked. */
  .select-sheet__title {
    flex-shrink: 0;
    padding: 2px 4px 10px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: #64748b;
  }

  .select-sheet__list {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }

  .select-sheet__list::-webkit-scrollbar {
    width: 0;
  }

  /* 52px rows, per AddDocument.dc.html. */
  .select-sheet__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    min-height: 52px;
    padding: 0 4px;
    border: none;
    border-bottom: 1px solid #f1f5f9;
    background: none;
    font-size: 16px;
    font-weight: 500;
    text-align: left;
    color: #0f172a;
  }

  .select-sheet__row:last-child {
    border-bottom: none;
  }

  /* The sheet takes focus on a tap so Escape has somewhere to land; it is not
     itself an actionable thing, so it draws no ring. Rows keep theirs — that
     is the keyboard's only cursor in this list. */
  .select-sheet:focus {
    outline: none;
  }

  .select-sheet__row:focus-visible {
    outline: 2px solid #4f46e5;
    outline-offset: -2px;
    border-radius: 8px;
  }

  .select-sheet__row.is-selected {
    color: #4f46e5;
    font-weight: 600;
  }

  .select-sheet__check {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
  }

  /* An explicit way out, because the scrim is not obviously tappable and this
     sheet sits over another one — dismissing it must not read as a risk of
     dismissing the form underneath. */
  .select-sheet__cancel {
    flex-shrink: 0;
    width: 100%;
    min-height: var(--tap);
    margin-top: 10px;
    border: 1px solid #e2e8f0;
    border-radius: 12px;
    background: #ffffff;
    font-size: 15px;
    font-weight: 600;
    color: #475569;
  }

  /* The warning that sending locks the draft. Amber, immediately above the
     send action, so it is read in the same glance as the button it qualifies. */
  /* Deliberately NOT a card. The confirm checkbox above it is already an amber
     box; a second one directly beneath reads as one double-height block and
     neither gets emphasis. A plain line lets the checkbox stay the thing you
     act on and this stay the thing you read. */
  .mobile-send-warning {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0 0 14px;
    padding: 0;
    background: none;
    border: none;
    font-size: 12px;
    line-height: 1.5;
    color: #92400e;
  }

  .mobile-send-warning svg {
    flex-shrink: 0;
    margin-top: 1px;
    color: #b45309;
  }

  /* The signable-fields toggle is created as `fixed bottom-6 right-6 z-[1001]`
     — above the modal layer, so on a phone it floats over the sheet AND over
     the chat composer behind it. It belongs over the document it toggles:
     moved under the app bar, and dropped below the overlay's z so an open sheet
     covers it rather than being covered by it. */
  #toggleSignOverlayBtn {
    top: calc(var(--safe-top) + 108px);
    right: 12px;
    bottom: auto;
    z-index: 40;
    min-height: var(--tap);
  }

  /* Signer cards: two columns of a 346px form is 135px and 187px per card, and
     a "Full Name" field inside one of those is 93px — narrow enough that
     "Sarah Smith" does not fit, let alone an email address. The two signers are
     sequential anyway: you fill the buyer, then the co-buyer. */
  .recipient-grid {
    grid-template-columns: 1fr !important;
    gap: 0.875rem !important;
    margin-bottom: 1.25rem !important;
  }

  /* The action row pins to the bottom of the sheet. These forms are long — the
     e-signature sheet is 743px of content in an 844px screen — so left in flow
     the primary action sits below the fold and the sheet looks like it has no
     way to complete it. Sticky rather than fixed so it still scrolls with the
     sheet's own rounded bottom and safe-area padding. */
  .sheet-footer {
    position: sticky;
    bottom: 0;
    z-index: 2;
    /* A sticky footer floats over the content beneath it until the scroll
       reaches its own flow position, so the last block before it reads as
       truncated. This gap is what the content scrolls into. */
    /* Written as one shorthand: a `margin-top` declaration above a `margin`
       shorthand is silently reset to 0 by it, which is how this gap went
       missing. */
    margin: 1rem -1.125rem 0;
    /* Real air under the row: 4px of padding put the buttons against the
       screen edge, where a thumb hits the bezel before the button. */
    padding: 0.875rem 1.125rem calc(0.875rem + var(--safe-bottom));
    background: #ffffff;
    border-top: 1px solid #e2e8f0;
    gap: 0.625rem;
  }

  .sheet-footer {
    flex-wrap: wrap;
  }

  .sheet-footer > button {
    height: 50px;
    min-height: 50px;
    border-radius: 14px;
    font-size: 15px;
    justify-content: center;
    white-space: nowrap;
  }

  /* Two actions — Cancel sized to its word, the primary taking the rest, so the
     thumb lands on the action and not on the gap beside it. */
  .sheet-footer > button:first-child {
    flex: 0 0 auto;
    min-width: 96px;
  }

  .sheet-footer > button:last-child {
    flex: 1;
  }

  /* Three actions do not fit on one 390px row: "Send for Review" alone needs
     ~150px, and squeezing three left every label wrapped to two and three
     lines. The primary takes a full row of its own; the two secondaries share
     the row beneath it, where wrapping is not a risk.

     Ordered so the primary sits on top, nearest the thumb. */
  /* Groups inside the footer dissolve so every action is a direct flex item —
     the review sheet nests Cancel and Send inside a div beside Skip Review. */
  .sheet-footer > div {
    display: contents;
  }

  /* Two actions keep source order — Cancel left, the primary right, which is
     where every other confirm in the product puts it. Reordering was meant for
     the stacked three-action case below and was being applied to every footer,
     putting "Send Request" on the LEFT of "Cancel". */
  .sheet-footer:has(button + button + button) .sheet-footer__primary,
  .sheet-footer:has(> div button) .sheet-footer__primary {
    order: -1;
  }

  /* Three actions do not fit on one 390px row: "Send for Review" alone needs
     ~150px, and squeezing three wrapped every label onto two and three lines.
     The primary takes a row of its own, on top where the thumb is; the
     secondaries share the row beneath, where wrapping is not a risk. */
  .sheet-footer:has(button + button + button),
  .sheet-footer:has(> div button) {
    flex-wrap: wrap;
  }

  .sheet-footer:has(button + button + button) .sheet-footer__primary,
  .sheet-footer:has(> div button) .sheet-footer__primary {
    flex: 0 0 100%;
  }

  /* The escape hatches keep the 44px floor but need not be full height — they
     are not the action. */
  .sheet-footer:has(button + button + button) button:not(.sheet-footer__primary),
  .sheet-footer:has(> div button) button:not(.sheet-footer__primary) {
    flex: 1;
    height: 44px;
    min-height: 44px;
    min-width: 0;
    font-size: 14px;
  }

  /* Controls inside a sheet take the same 44px floor as everything else. The
     confirm checkbox on the e-signature form gates sending a legal document
     and was rendering at 28px. */
  .modal-content button,
  .modal-content select,
  .modal-content input[type="text"],
  .modal-content input[type="email"],
  .modal-content input[type="tel"],
  .doc-modal button {
    min-height: var(--tap);
  }

  .modal-content input[type="checkbox"],
  .modal-content input[type="radio"] {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
  }

  /* The label beside a checkbox is the rest of the target — 22px of box plus a
     tappable label clears 44px without drawing a 44px square on the page. */
  .modal-content label:has(input[type="checkbox"]),
  .modal-content label:has(input[type="radio"]) {
    min-height: var(--tap);
    display: flex;
    align-items: center;
    gap: 0.625rem;
  }
}

/* --------------------------------------------------------------------------
   Coarse-pointer rules

   Keyed off the INPUT DEVICE rather than the width, because these are about
   what a finger can do, not how much room there is: a touchscreen laptop at
   1400px has the same hover problem as a phone.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  /* Tap highlight is the grey flash mobile WebKit/Blink paints over any
     element with a tap handler. The app draws its own :active states, so the
     flash reads as a rendering glitch on top of them. Inert on desktop
     browsers, which never paint one. */
  * {
    -webkit-tap-highlight-color: transparent;
  }
}

/* `hover: none` and not `pointer: coarse`, deliberately. A touchscreen laptop
   — or a Surface in tablet mode — reports a coarse pointer while still being
   a desktop-width machine someone may be driving with a mouse; suppressing
   :hover there would delete a working affordance from the desktop layout.
   `hover: none` is true only where the device genuinely cannot hover. */
@media (hover: none) {
  /* Sticky hover: iOS keeps :hover applied to the last element tapped until
     you tap somewhere else, so a tapped document row stays highlighted and
     competes with .doc-row--active for "which one am I looking at". The
     active class is the only thing that should answer that. */
  .doc-row:hover {
    background-color: transparent;
  }
}
