/* In-portal device acquisition. The binary is still the verified public
 * release, but choosing a job and understanding the next step stays inside
 * the signed-in product instead of opening a second marketing page. */

.ds-dialog {
  /* 920 -> 740 and 900 -> 760, OWNER BRIEF 2026-09-21 ("the dialog is
     excessively large -- occupies most of the viewport but still requires a
     prominent internal scrollbar; the content density does not justify this
     much visual mass").
     MEASURED at HEAD before this pass, .scratch/ds-probe.mjs, realistically
     seeded, dialog open on #devices:
       1440x900  920x876  = 64% x 97% of the viewport, body overflowing 33px
       1024x768  920x744  = 90% x 97%,                 body overflowing 165px
        390x844  354x834  = 91% x 99%,                 body overflowing 350px
     The cap alone would have made the scrolling WORSE, so it is only half of
     the change: the body below sheds a nested card, a pill row and a footer
     line, and the numbers this claim rests on are re-measured after, not
     before. See .scratch/WAL-device-setup-dialog.md.
     The second term is unchanged and is the load-bearing one: a native
     <dialog> that is not bounded by the viewport scrolls the PAGE behind
     itself instead of its own body (device-setup-portal-ui.test.js).

     MEASURED OFF HER OWN MODALS, not off a page. .scratch/mockup-modals.mjs
     rendered CheckinModal / ChoosePlanModal / PaymentModal /
     ConfirmDeleteModal.dc.html at 1440x900 and read the panel with
     getComputedStyle:
       CheckinModal        740x684   40px pad   28px radius   51.4% x 76.0%
       ChoosePlanModal     720x684   40px pad   28px radius   50.0% x 76.0%
       PaymentModal        640x627   40px pad   28px radius   44.4% x 69.6%
       ConfirmDeleteModal  488x377   36/34 pad  28px radius   33.9% x 41.9%
     720-740 wide and 28px radius is her modal, so the width lands exactly on
     it. The height sits at 84% rather than her 76% because this dialog carries
     a three-block progression where each of her screens carries one step --
     an intentional difference, recorded in the report and in
     .scratch/WAL-device-setup-dialog.md. */
  width: min(740px, calc(100vw - 24px));
  max-height: min(760px, calc(100dvh - 24px));
  margin: auto;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 28px;
  background: var(--dusk);
  color: var(--cloud);
  box-shadow: var(--shadow);
}

/* ===========================================================================
   THE DARK SCROLLBAR IN THE LIGHT MODAL — OWNER BRIEF 2026-09-21.
   ===========================================================================
   "The modal is light, but its internal scrollbar is nearly black and includes
   dark arrow buttons. It visually belongs to a different theme."

   IT DOES BELONG TO A DIFFERENT THEME: THE OPERATING SYSTEM'S.

   index.html declares `<meta name="color-scheme" content="light dark">` and NO
   stylesheet in this product declares the `color-scheme` PROPERTY anywhere
   (checked: 0 hits for `^\s*color-scheme` across app.css, base.css and every
   file in css/ — only `prefers-color-scheme` media queries, which are a
   different thing). The product's own theme is an attribute,
   `:root[data-theme]`, written by app.js applyTheme(). So the UA paints its
   scrollbars, form controls and default canvas from the OS preference while
   everything a parent can see is painted from the attribute. The two are never
   connected, and the default theme is LIGHT (app.js:37080) while most desktops
   ship dark — which is exactly the owner's screenshot.

   NOT A GUESS. MEASURED 2026-09-20, .scratch/ds-scrollbar.mjs, headed Chromium
   under Xvfb at 1024x768 with classic scrollbars, sampling the mean RGB and the
   darkest pixel of the 15px scrollbar column inside .ds-body:

     OS scheme   app theme   scrollbar mean RGB   darkest pixel luma
     dark        light       (110,110,110)        44      <- THE OWNER'S BUG
     dark        dark        (110,110,110)        44
     light       light       (187,187,187)        139
     light       dark        (187,187,187)        139     <- THE MIRROR BUG

   The scrollbar is a pure function of the OS and is IDENTICAL across both app
   themes, which is the whole defect stated as a number. Note row 4: a parent on
   a light-OS machine gets a near-white scrollbar inside our dark theme. A
   "light scrollbar" fix would have shipped that one and called it done — which
   is precisely what the brief says not to do.

   THE FIX IS TO BIND THE UA's SCHEME TO THE PRODUCT'S OWN, SCOPED TO THIS
   DIALOG. `color-scheme` inherits, so declaring it on .ds-dialog reaches
   .ds-body (the actual scroll region) and every control inside it without a
   single global rule. Dark-theme pages keep their dark scrollbars because the
   dark branch says so explicitly, rather than by omission.

   `:root:not([data-theme])` pairs with the light branch throughout this file
   (see the ::backdrop and .ds-truth rules) because app.js boots to light. */
:root[data-theme="light"] .ds-dialog,
:root:not([data-theme]) .ds-dialog { color-scheme: light; }
:root[data-theme="dark"] .ds-dialog { color-scheme: dark; }

/* DEF-006 — THE DIALOG IS THE ONLY SCROLL SURFACE WHILE IT IS OPEN.
 *
 * A native <dialog> opened with showModal() does NOT stop the page behind it
 * from scrolling; Chrome only blocks input to the inert content, not the
 * viewport's own scroll. device-setup.js calls showModal() and sets no scroll
 * lock, and nothing in app.css or base.css declares `overflow` on <html> at
 * all, so the document kept its scrollbar and kept moving underneath.
 *
 * MEASURED at HEAD, .scratch/def003-006-probe.mjs, realistically seeded
 * (3 children, 3 paired devices, 120 apps each), dialog open on #devices:
 *
 *              page scrollable   wheel over backdrop moved page   dialog body
 *   390x844        1753px                  500px                     350px
 *   375x812        1803px                  500px                     382px
 *   768x1024       1335px                  500px                      19px
 *   1440x900        808px                  500px                      33px
 *
 * Two live scroll surfaces at every viewport, which is exactly the audit's
 * DEF-006 evidence image: the dialog's own scrollbar AND a second page
 * scrollbar still painted beside it.
 *
 * WHY `:has()` AND NOT A CLASS: the open/close path is device-setup.js's, and
 * this lane owns this stylesheet only. `[open]` is already the authoritative
 * signal -- showModal() sets it, close() removes it -- so no second piece of
 * state can drift out of step with the dialog. app.css already relies on
 * `:has()` in 38 places, so this is not a new floor for the product.
 *
 * THIS RULE DELIBERATELY DOES NOT CARRY `scrollbar-gutter: stable`, and that is
 * the opposite of what it was first written with. The theory was that
 * `overflow: hidden` takes a classic scrollbar away and hands its 15px back to
 * the layout, so the page behind the scrim jumps sideways -- and that declaring
 * the gutter stable would hold that column. MEASURED instead, by mutating this
 * very rule through the CSSOM and reading #main's right edge as the dialog
 * opens (.scratch/def006-gutter.mjs, Chrome 149, 1440x900):
 *
 *   no rule at all            wheel scrolled the page 508px   #main shift   0px
 *   overflow:hidden           wheel scrolled the page   0px   #main shift   0px
 *   overflow:hidden + stable  wheel scrolled the page   0px   #main shift -15px
 *
 * The gutter declaration CAUSED the jump it was added to prevent: this browser
 * uses overlay scrollbars (window.innerWidth - documentElement.clientWidth is 0
 * -- checked, and not switchable: --disable-features=OverlayScrollbar,
 * FluentOverlayScrollbar,FluentScrollbar all still report 0), so there is no
 * scrollbar column to preserve and `stable` only reserves 15px that was never
 * there. Plain `overflow: hidden` is the version that measures clean.
 *
 * KNOWN LIMIT, stated rather than papered over: on a desktop configured for
 * CLASSIC scrollbars the 15px reclaim is presumably real, and it could not be
 * reproduced on this box to find out. DEF-006's own scope is mobile, where
 * scrollbars are overlay and this is exactly right.
 *
 * SECOND KNOWN LIMIT: `overflow: hidden` stops the USER -- wheel, drag and the
 * scrollbar itself -- but the document stays programmatically scrollable, so
 * window.scrollTo() still moves it (measured: 500px) and scrollHeight is
 * unchanged. The audit's acceptance is about what a parent can do, and no CSS
 * can close the programmatic half without position:fixed, which would lose the
 * parent's place on the page behind.
 *
 * THIRD KNOWN LIMIT, AND THE ONE MOST LIKELY TO MATTER -- iOS SAFARI IS
 * UNVERIFIED. Raised by an independent reviewer, and it is a fair hit:
 * `overflow: hidden` on <html> alone is the well-known INCOMPLETE scroll lock
 * on WebKit, where touch-dragging the page behind a modal can still rubber-band
 * it. DEF-006's own scope is mobile, so this is exactly the platform that
 * matters most.
 *
 * It is left as it stands rather than "fixed" blind, for the reason this lane
 * kept running into: EVERY browser check in platform/test/ drives Chromium,
 * there is no WebKit anywhere in this suite, and no iOS device was reachable
 * from the box this was written on. The usual WebKit remedy -- `position:
 * fixed; top: -scrollY` on <body> -- cannot be written in CSS at all (it needs
 * JS to capture and restore the offset), and the cheap-looking CSS version,
 * adding `overflow: hidden` to <body> as well, is NOT obviously safe: it makes
 * <body> a new block formatting context, and the same reviewer's other concern
 * is that an ancestor lock can freeze the dialog's own `overflow: auto`
 * scroller on WebKit -- which would put the installer download out of reach
 * behind ~350px of dialog overflow. Trading a page that still rubber-bands for
 * a dialog that cannot be scrolled is not a trade worth making unmeasured.
 *
 * MEASURED where it could be: in Chromium the inner scroller is NOT frozen --
 * a real wheel over `.ds-body` moves it 300px at 390x844, 82px at 375x812, and
 * to its 19px maximum at 768x1024, with the lock in place. The guard test
 * asserts that with a real wheel rather than a `scrollTop` assignment, so the
 * day this suite gains a WebKit runner the assertion is already the right one.
 * FOLLOW-UP: verify on a real iPhone (hawkeye can drive one) before trusting
 * this lock on iOS.
 *
 * NOT AN ed3eaf68 ("css that is really behaviour"): this sets `overflow` on
 * <html>, and hides no element. The only nodes device-setup.js writes
 * user-facing text into are #deviceSetupStatus, #deviceSetupRetry and
 * #deviceSetupManual (device-setup.js:361-465), all inside .ds-body, and none
 * of them is touched by this selector or by anything it can cascade to.
 *
 * `.dsp-dialog` carries `.ds-dialog` too, so the progress dialog gets the same
 * treatment, which is what it wanted anyway. */
html:has(dialog.ds-dialog[open]) {
  overflow: hidden;
}

.ds-dialog::backdrop {
  background: rgba(3, 9, 18, .72);
  backdrop-filter: blur(4px);
}
/* Same lighter scrim app.css already gives .stepup-dialog::backdrop and
 * .schedule-dialog::backdrop in light theme -- this dialog's own light
 * override was missing, so a light-toggled parent got the dark-theme .72
 * near-opaque scrim behind an otherwise correctly light dialog box (visually
 * read as "the whole screen went dark"). Confirmed by screenshot 2026-09-19. */
:root[data-theme="light"] .ds-dialog::backdrop,
:root:not([data-theme]) .ds-dialog::backdrop { background: rgba(10,16,32,.42); }

/* ONE DIALOG SHELL: a fixed header, ONE scrolling body, a fixed footer, and the
   same inner padding down all three so the three bands read as one dialog
   rather than three stacked components (owner brief, "poor scrolling
   structure"). --ds-pad is that single inner padding; every band below reaches
   for it instead of carrying its own number, which is what kept the header,
   body and footer out of step before. */
.ds-shell {
  --ds-pad: 24px;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  max-height: min(760px, calc(100dvh - 24px));
}

.ds-head,
.ds-foot {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px var(--ds-pad);
  background: var(--dusk);
}

/* --line -> --border-subtle ON THE TWO BAND DIVIDERS, and this is NOT the
   owner-pinned border value being re-litigated.
   plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md pins --line at
   rgba(22,35,63,.16) in light; that token's VALUE is untouched here. What
   changes is which token these two rules reach for, and a prior lane already
   made and documented exactly this move in this file for .ds-plan and
   .ds-download-card: a card/panel surface is the .08 token, a FORM CONTROL is
   the .16 one. A header/footer divider is a panel edge, not a control border,
   so it belongs with the former — which is also what stops the three bands
   reading as three stacked components.
   In dark theme --border-subtle resolves to var(--line) (app.css's dark :root),
   so nothing moves there at all; this is a light-theme softening only. */
.ds-head {
  align-items: flex-start;
  justify-content: space-between;
  border-bottom: 1px solid var(--border-subtle, var(--line));
}

/* STEP 4 OF THE PROGRESSION, "continue or finish". Was a lone ghost button
   floating at the right of an otherwise empty bar -- the owner's "Done is a
   small detached footer button". #deviceSetupNext ("Run ParentProof Setup and
   sign in. Pick the job and child there.") moved out of the scrolling body and
   into this bar beside it, so the footer carries the sentence that says what
   finishing means, the way her own footers pair a helper line with the forward
   control. It also takes ~30px of content out of the scroll region.
   flex-wrap, not a display toggle: at a phone width the line takes its own row
   ABOVE the button. It is never hidden at any width -- device-setup.js writes
   into it (setText('deviceSetupNext')) and an ed3eaf68-shaped `display: none`
   here would silently delete the last instruction in the flow. */
.ds-foot {
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px 16px;
  padding-bottom: max(14px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--border-subtle, var(--line));
}

.ds-eyebrow {
  margin: 0 0 3px;
  color: var(--glow);
  /* HER MEASURED EYEBROW, 2026-09-16 handoff: Quicksand 11/700 at .08em
     uppercase -- the same spec app.css already records for the screen-level
     eyebrow. This was Nunito 760: Quicksand is the family she reserves for
     labels, headings and buttons, and 760 is a synthesised weight between the
     two real faces we self-host (700 and none above it), so it rendered as
     700 anyway while claiming otherwise. */
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.ds-title {
  margin: 0;
  /* 32px -> 22px, AND THIS IS A DELIBERATE EDIT BY THE device-setup-dialog
     LANE (handle claude-device-setup-dialog-lane, 2026-09-21), not a harness
     artefact and not a revert of anyone's work.
     The comment that stood here read "HER h1: 32/700, 48px line box" and was
     accurate about the measurement and wrong about the artboard: 32/48 is her
     PAGE h1, measured off DownloadInstaller.dc.html, which is a full screen.
     This is a MODAL. Asked her own modal artboards the same question
     (.scratch/mockup-modals.mjs, getComputedStyle at 1440x900):
       CheckinModal        "Send a check-in"               22px / 700 / 33px
       ChoosePlanModal     "Choose your plan"              22px / 700 / 33px
       PaymentModal        "Payment"                       22px / 700 / 33px
       ConfirmDeleteModal  "Remove Maya's profile?"        21px / 700 / 31.5px
     Four artboards, one answer: her modal title is 22/700/33 in Quicksand,
     which is --font-heading and is unchanged. It also gives the fixed header
     back 15px, which the body spends on the installer cards.
     The clamp stays, for the reason the old comment gave: this title lives in
     a box that can be a phone. Only the top end moves. */
  font-family: var(--font-heading);
  font-size: clamp(19px, 2.4vw, 22px);
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: -.22px;
}

/* The title receives programmatic focus so screen readers announce the newly
   opened dialog. It is not an interactive control, so suppress the browser's
   large default focus rectangle without changing keyboard focus on controls. */
.ds-title:focus { outline: none; }

.ds-close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: 0 0 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: var(--dusk2);
  color: var(--haze);
  font-size: 22px;
}

.ds-close:hover,
.ds-close:focus-visible { border-color: var(--glow); color: var(--glow); }

/* THE MODAL'S ACTUAL SCROLL REGION, and the only element in this file that
   gets scrollbar styling. The brief asks for "theme-aware styling scoped to
   the modal's actual scroll region" and explicitly forbids "a global light
   scrollbar that damages dark-theme pages", so nothing here is written at
   :root, at *, or at any shared scroll container.

   `scrollbar-gutter: stable` is the "stable gutter so content never sits
   underneath the scrollbar" requirement. It reserves the column inside the
   padding box whether or not the content currently overflows, so the body does
   not reflow the moment a longer plan appears, and the 22px of padding still
   stands between the text and the bar -- the owner's "the scrollbar runs
   tightly against content".

   THIS IS NOT THE `scrollbar-gutter` THAT WAS MEASURED HARMFUL. The DEF-006
   comment above rejects `scrollbar-gutter: stable` on the `html:has(dialog
   .ds-dialog[open])` rule, where it reserved a 15px column on the PAGE BEHIND
   the scrim and shifted #main sideways every time the dialog opened. That is a
   different element and a different scroll surface; declaring it on .ds-body
   cannot move anything outside the dialog, and the guard test measures #main's
   own right edge across open/close, so the claim can still fail. */
.ds-body {
  padding: 16px var(--ds-pad, 20px) 20px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
}

/* ===========================================================================
   DO NOT DECLARE `scrollbar-width` OR `scrollbar-color` ON .ds-body.
   ===========================================================================
   This rule set was first written with the standard properties AND the
   ::-webkit-scrollbar pseudo-elements together, on the reasonable theory that
   -webkit- would serve Chromium and the standard pair would serve Firefox.
   THAT IS NOT WHAT HAPPENS, and it silently threw away half of the brief.

   CURRENT CHROMIUM IGNORES EVERY ::-webkit-scrollbar* RULE ON A SCROLLER THAT
   ALSO DECLARES scrollbar-width OR scrollbar-color. Measured, headed under
   Xvfb with classic scrollbars, on a plain overflowing div, reading the
   scrollbar column's pixels and the layout width the bar actually took
   (.scratch/sb-button-exp.mjs):

     rules on the scroller                       bar width   arrow glyph
     ::-webkit-scrollbar{width:20px} only            20px    none
     ...plus ::-webkit-scrollbar-button{background:red}
                                                     20px    15px RED block
                                                             (so the button box
                                                             is real, and
                                                             display:none does
                                                             remove it)
     ...plus scrollbar-width:thin + scrollbar-color  10px    PRESENT again

   The 20px request collapsing to 10px is the tell: with the standard
   properties present Chromium paints ITS OWN thin scrollbar and every
   -webkit- declaration -- width, the rounded thumb, the inset, and the
   `::-webkit-scrollbar-button { display: none }` -- is discarded. Six
   different spellings of the button-removal rule were measured pixel-identical
   to NO RULE AT ALL, which is exactly what "discarded" looks like from the
   outside and is why this was nearly shipped: the colour half of the fix
   worked, so the bar looked fixed.

   The brief asks for BOTH "rounded thumb matching the design system" AND "no
   dark arrow-button blocks". Only the -webkit- path delivers those on
   Chromium, so the -webkit- path is the primary one and the standard pair is
   confined to engines that have no -webkit- path at all.

   `scrollbar-gutter: stable` is NOT one of the two and does not disable
   anything -- measured above, the 20px bar kept its width with the gutter
   declared.

   HONEST LIMIT: there is no Gecko runner anywhere in platform/test, so the
   @supports branch below is REASONED, NOT MEASURED, same as the iOS caveat in
   the DEF-006 comment near the top of this file. It is also not load-bearing:
   `color-scheme` above already binds Firefox's own default scrollbar to the
   product's theme, so a Firefox that never matches this block still gets a
   light bar in the light modal -- it just does not get the thin one. */
@supports not selector(::-webkit-scrollbar) {
  .ds-body {
    scrollbar-width: thin;
    scrollbar-color: var(--ds-sb-thumb) transparent;
  }
}

/* NEUTRAL, THEME-AWARE, AND MEASURED FOR CONTRAST -- not --line.
   --line is rgba(22,35,63,.16) in light, which is 1.37:1 on white: it is
   pinned at that value by plans/DECISIONS-2026-09-20-design-fidelity-over-
   contrast.md as a CARD EDGE, and that decision is not being re-litigated
   here. A scrollbar thumb is a control a parent has to find and drag, so it
   gets its own token rather than borrowing the border one.
   --cloud is the theme's own text hue (#16233F light, #EDF5FF dark), so one
   color-mix gives a dark thumb on the light surface and a light thumb on the
   dark one with no duplicated literal:
     light  rgba(22,35,63,.45) over #FFFFFF    -> ~#8C93A2, 3.0:1  (WCAG 1.4.11)
     dark   rgba(237,245,255,.45) over #101B2D -> ~#7C8B9F, 4.9:1
   Hover/active step up one stop, per "slightly darker hover/active thumb". */
.ds-dialog {
  --ds-sb-thumb: color-mix(in srgb, var(--cloud) 45%, transparent);
  --ds-sb-thumb-hover: color-mix(in srgb, var(--cloud) 62%, transparent);
}

.ds-body::-webkit-scrollbar { width: 12px; height: 12px; }
/* Light/transparent track: the dialog surface shows straight through, which is
   what makes the bar stop being the most prominent element in the modal. */
.ds-body::-webkit-scrollbar-track { background: transparent; }
/* Rounded thumb at the design system's pill radius, inset from the track by a
   transparent border so the 12px column paints a 6px bar with clearance on
   both sides. `background-clip: padding-box` is what makes that border show
   the track instead of the thumb colour. */
.ds-body::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: var(--radius-pill, 999px);
  background: var(--ds-sb-thumb);
  background-clip: padding-box;
}
.ds-body::-webkit-scrollbar-thumb:hover,
.ds-body::-webkit-scrollbar-thumb:active { background: var(--ds-sb-thumb-hover); background-clip: padding-box; }
/* "No dark arrow-button blocks." Styling ::-webkit-scrollbar at all already
   drops the UA's stepper buttons in Chromium; this says so explicitly so a
   later edit cannot bring them back by accident, and covers the corner where a
   horizontal bar would meet a vertical one. */
.ds-body::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
.ds-body::-webkit-scrollbar-corner { background: transparent; }

/* "Respect operating-system high-contrast and accessibility settings."
   In forced-colors mode the parent's own palette wins outright: hand the
   Firefox-shaped property back to the UA and repaint the -webkit- thumb and
   track in system colours, because a custom rgba() thumb is exactly the thing
   that disappears against a forced background. */
@media (forced-colors: active) {
  @supports not selector(::-webkit-scrollbar) {
    .ds-body { scrollbar-color: auto; }
  }
  .ds-body::-webkit-scrollbar-track { background: Canvas; }
  .ds-body::-webkit-scrollbar-thumb { background: ButtonText; border-color: Canvas; }
}
/* A parent who has asked their OS for more contrast gets a solid thumb rather
   than a 45% wash. */
@media (prefers-contrast: more) {
  .ds-dialog {
    --ds-sb-thumb: color-mix(in srgb, var(--cloud) 72%, transparent);
    --ds-sb-thumb-hover: var(--cloud);
  }
}

.ds-intro {
  max-width: 68ch;
  margin: 0 0 12px;
  color: var(--haze);
  font-size: 13.5px;
  line-height: 1.5;
}

/* THE SETUP SEQUENCE, MADE VISIBLE — owner brief, "weak setup sequence ...
   the user is not guided through a clear progression". The body is now three
   numbered blocks in the brief's own order (1 select device type, 2 review
   preparation steps, 3 download the setup application) and the footer is
   step 4, continue or finish.
   The numeral is a real element in the markup carrying aria-hidden, NOT a
   ::before on the heading: every one of these headings is an
   `aria-labelledby` target AND has its textContent rewritten by
   device-setup.js render() on every language change and every intent click,
   so the number stays out of both the accessible name and the write path. */
.ds-step-block + .ds-step-block { margin-top: 16px; }

.ds-step-head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 9px;
}

.ds-step-index {
  display: grid;
  place-items: center;
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--glow) 14%, var(--dusk));
  color: var(--glow);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 800;
}

/* HER CARD TITLE, measured off "What you'll need" / "What will happen" on
   DeviceSetupSteps: Nunito 16.5/800. These three play exactly that role --
   they head a section of the dialog -- and were 13px/760, which read as a
   form-field label rather than a title and was the main reason the dialog's
   internal hierarchy went flat next to her artboard. */
.ds-choice-label,
.ds-plan-title,
.ds-download-title {
  /* The 9px bottom margin moved to .ds-step-head, which is now what separates
     a heading from its block; leaving it here would have double-spaced them. */
  margin: 0;
  color: var(--cloud);
  font-family: var(--font-body);
  font-size: 16.5px;
  font-weight: 800;
  line-height: 1.3;
}

/* auto-fit, NOT repeat(3, 1fr), and this is a real defect fix rather than a
   preference. The Windows and Mac choices are hidden by the desktop-visibility
   gate (device-setup.js render(): `button.hidden = !desktopSetupVisible && ...`),
   which is the DEFAULT state -- so most parents see ONE choice. `repeat(3, …)`
   keeps all three tracks whether or not anything occupies them, so that single
   card was laid out at a third of the row: measured at the 740px cap it came
   out ~218px wide and "Android tablet or phone" broke across two lines inside
   a box with two thirds of the row empty beside it. auto-fit collapses the
   empty tracks, so one choice takes the row and three still sit in three
   columns. Same reasoning as .ds-downloads below, and the same guarantee: a
   track is a whole card or it does not exist, so nothing part-wraps. */
.ds-intents {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 10px;
}

/* 82 -> 72 min-height and 12 -> 11px padding. These three are a chooser, not
   three cards: with the gate closed only ONE of them renders (the tablet
   choice), so an 82px-tall box holding two short lines was the single largest
   piece of dead vertical mass at the top of the dialog. */
.ds-intent {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  gap: 11px;
  align-items: center;
  min-width: 0;
  min-height: 72px;
  padding: 11px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk2);
  color: var(--cloud);
  text-align: left;
}

.ds-intent:hover,
.ds-intent:focus-visible { border-color: var(--track); }

.ds-intent[aria-pressed="true"] {
  border-color: var(--glow);
  background: color-mix(in srgb, var(--glow) 11%, var(--dusk2));
  box-shadow: inset 3px 0 0 var(--glow);
}

.ds-icon-tile {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--dusk);
  color: var(--glow);
}

.ds-icon {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ds-intent-copy { min-width: 0; }
.ds-intent-title { display: block; font-size: 14px; font-weight: 760; }
.ds-intent-sub { display: block; margin-top: 3px; color: var(--haze); font-size: 11px; line-height: 1.35; }

/* CARD vs CONTROL, measured across all eight of her device-setup artboards:
   a card/panel surface is 1px rgba(22,35,63,.08) at radius 22, a FORM CONTROL
   is 1px rgba(22,35,63,.16) at radius 12-14. Live painted both with --line
   (.16), so her soft panel edges read as hard control boxes and the dialog
   looked boxier than her work at every size.
   This does NOT touch the owner-pinned .16 border value -- see
   plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md, which pins
   --line and is NOT being re-litigated here. --border-subtle (.08) already
   existed in app.css's light block for exactly this role; the only change is
   that card surfaces in this dialog now reach for it instead of the control
   token. Controls below (.ds-intent, .ds-close) deliberately keep --line. */
/* THE CARD THAT WRAPPED ANOTHER CARD IS GONE — owner brief, "too many nested
   rounded rectangles: an instruction card, a callout inside that card, two
   installer cards, pills ... the hierarchy becomes a stack of boxes instead of
   a clear setup sequence."
   .ds-plan was a bordered, filled card whose ONLY boxed child was .ds-truth,
   the tinted callout below -- a box inside a box, drawn on a dialog body that
   is already a white panel, so the outer edge separated nothing from anything.
   The numbered step head above it is what identifies the block now, and
   .ds-truth stays as the one callout in it, which is the shape her own
   DownloadInstaller uses ("What happens next" as a single tinted panel on the
   page, not nested inside a second card).
   The rule is kept rather than deleted so the class still resolves in
   style-vocabulary.test.js and so this reasoning has somewhere to live.
   NOTHING HERE TOUCHES display/visibility/opacity: device-setup.js writes into
   #deviceSetupPlanTitle, #deviceSetupSteps, #deviceSetupTruth and
   #deviceSetupManual inside this section and all four still paint. */
.ds-plan {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.ds-steps {
  display: grid;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-step {
  display: grid;
  grid-template-columns: 25px minmax(0, 1fr);
  gap: 9px;
  align-items: start;
  color: var(--haze);
  font-size: 13px;
  line-height: 1.45;
}

.ds-step-number {
  display: grid;
  place-items: center;
  width: 23px;
  height: 23px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--glow) 14%, var(--dusk));
  color: var(--glow);
  font-size: 11px;
  font-weight: 800;
}

/* UI-DIRECTION §3 (binding, five meanings since 2026-09-18): green means
   DONE/APPLIED. The sentence in this callout ("Sign in inside ParentProof
   Setup and pick the child. No code needed.") is guidance before anything has
   happened -- it asserts no completed state at all -- so green was claiming a
   meaning the copy does not carry. Brand blue is the trusted/informational
   role, and her own counterpart panel ("What happens next", DownloadInstaller)
   is the #E8F1FA info tint at radius 14. Same reasoning, and the same shape of
   fix, as the .dsp-blocker coral -> amber correction recorded further down
   this file; no OWNER DECISION sanctioned the green, so this is a fresh
   miscolouring rather than an intentional deviation. */
.ds-truth {
  margin: 10px 0 0;
  padding: 11px 13px;
  border-left: 3px solid var(--info, var(--glow));
  border-radius: 14px;
  background: color-mix(in srgb, var(--info, var(--glow)) 9%, var(--dusk));
  color: var(--cloud);
  font-size: 12.5px;
  line-height: 1.45;
}
:root[data-theme="light"] .ds-truth,
:root:not([data-theme]) .ds-truth { background: var(--info-tint, #E8F1FA); }

/* .ds-download-section's own top margin is gone: .ds-step-block + .ds-step-block
   owns the rhythm between the three numbered blocks now, and keeping both
   would have made this one gap larger than the other. */
.ds-download-section { margin-top: 0; }

/* "At desktop width, installer options may use two balanced columns. At
   narrower widths, stack them as complete cards. Do not allow partial wrapping
   or clipping."
   auto-fit + minmax is what guarantees that: a track is either a whole 230px+
   card or it does not exist, so a card can never be half-wrapped. There are
   exactly two platforms, and at the 820px cap the inner width is ~752px, which
   is two tracks -- never three.
   justify-items + the card's own max-width is the single-card case: the
   Windows and Mac intents each render ONE card, and a lone card stretched to
   the full 752px put a 750px-wide gradient button on screen, which is the
   "enormous" the owner is objecting to. Capped and centred it stays the same
   card it is beside its sibling. */
.ds-downloads {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  justify-items: center;
  gap: 12px;
}

/* Her tile sits ABOVE the platform name and is bigger than the intent-card
   tile beside it (48 vs 42), on the brand tint rather than the page. */
.ds-download-tile {
  width: 40px;
  height: 40px;
  border-color: transparent;
  background: var(--brand-tint, var(--dusk));
}
.ds-download-tile > .ds-icon { width: 21px; height: 21px; }

/* HER DownloadInstaller card: a white panel at the card edge (.08 / r22),
   its contents CENTRED under the platform name, with the download pill run
   full width and the trust line beneath it. Live was a left-aligned control
   box. Nothing is dropped in the move -- version, signature/notarisation
   badges and the OS requirement line all stay, they are simply centred with
   the rest of the stack. */
.ds-download-card {
  display: grid;
  gap: 5px;
  justify-items: center;
  text-align: center;
  width: 100%;
  max-width: 380px;
  min-width: 0;
  /* TIGHT ON PURPOSE. The platform tile this card gained is ~48px of new
     height, and at 982px -- the height of her own artboards -- that was
     enough to push BOTH download pills below the dialog's scroll fold, so
     the primary action of the screen was invisible until a parent scrolled.
     Measured, not guessed: the fold check in this lane's harness reports the
     action's bottom against the scroll viewport's bottom. The row below
     raises the dialog cap for the same reason. */
  padding: 14px 14px 15px;
  border: 1px solid var(--border-subtle, var(--line));
  border-radius: var(--radius-card, 14px);
  background: var(--dusk2);
}

/* "Current/recommended platform identified when reliably detectable" and
   "other platform still available without receiving excessive visual
   emphasis". Reliably detectable is the whole condition: device-setup.js's
   hostOs() returns null on anything that is not clearly Windows or macOS, and
   nothing is marked when it does -- neither card is guessed at.
   Brand blue only, because rule 3 reserves blue for trusted+primary and this
   is a recommendation, not a completed state. Deliberately a 1px edge and a
   small chip rather than a fill: the other card must stay equally clickable. */
.ds-download-card.is-recommended {
  border-color: color-mix(in srgb, var(--glow) 45%, var(--border-subtle, var(--line)));
}

.ds-recommended {
  padding: 3px 10px;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in srgb, var(--glow) 12%, transparent);
  color: var(--glow);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 800;
  line-height: 1.35;
}

.ds-download-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}

/* Her platform name: Nunito 16.5/800, the same card-title weight the section
   headings above use, with the version dropping to the secondary line under
   it rather than sitting opposite it on the same row. */
.ds-download-name {
  color: var(--cloud);
  font-family: var(--font-body);
  font-size: 16.5px;
  font-weight: 800;
  line-height: 1.3;
}
.ds-version { color: var(--haze); font-size: 11.5px; white-space: nowrap; }

/* THE PILL ROW IS ONE LINE NOW — owner brief, "version and signing information
   is scattered across several small labels" and "trust/signing information in
   one concise line".
   WAS: .ds-badges, a wrapping row of up to THREE bordered 999px pills per card
   (Signed / Notarized by Apple / For this computer) sitting between the
   version and the requirement line, i.e. three more rounded rectangles inside
   a rounded rectangle inside the dialog. Her own DownloadInstaller card
   carries a single green-checked sentence, "Verified & safe to run".
   WE DO NOT COPY HER SENTENCE, and that is deliberate. "Verified & safe to
   run" is one claim; the manifest reports TWO separate facts and either can be
   false (entry.signed, entry.notarized), and this product's whole UI direction
   is that a claim is never made ahead of the thing that confirms it. So the
   line is built from the real per-entry words that were already on the pills
   -- Signed / Signature not verified / Notarized by Apple / Notarization not
   verified -- joined into one line, and the green check is shown ONLY when
   every fact in it is confirmed. A card whose signature did not verify says so,
   in neutral muted text with no check, exactly as its pill did.
   Green here is rule 3's done/applied, unchanged from .ds-badge.ok's own use
   of --mint / #0A6242 -- this is a restyle of that state, not a new meaning. */
.ds-trust {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0 6px;
  margin: 1px 0 0;
  color: var(--haze);
  font-family: var(--font-body);
  font-size: 11.5px;
  font-weight: 700;
  line-height: 1.4;
}
.ds-trust.ok { color: var(--done-text); }
:root[data-theme="light"] .ds-trust.ok,
:root:not([data-theme]) .ds-trust.ok { color: #0A6242; }
.ds-trust-mark { font-weight: 800; }

/* Version and compatibility on one line, per "version and compatibility" as a
   single item of the compact card. Two spans rather than one concatenated
   string so each stays its own translatable unit, with a separator that is
   aria-hidden because a screen reader should hear two facts, not a bullet. */
.ds-download-meta {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0 6px;
  margin: 0;
  color: var(--haze);
  font-size: 11.5px;
  line-height: 1.4;
}
.ds-requirements { margin: 0; color: var(--haze); font-size: 11.5px; line-height: 1.4; }

/* MEASURED 2026-09-20, and this is the single most visible defect on this
   dialog: this control computed border-radius 0px and Nunito 400.
   It is an <a class="primary btnlink ds-download-action">, and app.css gives
   the pill radius and the 700 weight to the `button` ELEMENT selector
   (`button { font-family: var(--ui); font-weight: 700; border-radius:
   var(--radius-pill) }`), not to `.primary` -- so an anchor wearing .primary
   inherits the fill and loses the shape and the weight. Every other primary
   control in the product is a real <button>, which is why nothing caught it.
   Restoring exactly what `button {}` would have supplied, plus her measured
   BUTTON family: Quicksand (--font-heading), which is the family she reserves
   for buttons. Colour is deliberately NOT set here -- it belongs to .primary,
   and a second gradient literal in this file would be a second thing to keep
   in step with app.css. */
/* HER BUTTON SYSTEM, NOT A BESPOKE HERO CONTROL — and the brief's "no
   oversized bespoke gradients appear unless they are present in the approved
   mockup" is satisfied by KEEPING the gradient, not by removing it.
   MEASURED off the approved 2026-09-16 handoff, DownloadInstaller.dc.html
   rendered at 1440 and read with getComputedStyle
   (.scratch/device-setup-dialog/mockup-DownloadInstaller.txt): "Download for
   Windows" and "Download for Mac" are
   `linear-gradient(135deg, rgb(27,74,115) 0%, rgb(79,168,224) 100%)` at
   border-radius 999px, 51px tall, run full width inside their card. That is
   app.css's own `.primary` / --primary-cta-gradient token, so this control is
   already the established one; what was wrong was its SIZE relative to
   everything around it.
   app.css `.primary` ships padding 14px, font-size 16px, min-height 50px and
   margin-top 12px, tuned for a full-width page CTA. Inside a 340px card that
   is the "more visual weight than the actual device-setup instructions" the
   owner is objecting to, so the three are pulled back to the product's
   standard button metrics (44px min target, 14.5px) while the fill, the
   radius and the family stay exactly hers.
   `margin-top: auto` keeps the pills on a shared baseline when the two cards
   have different-length trust lines -- "compact, aligned". */
.ds-download-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  width: 100%;
  min-height: 44px;
  margin-top: auto;
  padding: 11px 16px;
  border-radius: var(--radius-button, var(--radius-pill, 999px));
  font-family: var(--font-heading);
  font-size: 14.5px;
  font-weight: 700;
  box-shadow: none;
}

.ds-status { margin: 0; color: var(--haze); font-size: 12px; line-height: 1.45; }
.ds-status.error { color: var(--coral); }
:root[data-theme="light"] .ds-status.error,
:root:not([data-theme]) .ds-status.error { color: #B23127; }

.ds-retry { margin-top: 9px; }

/* NOW LIVES IN .ds-foot (index.html), not at the bottom of the scroll region.
   `margin-right: auto` is what pins it to the LEFT of the footer bar with Done
   at the right; `flex: 1 1 260px` lets it take its own row below 560px rather
   than squeezing Done. It is never display:none -- see .ds-foot's comment. */
.ds-next {
  flex: 1 1 260px;
  margin: 0 auto 0 0;
  color: var(--haze);
  font-size: 12px;
  line-height: 1.4;
}

.ds-done { flex: none; }

.ds-manual {
  display: inline-flex;
  margin-top: 9px;
}

@media (max-width: 700px) {
  .ds-intents { grid-template-columns: 1fr; }
  .ds-intent { min-height: 66px; }
}

@media (max-width: 420px) {
  .ds-dialog {
    width: calc(100vw - 10px);
    max-height: calc(100dvh - 10px);
    border-radius: 17px;
  }
  .ds-shell { --ds-pad: 14px; max-height: calc(100dvh - 10px); }
  .ds-head { padding: 13px var(--ds-pad) 11px; }
  .ds-body { padding: 13px var(--ds-pad) 16px; }
  .ds-foot { padding: 11px var(--ds-pad) max(11px, env(safe-area-inset-bottom)); }
  .ds-downloads { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .ds-dialog::backdrop { backdrop-filter: none; }
}

@media (forced-colors: active) {
  .ds-intent[aria-pressed="true"] { outline: 3px solid Highlight; outline-offset: -4px; }
  .ds-truth { border-color: Highlight; }
  /* The recommended card is identified by a colour-mixed border, and
     forced-colors replaces every author border colour with one system colour —
     so without this the recommendation simply disappears for the parents most
     likely to need it pointed out. */
  .ds-download-card.is-recommended { outline: 2px solid Highlight; outline-offset: -3px; }
  .ds-step-index { border: 1px solid ButtonText; }
}

/* ---- DEVICE SETUP PROGRESS (Redesign Ledger device-setup area, decision
   idx0) — js/device-setup-progress.js. Reuses .ds-dialog/.ds-shell/.ds-head/
   .ds-body/.ds-foot/.ds-status wholesale so this screen never drifts from the
   acquisition dialog's own sizing, theming and reduced-motion rules; only the
   phase list and blocker card below are new. */

/* GoodQA #416: the free-text detail a real installer can attach to a status
 * report, alongside whichever phase/blocker is current. Reuses .ds-status's
 * muted/small styling; only the extra bottom margin is specific to sitting
 * above the phase list rather than at the foot of the dialog. */
/* `.dsp-dialog` (index.html:4004, `<dialog class="ds-dialog dsp-dialog">`)
 * had NO rule in ANY loaded stylesheet, which is half of the pair
 * style-vocabulary.test.js reports at HEAD; the other half, `dm-picker`, is
 * app.css's and is deliberately untouched here (follow-up DF-18). A class in
 * the markup that nothing styles is either dead or a missing rule, and this
 * one is the second: the progress dialog is a short status readout, not the
 * chooser, so sharing .ds-dialog's 920px stretched her phase card roughly
 * twice as wide as the column she draws it in. 720 is a judgement, not a
 * measurement off her artboard -- hers is a full page, not a dialog, so there
 * is no dialog width of hers to copy; it is the width at which the phase rows
 * stop looking stretched, checked in the browser at 1512x982. */
.dsp-dialog { width: min(720px, calc(100vw - 24px)); }

.dsp-message { margin-bottom: 10px; }

/* HER DeviceSetupProgress, measured: the phases are ONE white card at the
   card edge (.08 / r22) with the rows divided by hairlines -- not one
   bordered box per row. Live drew six separate outlined pills stacked with a
   10px gap, which is the loudest difference between this screen and her
   artboard: six competing boxes where she has a single calm list. State is
   carried by the MARK and the label weight, exactly as she draws it, so the
   rows below no longer need a border or a fill of their own. */
.dsp-phases {
  display: grid;
  gap: 0;
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--border-subtle, var(--line));
  border-radius: var(--radius-card, 22px);
  background: var(--dusk2);
  /* Her card sits on a white dialog body, so the .08 edge alone would not
     separate it; the soft card shadow app.css already defines is what makes
     the panel read as a panel in her artboard. */
  box-shadow: var(--shadow-card, none);
  overflow: hidden;
}

.dsp-phase {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 15px 18px;
  border: 0;
  border-bottom: 1px solid var(--border-subtle, var(--line));
  border-radius: 0;
  background: transparent;
  color: var(--text-tertiary, var(--haze));
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.4;
}
.dsp-phase:last-child { border-bottom: 0; }

/* UI-DIRECTION rule 1: 'Applied \2713' is device-confirmed, 'Pending\2026' is
   requested-not-confirmed. This screen carries the same discipline: a phase
   is only ever drawn 'done' once the SERVER reported it, never optimistically
   ahead of the polled state. */
.dsp-phase-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--line);
  font-size: 12px;
  font-weight: 800;
}

/* A DONE row is her solid green disc with a white tick and a full-strength
   label; a CURRENT row is her blue ring and a full-strength label; an
   untouched row keeps the empty grey ring and the muted label set above.
   The green disc is --mint (a FILL, so her literal #3FA66C per the
   2026-09-20 third ruling's role split); the tick glyph sits ON that fill,
   not on the page, so 1.4.11's 3:1 applies to it rather than 1.4.3's 4.5:1.
   THE TICK COLOUR IS PER-THEME AND BOTH HALVES ARE DELIBERATE. Light keeps
   her artboard's white tick on #3FA66C (3.05:1 -- clears 1.4.11). Dark keeps
   --dusk, because dark's --mint is #4EE0A5 and a white tick on that measures
   ~1.7:1; carrying the light literal into both themes would have been a real
   regression on the theme nobody screenshotted. */
.dsp-phase.is-done { color: var(--text-primary, var(--cloud)); }
.dsp-phase.is-done .dsp-phase-mark {
  border-color: transparent;
  background: var(--mint);
  color: var(--dusk);
}
:root[data-theme="light"] .dsp-phase.is-done .dsp-phase-mark,
:root:not([data-theme]) .dsp-phase.is-done .dsp-phase-mark { color: #FFFFFF; }

.dsp-phase.is-current { color: var(--text-primary, var(--cloud)); }
.dsp-phase.is-current .dsp-phase-mark {
  border-color: var(--glow);
  border-width: 2px;
  color: var(--glow);
}

/* UI-DIRECTION-2026-08-22.md §3 (binding): amber = attention/a request
 * waiting, coral = destructive/paused. A setup blocker (e.g. "Airplane mode
 * is on", "This computer is not allowed to open the tablet") is a
 * recoverable attention-waiting state a parent can act on, not a
 * destructive/paused one -- the same distinction overview.js's own
 * attentionStrip/.badge.attn already draws in amber. Round 5 audit finding
 * (GoodQA #415): this card shipped painted coral; no OWNER DECISION or
 * docs/OWNER-DECISIONS.md entry sanctions that, so it was a fresh
 * miscoloring, not an intentional deviation. */
.dsp-blocker {
  margin-top: 14px;
  padding: 14px;
  border-left: 3px solid var(--amber);
  border-radius: 10px;
  background: color-mix(in srgb, var(--amber) 9%, var(--dusk));
}
:root[data-theme="light"] .dsp-blocker,
:root:not([data-theme]) .dsp-blocker { background: color-mix(in srgb, var(--amber) 12%, var(--panel, var(--dusk))); }

.dsp-blocker-title {
  margin: 0 0 6px;
  color: var(--cloud);
  font-size: 14px;
  font-weight: 760;
}

.dsp-blocker-explain {
  margin: 0 0 8px;
  color: var(--haze);
  font-size: 12.5px;
  line-height: 1.45;
}

.dsp-blocker-fixes {
  margin: 0 0 10px;
  padding-left: 18px;
  color: var(--cloud);
  font-size: 12.5px;
  line-height: 1.55;
}

.dsp-done {
  margin-top: 14px;
  padding: 12px 14px;
  border-left: 3px solid var(--mint);
  border-radius: 10px;
  background: color-mix(in srgb, var(--mint) 9%, var(--dusk));
}
.dsp-done-text { margin: 0; color: var(--cloud); font-size: 13px; line-height: 1.45; }

@media (max-width: 420px) {
  .dsp-phase { padding: 9px 10px; font-size: 12.5px; }
}

/* =======================================================================
   L4 — INLINE QUICK ACTIONS IN THE DEVICE MENU (js/devices.js)

   OWNER, 2026-09-20, on the live portal: the device menu's items "take you
   to some other page where you could 'look around and try to find how to do
   what you wanted' - they should be built-in functions - all without leaving
   the page." js/devices.js replaces two of those doorways with the control
   itself; these are the styles for the control.

   IN THIS STYLESHEET AND NOT app.css, because app.css belongs to the
   coordinator this pass. Everything below is namespaced `.dqa*` and only
   ever applies inside `.device-menu-actions`, so it cannot reach a card,
   a form or a row anywhere else in the product.

   NO display / visibility / opacity RULE ON `.dqa-msg`. It is the element
   js/devices.js writes every result and every failure into, and a stylesheet
   that can hide it is a behaviour change wearing a stylesheet (the
   #pauseAllMsg regression, 2026-09-19, which zero tests caught). An empty
   block element already occupies no height, so there is nothing to hide.

   TOKENS ONLY, AND EXISTING ONES. --surface-raised (#F1F3F6 light,
   var(--dusk2) dark) is what separates this block from the panel behind it;
   --line is the owner-accepted rgba(22,35,63,.16) and is deliberately NOT
   "fixed" here. No new colour meanings: the chip is --haze, not mint/amber.
   ======================================================================= */

/* THE SCROLL CONTAINER ITSELF (#deviceMenuActions / .device-menu-actions).
   app.css owns `overflow-y: auto` and the flex-sizing that makes this list
   scroll at all (GoodQA #427) — this section touches neither. It only adds
   two purely cosmetic properties app.css does not set, so there is no
   double-declaration to collide with once that lane's own work lands.

   OWNER'S SCREENSHOT SET, 2026-09-20 19:01 (190151.png):

   1. AN UNSTYLED OS SCROLLBAR sitting inside an otherwise soft, fully-rounded
      dialog — a thick square-cornered track+thumb+arrow-button stack is the
      one piece of chrome in the whole modal that looks like it belongs to a
      different, older application. `scrollbar-width/-color` (Firefox-shaped
      API, also implemented by current Chromium) and the `::-webkit-scrollbar*`
      pseudo-elements (Chromium/Safari) together restyle it to a thin, pill-
      radius thumb in the SAME --line token every control edge in this menu
      already uses, rather than inventing a colour.
   2. A ROW SLICED IN HALF AT THE TOP EDGE of the scrolled list ("...or kid
      mode", cut mid-row, directly under the offline banner). MEASURED: a
      freshly-opened menu starts at scrollTop 0 with the first row intact —
      this is not a positioning bug, it is what a normal scroll position
      partway down the list looks like, and normal scrolling is not going
      away. A soft fade at the scroll edges (a `mask-image` gradient) turns a
      row that is cut off into a row that is fading out of view, which is
      the standard, deliberate way a scrollable list signals "there is more
      above" without a hard-edged clip reading as broken. */
.device-menu-actions {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  /* NO mask-image FADE. REVERTED 2026-09-20 BY THE COORDINATOR, SAME DAY IT
     SHIPPED, AND THE REASON IS WORTH KEEPING.
     The intent was good: turn the hard-clipped half-row at the scroll edge
     into something that reads as "there is more above/below". What it actually
     did was GHOST REAL CONTROLS. A 14px fade over a 48px row washes out the
     top third of it, so "Remote view and device tools" and "Open setup window"
     sitting at the dialog's bottom edge rendered pale and half-cut — they read
     as DISABLED. Measured by rendering the menu and looking at it, after the
     owner's reaction to the deployed build.
     A crisp half-row is honest: it plainly means "scroll". A faded half-row
     tells a parent the control is unavailable. Between a clip that looks
     unfinished and a fade that lies about affordance, take the clip.
     If this is revisited: give the scroll container bottom padding so a row is
     never bisected at rest, rather than fading the rows themselves. */
}
.device-menu-actions::-webkit-scrollbar { width: 8px; }
.device-menu-actions::-webkit-scrollbar-track { background: transparent; }
.device-menu-actions::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
.device-menu-actions::-webkit-scrollbar-thumb:hover { background: var(--haze); }

/* Spans the menu's two-column grid: a label, a control and a result line do
   not fit in one half-width cell, and splitting them across columns would put
   a parent's result somewhere other than under the control that produced it.
   OWNER, 2026-09-20: "the device modal menu is super jank looking.. all
   sorts of spacing and border issues." MEASURED (this lane,
   .scratch/devicemenu-shots/report-desktop.json): this block carried
   `border: 1px solid var(--line)` around a `.dqa-form > .inp` field that ALSO
   carries `border: 1px solid var(--line)` (same colour) -- a card wrapping a
   control in its own edge, i.e. a doubled border, one row deep. Checked
   against the owner's own binding 2026-09-16 handoff (ConfirmDeleteModal's
   grey summary box and CheckinModal's amber notice, both rendered and
   measured live via Playwright): HER highlighted note/group boxes inside a
   modal never carry their own heavy CONTROL-weight border -- fill + radius,
   the border (when there is one) lives on the interactive control alone.

   OWNER'S OWN SCREENSHOTS, 2026-09-20 19:01 (Screenshot 190151.png): with
   the border removed entirely, the coordinator relayed that these two grey
   blocks still "read as a different design system" from the white,
   --line-bordered doorway pills beside them -- fill alone wasn't enough of a
   shared visual vocabulary. Resolved with the owner's OWN two-tier border
   system, already ruled and already a token: CARD edges are
   `--border-subtle` (rgba(22,35,63,.08)), lighter and structurally different
   from a CONTROL edge (`--line`, rgba(22,35,63,.16)) -- exactly the
   distinction that keeps this from re-introducing the original doubled-edge
   problem (a --border-subtle wrapper around a --line control reads as two
   DIFFERENT kinds of edge, not one edge stacked on itself). This is the
   product's own "a card is not a control" rule, applied to a card that
   happens to hold a control.

   LIGHT THEME ONLY — CORRECTED after an independent adversarial review
   (2026-09-20) caught this being wrong: app.css's BASE `:root` block (the
   DARK theme's own tokens) declares `--border-subtle: var(--line)` — a
   literal alias, not a distinct value. Only `:root[data-theme="light"]`
   gives the two tokens different alpha (`.16` vs `.08`). Applying this
   border unconditionally would have reintroduced the EXACT pass-1 doubled
   edge in dark mode (verified: both borders compute to the identical
   rgb(37,52,75)) — recolouring the bug, not fixing it. Scoped to light,
   which is this product's default theme and what the owner's own
   screenshots show; dark keeps pass 1's original, still-correct answer
   (fill only, no border — see the base rule below). */
.device-menu-actions .dqa {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border-radius: 14px;
  background: var(--surface-raised);
}
:root[data-theme="light"] .device-menu-actions .dqa {
  border: 1px solid var(--border-subtle);
}

.device-menu-actions .dqa-title {
  color: var(--cloud);
  font-size: 12.5px;
  font-weight: 800;
  line-height: 1.3;
}

.device-menu-actions .dqa-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* The field takes the slack, the Send button keeps its own size — and
   `min-width: 0` is what stops a long placeholder or a long app name from
   pushing the button out of a 320px-wide panel.

   `width: auto` IS LOAD-BEARING AND WAS MEASURED. `.inp`'s base rule is
   `width: 100%`, which beats `flex-basis: 190px` — so with `flex` alone the
   field claimed the whole line and Send wrapped underneath it. That turned
   each block into 180-204px and added 384px to a menu the owner had ALREADY
   called crowded (L3). Side by side, each block is ~105px instead. */
.device-menu-actions .dqa-form > .inp {
  flex: 1 1 190px;
  width: auto;
  min-width: 0;
}

/* `.primary`'s own `width: 100%; margin-top: 12px` is for a form's single
   full-width submit at the foot of a card. This one sits BESIDE its field, so
   both are overridden here rather than by dropping `.primary` — the gradient,
   the shadow and the light-theme white-on-blue contrast fix all live on that
   class, and a bespoke button would have to re-earn every one of them. */
.device-menu-actions .dqa-send {
  flex: 0 0 auto;
  width: auto;
  margin-top: 0;
  min-height: 48px;
  padding: 12px 18px;
  /* OWNER'S SCREENSHOT SET, 2026-09-20 19:04 (190406.png): a similarly-sized
     blue pill button elsewhere in this product wrapped its own label onto
     two lines ("Choose / an app") because the button was narrower than its
     text. Not observed on THIS Send button yet, but it is the same shape of
     control (fixed small padding, no explicit width) and a longer
     translation of "Send" would hit the identical failure with no warning.
     white-space:nowrap is a one-line guard against that whole class. */
  white-space: nowrap;
}

/* THE FIELD FILL IS SET LOCALLY, AND THAT IS A WORKAROUND FOR A GLOBAL BUG
   THIS LANE DOES NOT OWN. MEASURED 2026-09-20 with getComputedStyle on the
   live portal, both themes:

     :root declares --field-fill TWICE (app.css:117 `var(--dusk2)` and
     app.css:186 `#F5F7F9`) inside the SAME base :root block, so the later
     literal wins and --field-fill resolves to #F5F7F9 in DARK as well.
     `.inp { background: var(--field-fill); color: var(--cloud) }` therefore
     paints #EDF5FF text on a #F5F7F9 field in dark — 1.02:1, across the
     WHOLE product, not just here. app.js's own device-menu <select>
     (.dm-picker) measures the identical 1.02:1 in the same frame.

   The one-line fix belongs in app.css, which is the coordinator's this pass,
   and it is reported to them. Until it lands, these two controls set their own
   fill from --dusk — the PANEL's colour — which is correct in both themes on
   its own terms: in light it makes a WHITE field on the grey --surface-raised
   block, which is the designer's own auth-field relationship (white field on a
   card over a tinted surface) rather than the 1.04:1 grey-on-grey the token
   would give here; in dark it recesses (#101B2D field in a #152238 block).
   Remove this rule the moment --field-fill is correct per theme. */
.device-menu-actions .dqa-form > .inp {
  background: var(--dusk);
}

/* THIS RULE WAS REMOVED 2026-09-20 AS A CORRECTION, NOT AN OVERSIGHT —
   read this before re-adding anything like it.

   PASS 1 shipped a `.device-menu-actions .dqa-form > .inp:disabled` rule
   here, on the theory that a disabled `<select>` was falling back to the
   BROWSER's own native theme (measured colours: background rgb(224,232,240),
   border rgb(107,132,157)) because nothing resets `appearance` on
   `select.inp`. That diagnosis was WRONG, caught by an independent
   adversarial review: those exact colours (#E0E8F0 / #6B849D) are
   app.css:8193's own DELIBERATE, ALREADY-CORRECT rule for
   `:root[data-theme="light"] .inp:disabled` — including
   `-webkit-text-fill-color`, which this lane's replacement did not set,
   for exactly the Safari-disabled-text-rendering reason app.css's own
   comment there gives. Pass 1's rule had the SAME specificity
   (`.device-menu-actions .dqa-form > .inp:disabled` vs app.css's
   `:root[data-theme="light"] .inp:disabled`, both (0,4,0)), and won the
   cascade tie purely on SOURCE ORDER (this file loads after app.css) —
   which means it silently replaced a more complete, intentional,
   accessibility-conscious rule with a narrower one that was missing
   `-webkit-text-fill-color` and used different (also fine, but
   unnecessary and undocumented-as-a-second-source-of-truth) colours.
   The fix is to have NO rule here at all, so app.css's own answer for
   "what a disabled .inp looks like" applies to this picker exactly the
   way it applies to every other disabled `.inp` in the product — one
   definition, not two that happen to agree by coincidence today. */

/* THE DRAWER'S FORM CONTROLS FOLLOW THE APP'S THEME, NOT THE OPERATING
   SYSTEM'S — 2026-09-21, owner brief 6: "The 'Keep this page allowed from now
   on' checkbox renders as a nearly black square despite the drawer using the
   light theme."

   MEASURED, and it is one missing declaration rather than a wrong token
   (.scratch/device-menu-drawer/, diag-os-dark.png):

     document.documentElement data-theme          = "light"
     getComputedStyle(:root).colorScheme          = "normal"
     <meta name="color-scheme" content="light dark">   (index.html:31)
     matchMedia('(prefers-color-scheme: dark)')   = true
     the checkbox: appearance "auto", accent-color "auto",
                   background rgba(0,0,0,0), border 0px

   NOTHING in this product's CSS declares `color-scheme` at all — grepped,
   not assumed. With none, the meta tag's "light dark" tells the browser
   "this page supports both, pick per the OS". But this app's theme comes
   from `data-theme`, written by public/theme-preinit.js out of localStorage,
   which the OS knows nothing about. So on any machine set to dark, every
   NATIVELY painted control inside the LIGHT app is drawn with the browser's
   dark form theme. The checkbox is merely the most visible one: a filled
   near-black 18px square in a white card. `background: rgba(0,0,0,0)` and
   `border: 0px` in the measurement are the tell that no CSS is painting it
   at all — the widget is the UA's.

   DECLARED ON THE PANEL, NOT ON :root, AND THAT IS DELIBERATE. The defect is
   product-wide (.consentRow's checkbox, .happs-cell-check's, every <select>,
   every date and time field), and the one-line `:root[data-theme="light"] {
   color-scheme: light }` is the genuinely right fix — but it repaints every
   native control, scrollbar, dropdown and autofill in the app, which is a
   pass that can verify all of them, not this lane. This fixes its own
   component, which is exactly what the brief's own required behaviour asks
   for ("Apply light-theme form-control tokens explicitly inside the light
   drawer"), and the product-wide finding is written up as a follow-up rather
   than smuggled in behind a drawer ticket. BOTH directions are set, because
   the invariant is "the drawer matches the APP": a dark-theme drawer on a
   light-preferring OS is the same bug mirrored.

   THE NO-ATTRIBUTE CASE IS LIGHT, NOT DARK, AND THAT IS THIS PRODUCT'S OWN
   CONVENTION RATHER THAN A GUESS. public/theme-preinit.js sets `data-theme`
   before first paint and writes only "light" or "dark", defaulting to light
   ("LIGHT IS THE DEFAULT (owner, 2026-09-20)"), so the attribute is missing
   only if that blocking script never ran. app.css already answers that case
   the same way in ~30 places — `:root:not([data-theme]) .pill.on`,
   `.banner.warn`, `.devlockbtn.lock` and the rest all carry the LIGHT
   values. A first draft of this rule used `:not([data-theme="light"])`, which
   would have handed that same case the DARK form theme and quietly disagreed
   with every one of them. */
:root[data-theme="light"] .device-menu-panel,
:root:not([data-theme]) .device-menu-panel { color-scheme: light; }
:root[data-theme="dark"] .device-menu-panel { color-scheme: dark; }

/* ONE CLICKABLE CONTROL, VERTICALLY CENTRED, ONE DECLARED GAP (brief 7).
   `.dqa-check` is already a <label> wrapping both the box and its text, so it
   was always one hit target; what made it read as "detached from the label"
   is that the UA's own `margin: 3px 3px 3px 4px` on the input sat on TOP of
   the flex gap — 12px of space on one side of an 18px box and 11px on the
   other, none of it declared anywhere. Zeroed, and the gap declared once at
   10px, the middle of the brief's 8-12px. `flex: 0 0 auto` with an explicit
   square size stops the box being squeezed out of shape at 320px, where the
   label genuinely wraps. */
.device-menu-actions .dqa-check {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  color: var(--haze);
  font-size: 12.5px;
  line-height: 1.4;
}
.device-menu-actions .dqa-check > input[type="checkbox"] {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  min-width: 18px;
  min-height: 18px;
  margin: 0;
  /* CHECKED USES THE PRODUCT'S BRAND ACCENT, WHICH IS ALREADY A TOKEN AND
     ALREADY MEANS THIS. UI-DIRECTION rule 3 gives brand blue exactly one
     meaning — trusted + primary — and a checked primary control is that;
     `--glow` is the same value `.primary` builds its gradient from (light
     theme moved it teal -> brand blue, app.css's own note beside .primary).
     No sixth colour invented, and green/amber/indigo/red keep their meanings.
     `accent-color` keeps the NATIVE checkmark and native hit behaviour rather
     than redrawing a tick behind `appearance: none`, which would have to
     re-earn the focus ring, the indeterminate state and the forced-colours
     fallback this control gets for free. */
  accent-color: var(--glow);
}
/* THE PRODUCT'S OWN FOCUS RING. app.css's shared `:focus-visible` rule lists
   button / a / summary / [tabindex] and the `.inp` family — a bare checkbox
   is in neither list, so this control had only the browser default. Same 2px
   var(--glow) ring every other control in this drawer shows. */
.device-menu-actions .dqa-check > input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 2px;
}
/* A hover cue for a natively painted box, without giving up the native tick. */
.device-menu-actions .dqa-check:hover > input[type="checkbox"] {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--glow) 20%, transparent);
  border-radius: 4px;
}

.device-menu-actions .dqa-msg {
  margin: 0;
  color: var(--haze);
  font-size: 12.5px;
  line-height: 1.45;
}

/* "Opens a screen" / "Opens a window here" — the badge on the items that
   genuinely are doorways. A PROVENANCE-style neutral chip, deliberately
   borrowing none of mint (healthy), amber (waiting) or coral (paused):
   it says where a tap goes, never how a tablet is.

   OWNER'S SCREENSHOT, 2026-09-20 19:01 (190151.png), RAGGED ROW RHYTHM: as an
   `inline-block` this chip flowed as ordinary text after the label, so it
   sat on the SAME line for a short label ("Allowed apps", "More settings",
   "Remove device") and wrapped to a SECOND line for a longer one ("Allowed
   web pages", "Remote view and device tools", "Open setup window", "Adjust
   remaining time") — whichever happened to fit. Four rows one line tall next
   to four rows two lines tall, in the SAME 2-column grid, is exactly the
   "spacing... issues" complaint: no reader can tell that was a rule rather
   than an accident. ONE RULE, applied to every row alike: the chip is now
   `display: block`, which unconditionally starts a new line regardless of
   label length, so every doorway row is the same shape. `width: max-content`
   keeps it sized to its own text (a pill, not a full-width bar) — a bare
   `display: block` with no width would stretch it to the row's full width. */
.device-menu-actions .dqa-goes {
  display: block;
  width: max-content;
  /* RESERVED SPACE, NOT A MARGIN — owner brief 5: "Treat it as supporting
     metadata with its own reserved space". The row is a flex COLUMN as of
     2026-09-21 (app.css, `.device-menu-action`), so the space between the
     label and this chip is that column's declared `gap` — one value, shared
     with every other row, instead of a margin that only this element knew
     about and that stacked on top of the gap. `max-width` is the guard for a
     longer translated promise ("Ouvre une fenêtre ici") in a 239px cell. */
  margin: 0;
  align-self: flex-start;
  max-width: 100%;
  padding: 2px 7px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--haze);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* A very narrow panel (320px and down) is the only place the field and the
   button genuinely cannot share a line. Below that the button takes its own
   row rather than shrinking into a target a thumb cannot hit. */
@media (max-width: 360px) {
  .device-menu-actions .dqa-send { flex: 1 1 100%; }
}

/* ===========================================================================
   THE DEVICE MENU AS THE DESIGNER DRAWS A MODAL — 2026-09-20.
   ===========================================================================
   The owner, on the deployed build: "the device modal menu is super jank
   looking.. all sorts of spacing and border issues", and then, after two
   rounds of spot fixes, "its not fixed at all basically". He was right, and
   the earlier rounds were aimed one level too low: they straightened the chip
   rhythm and the scrollbar inside a modal whose SHAPE was never hers.

   MEASURED, not guessed. Her CheckinModal.dc.html rendered in a real browser
   and read with getComputedStyle, beside ours read the same way:

                         HERS                       OURS (before)
     dialog padding      40px                       18px
     dialog radius       28px                       21px
     dialog border       none (shadow only)         1px solid rgba(22,35,63,.16)
     dialog shadow       0 30px 70px rgba(0,0,0,.35) 0 24px 60px rgba(0,0,0,.45)
     field radius        14px                       12px
     field fill/border   #F5F7F9 / rgba(22,35,63,.16)  same  <- already right
     999px pill radius   used for CHILD CHIPS only   used for EVERY ACTION ROW

   THE HEADLINE: she reserves the 999px pill for small chips — a child's name,
   a filter. Her modal's own content sits in 14px rounded rectangles. Ours made
   all seven destination rows full pills, which reads as a scatter of chips
   rather than a menu, and is the single biggest reason it does not look like
   her drawing. 18px of padding against her 40px is the second.

   SCOPED TO `.device-menu-panel`, which is this dialog's own class, so the
   shared `.pickwrap`/`.pickpanel` (the app picker, five other callers) and the
   global `.ghost` button are untouched. `.ghost` in particular is used across
   the whole product and its 999px radius is correct everywhere else. */
.device-menu-panel {
  padding: 28px;
  border-radius: 28px;
  border-color: transparent;
  box-shadow: 0 30px 70px rgba(0,0,0,.35);
}
.device-menu-panel .toprow { margin-bottom: 14px; }

/* HER SHAPE FOR A ROW, NOT A CHIP. 14px matches her fields and her cards, and
   matches the `.dqa` blocks these rows sit between — which were ALREADY 14px,
   so the pills were inconsistent with their own neighbours inside this very
   menu, not only with her design. Weight drops 700 -> 600: at 14px, 700 on
   seven stacked rows reads as seven headings. */
.device-menu-panel .device-menu-actions .device-menu-action {
  border-radius: 14px;
  font-weight: 600;
}
/* Her field radius exactly. */
.device-menu-panel .device-menu-actions .inp,
.device-menu-panel .device-menu-actions input.dqa-url { border-radius: 14px; }

/* A ROW THAT CARRIES A CONTROL NEEDS THE WHOLE WIDTH.
   "Parent mode or kid mode" is a label plus a <select> sharing one cell of a
   two-column grid, and at 560px the cell is ~247px wide — so the label wrapped
   onto THREE lines beside a squeezed select and the row read as broken. Every
   other row in that grid is a label and a small chip, which fits.
   `:has()` keeps this to rows that actually contain a control rather than
   hard-coding the one key, so the next row to gain a select is handled too. */
.device-menu-panel .device-menu-actions > *:has(select),
.device-menu-panel .device-menu-actions > *:has(input:not([type="checkbox"])) {
  grid-column: 1 / -1;
}
