/* ParentProof Secure — parent settings, served from platform.framebright.ai.
 *
 * DESIGN-SYSTEM FOUNDATION · 2026-09-18
 * The binding visual source is the 2026-09-16 handoff named in
 * docs/OWNER-DECISIONS.md §0.4. This layer owns only fonts and tokens; the seven
 * screen-area epics apply them to individual components. Dark remains the bare
 * :root palette and every pre-existing dark value below is intentionally exact.
 */

/* Self-hosted Google Fonts latin subsets. The static server already sends WOFF2
 * as font/woff2 and the CSP already permits font-src 'self'; no remote stylesheet,
 * font origin, inline style, or CSP relaxation is needed. */
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/fonts/nunito-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/fonts/nunito-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/fonts/nunito-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 800; font-display: swap;
  src: url('/fonts/nunito-800-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Quicksand'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/fonts/quicksand-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Quicksand'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/fonts/quicksand-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Quicksand'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/fonts/quicksand-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* LIGHT IS THE DEFAULT SINCE 2026-09-20, AND IT IS THE DESIGN — NOT A
 * PREFERENCE. This block is therefore the DARK palette, which is now reached
 * only by an explicit opt-in; the light palette lives in the
 * `:root[data-theme="light"]` block further down and is what a parent sees on
 * a cold load.
 *
 * It said the opposite until 2026-09-20, and the second half of that sentence
 * is the part that has NOT changed and is worth keeping: the default is a
 * DESIGN decision, not a reading of the parent's machine. There is no
 * `prefers-color-scheme` fallback here or in app.js's boot, on purpose, and
 * nav-reference-parity.test.js points a DARK-mode browser at the dashboard and
 * requires light specifically so that cannot quietly come back.
 *
 * Owner ruling: "absolutely light needs to be default". The 2026-09-16
 * prototype handoff is drawn light on all 49 artboards and was ruled binding
 * on 2026-09-18. See plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md.
 *
 * NOTE, because this comment is load-bearing: tablet-setup-branding-ui.test.js
 * greps this file for the "LIGHT IS THE DEFAULT" sentence. The tablet hand-off
 * page has no theme toggle and follows whatever the portal defaults to, so
 * that test reads the decision HERE rather than restating it.
 *
 * Legacy dark tokens are kept because the live UI already consumes these names.
 * The role-named aliases below let the re-skin stop spreading hue names while
 * resolving to those same, already-reviewed dark values. */
:root {
  color-scheme: dark;
  --ink:#0A1020; --dusk:#101B2D; --dusk2:#152238; --glow:#69D8FF; --coral:#FF6B5E;
  --mint:#4EE0A5; --sky:#9C8CFF; --cloud:#EDF5FF; --haze:#91A2BB; --line:#25344B;
  --bg:#09101D; --statustop:#11263A; --statusbot:#102033; --arowline:#25344B; --track:#334764;
  /* GOODQA #32 — `--track` (#334764) is the shared muted-fill token: switch
     tracks (`.toggle.off`/`.sw.off`), progress-bar troughs (`.ovbar`,
     `.loginstep`), image placeholders and secondary button fills all paint
     it. A SWITCH'S TRACK IS ITS ENTIRE VISIBLE SHAPE — no separate border —
     so it alone has to clear WCAG 1.4.11's 3:1 non-text-contrast floor
     against the card it sits on; the other consumers do not carry that same
     obligation the same way (a progress trough is not a "boundary of a UI
     component" in the same sense). MEASURED: #334764 is only 1.83:1 against
     `--dusk` (#101B2D), 1.69:1 against `--dusk2`, 2.02:1 against `--bg`.
     `--track-switch` SPECIALIZES the token for switches alone, same hue and
     saturation as `--track`, lightened until the floor clears against every
     card background a switch renders on: 3.38:1 / 3.66:1 / 4.04:1
     respectively (WCAG 2.x relative luminance, computed). `--track` itself is
     UNCHANGED — its other consumers keep their exact existing pixels. */
  --track-switch:#5475A4;
  /* The five semantic roles are exposed below. These legacy hue tokens retain
     their exact dark values so existing dark screens do not move. */
  --amber:#FFC44B;
  /* GOODQA #465/#466/#468/#470, OWNER-DECISIONS.md §0.4 (2026-09-18), decisions
     #508/#509/#510 (locked, see plans/DECISIONS-2026-09-18-prototype-conflicts.md).
     The 2026-09-16 handoff is binding for LIGHT only; dark keeps every value
     above unchanged — this pass adds two tokens neither theme had before, and
     retints one existing one (--glow, below, light block only).

     --danger and --indigo are NEW — the handoff's own light literals (#C0342C,
     #6D5BD0) are adopted for light AS GIVEN (both clear 4.5:1 on white: 5.57:1
     and 5.18:1 — computed, WCAG 2.x relative luminance). Dark has no prior
     value to keep, so these are DERIVED, same hue, lightened for a dark
     ground, following the --coral/--mint/--sky/--amber dark derivations
     already in this file: --danger dark #EE5864 (5.63:1 on --bg, 4.72:1 on
     --dusk2); --indigo dark #A792FC (7.4:1 on --bg, 6.19:1 on --dusk2). Both
     verified against the two backgrounds anything painted with them actually
     sits on, not just #FFFFFF/--bg in isolation. See `.pill.danger`/
     `.pill.attn` below for the further-darkened/lightened literals their own
     translucent fills need (same discipline as `.pill.off`'s own #FFB4AC). */
  --danger:#EE5864;
  --indigo:#A792FC;
  /* GOODQA #467 — "no dedicated sidebar-chrome token pair" (evidence: the
     rail paints `.nav { background: var(--dusk) }`, the SAME token a card
     uses, so rail and page read as one surface). Dark KEEPS today's actual
     look: these two new tokens just point at --dusk/--haze's existing dark
     values rather than inventing a new one — "dark keeps today's token
     values" (§0.4) is honoured at the pixel, not just the number. Light
     gets the handoff's own two-tone chrome below, since light had no
     rail-vs-card distinction to keep. */
  --railbg: var(--sidebar-bg);
  --railtext: var(--sidebar-inactive);
  --radius: 20px;
  /* GOODQA #470 — the handoff's dominant button/chip radius (259 of its own
     `border-radius:999px`, against `.card`'s 20-32px, which is already
     on-spec and untouched here). New token, not a rename: `.card` keeps
     reading --radius. */
  --radius-pill: 999px;
  --shadow: 0 24px 70px #0007;
  /* THE DARK FIELD FILL. Her measured light value (#F5F7F9) lives in the
     `:root[data-theme="light"]` block below, NOT here — exactly like
     --surface-raised above it.
     MEASURED REGRESSION, 2026-09-20: #F5F7F9 was declared a second time INSIDE
     THIS SAME :root BLOCK, after this line, so it won at every theme and dark
     painted the LIGHT fill. axe-core caught it on #deviceTaskSelect at 320x568
     and 440x956 in dark: contrast 1.02:1, #EDF5FF text on #F5F7F9. It shipped
     live in 949c1ed3 as part of a change whose whole purpose was contrast. A
     light value in the dark base block is not an override, it is a takeover. */
  --field-fill: var(--dusk2);
  /* Typography is not a per-theme choice (UI-DIRECTION §3) — --font-heading and
     --font-body below are unscoped globals, so headings/body already render in
     Quicksand/Nunito in dark today. --ui itself leads with Nunito too, so the
     ~13 form controls that set font-family: var(--ui) directly (button, input,
     textarea labels, etc.) match rather than silently staying on the old system
     stack while their surrounding headings moved. */
  --ui: 'Nunito', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  /* No dark colour was invented: roles alias existing dark tokens, while
     #8FE7FF is the live CTA gradient's existing second endpoint. Measured worst
     case on --dusk2: primary 14.50, secondary/control 6.14, brand 9.76,
     done 9.51, waiting 10.05, attention 5.77, danger 5.70. */
  --font-body: 'Nunito', var(--ui);
  --font-heading: 'Quicksand', var(--ui);
  --text-primary: var(--cloud);
  --text-secondary: var(--haze);
  --text-tertiary: var(--haze);
  --text-tertiary-ui: var(--haze);
  --brand: var(--glow);
  --brand-deep: var(--glow);
  --brand-tint: var(--dusk2);
  --brand-bright: #8FE7FF;
  --primary-cta-gradient: linear-gradient(135deg, var(--glow), #8FE7FF);
  --primary-cta-text: var(--ink);
  --info: var(--glow);
  --info-text: var(--glow);
  --info-tint: var(--dusk2);
  --done: var(--mint);
  --done-text: var(--mint);
  --done-accent: var(--mint);
  --done-tint: var(--dusk2);
  --waiting: var(--amber);
  --waiting-text: var(--amber);
  --waiting-accent: var(--amber);
  --waiting-tint: var(--dusk2);
  --attention: var(--sky);
  --attention-text: var(--sky);
  --attention-accent: var(--sky);
  --attention-tint: var(--dusk2);
  --danger: var(--coral);
  --danger-text: var(--coral);
  --danger-accent: var(--coral);
  --danger-tint: var(--dusk2);
  /* A RAISED SURFACE THAT IS ACTUALLY RAISED IN BOTH THEMES.
     --dusk2 was doing this job and still does elsewhere, but in the LIGHT
     palette --dusk and --dusk2 are BOTH #FFFFFF, so every "raised band on a
     card" collapsed to one flat white and the distinction silently stopped
     existing. It went unnoticed because dark was the default and dark's
     --dusk/--dusk2 (#101B2D/#152238) really do differ.
     Dark points at --dusk2 so nothing dark moves; light gets the handoff's own
     neutral #F1F3F6 (the ground she puts her muted "No device yet" pill on).
     Deliberately NOT a global --dusk2 change: 83 rules consume that token and
     re-tinting all of them to fix one band is a far larger visual change than
     the defect warrants. */
  --surface-raised: var(--dusk2);
  --sidebar-bg: var(--dusk);
  --sidebar-inactive: var(--haze);
  --sidebar-active: var(--glow);
  --sidebar-active-indicator: var(--ink);
  --border-subtle: var(--line);
  --border-control: var(--haze);
  /* HER TWO-LAYER CARD SHADOW, measured off the Home artboard:
     rgba(22,35,63,.03) 0 1px 2px, rgba(22,35,63,.04) 0 6px 18px.
     It replaces `0 24px 70px #0007` — a DARK-theme shadow (near-black at 44%)
     that light was inheriting, which is why light cards had no usable depth
     and the whole surface read flat. */
  --shadow-card: 0 1px 2px rgba(22,35,63,.03), 0 6px 18px rgba(22,35,63,.04);
  --radius-pill: 999px;
  --radius-button: 999px;
  --radius-input: 999px;
  --radius-card: 20px;
  --radius-card-large: 32px;
  --radius-control: 14px;
  --radius-icon: 14px;
}

/* Light is opt-in through the persisted theme toggle. This is the single light
 * palette definition: later screen epics consume the role names and do not
 * introduce per-screen colour systems. */
:root[data-theme="light"] {
  color-scheme: light;
  /* Existing screens still consume the legacy hue names. They map onto the new
     foundation here, while the role aliases below are the public token layer. */
  --ink:#16233F; --dusk:#FFFFFF; --dusk2:#FFFFFF; --cloud:#16233F;
  --bg:#FFFFFF; --statustop:#FFFFFF; --statusbot:#FFFFFF;

  /* Text measured on white, THE DESIGNER'S OWN VALUES.
     OWNER DECISION 2026-09-20 — same decision file as the borders below.
     --text-tertiary was #6D778A, the minimum same-family adjustment that
     clears WCAG 1.4.3's 4.5:1 (it measures 4.51:1); the handoff's own #8A93A3
     is 3.10:1 and fails. The owner chose the handoff value knowingly.
     Secondary #5B6472 (5.98:1) and primary #16233F (15.60:1) were already hers
     and already pass, so they are unchanged. */
  --haze:#5B6472;
  --text-primary:#16233F;
  --text-secondary:#5B6472;
  /* BOTH OF THESE HAVE ZERO CONSUMERS -- measured, `var(--text-tertiary)` and
     `var(--text-tertiary-ui)` appear 0 times in app.css and 0 times in css/*.
     ab3cb09c's comment here claimed --text-tertiary paints `.small`, `.legal`
     and `.footbrand`. IT PAINTS NOTHING. That claim was inferred from the
     selectors an axe failure happened to name and was never checked against
     the stylesheet -- the precise trap this very file warns about elsewhere
     ("a token existing is NOT a token being consumed"), written by the person
     who wrote the warning.
     They are kept, at the role-split values, because the role layer is meant
     to be the public token surface and a future consumer should find the right
     pair already here. But nothing renders from them today, so neither value
     can move a single pixel or a single contrast test. Said plainly so the
     next reader does not spend time here believing otherwise. */
  --text-tertiary:#6D778A;
  --text-tertiary-ui:#8A93A3;

  /* Cool navy boundaries, THE DESIGNER'S OWN ALPHA.
     OWNER DECISION 2026-09-20 — plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md
     These were .482, and that was a deliberate, correct accessibility choice:
     the handoff's rgba(22,35,63,.16) measures 1.37:1 on white and WCAG 1.4.11
     wants 3:1 for a UI component boundary, which .482 clears at 3.01:1.
     It was also the single largest reason the designer said the live product
     "doesn't look or feel anything like" her designs — roughly 3x her weight,
     which reads as hard and boxy next to her work.
     Asked directly, with the regression spelled out, the owner chose her exact
     values and accepted the regression. A middle option (soft decorative card
     edges + fill-identified controls, which would have PASSED 1.4.11) was
     offered and declined. This is therefore KNOWN AND ACCEPTED, not an
     oversight — do not "fix" it back without going to the owner, and read that
     decision file first. Revisit if a formal conformance claim is ever needed. */
  --line:rgba(22,35,63,.16);
  --arowline:rgba(22,35,63,.16);
  --track:rgba(22,35,63,.16);
  --border-control:rgba(22,35,63,.16);
  --border-subtle:rgba(22,35,63,.08);
  /* GOODQA #32 is DARK-THEME ONLY (ticket title names `--dusk`). --track's own
     1.37:1-on-white value immediately above is the OWNER-ACCEPTED regression
     this file's own comment already documents — do not fix it here. This is
     the same numeric value under the new specialized name so
     `.toggle.off`/`.sw.off` keep painting EXACTLY what they always have in
     light; nothing about the light switch changes in this pass. */
  --track-switch:rgba(22,35,63,.16);

  /* Brand blue is 5.42:1 on white and 4.75:1 on its tint; deep is 9.24:1.
     The literal bright endpoint #4FA8E0 is 2.62:1 with white, so both its role
     token and the CTA endpoint are minimally darkened to #207CB7, which gives
     white text 4.54:1 across the full gradient. */
  --glow:#2B6CB0;
  --brand:#2B6CB0;
  --brand-deep:#1B4A73;
  --brand-tint:#E8F1FA;
  --brand-bright:#207CB7;
  --primary-cta-gradient:linear-gradient(135deg, #1B4A73, #207CB7);
  --primary-cta-text:#FFFFFF;
  --info:#2B6CB0;
  --info-text:#2B6CB0;
  --info-tint:#E8F1FA;

  /* HER EXACT ACCENTS ON FILLS, ICONS, BADGES AND BORDERS; MINIMALLY DARKENED
     WHERE THE SAME HUE IS READ AS TEXT.
     OWNER DECISION 2026-09-20, second pass — plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md

     The first pass took her literals for text as well, on the owner's ruling.
     Measured cost, which he did not have when he ruled: EIGHT failing
     accessibility tests across Devices, Apps, Categories, search, pricing and
     Activity, every one of them `color-contrast`. Shown that number he chose
     the narrower rule below.

     The split is by ROLE, not by token name:
       -accent  = a FILL, ICON, BADGE GROUND or BORDER. Her literal, always.
                  This is what the eye actually reads as "her palette".
       -text    = GLYPHS. The smallest same-hue darkening that clears WCAG
                  1.4.3's 4.5:1 on the tint it lands on.
     At 11px bold the two are near-indistinguishable; the contrast difference
     between them is the entire defect.

     Measured on the tint each one actually lands on:
       amber  #C2760A -> #9D5E08   3.10:1 -> 4.53:1 on the rendered #F7EEE3 pill
       green  #3FA66C -> #2F7B50   2.69:1 -> 4.55:1 on #E3F5EA
       indigo #6D5BD0 -> #6A57CF   4.31:1 -> 4.51:1 on #ECE8FB
       red    #C0342C is 4.61:1 already and is hers unchanged
     --text-tertiary is a GLYPH token (it paints .small, .legal, .footbrand)
     and takes the same treatment; --text-tertiary-ui keeps her #8A93A3 for
     the non-text uses, where 1.4.3 does not apply.

     NOT CHANGED, and still an owner-accepted regression: the .16 control
     borders. They are a BORDER, so they fall under the -accent half of this
     rule. WCAG 1.4.11 still measures them at 1.38:1 and the test that says so
     is left failing and marked, not suppressed. */
  /* THE BASE HUE TOKENS ARE HERS, BECAUSE THEY ARE THE FILL TOKENS.
     Corrected after a blind review of ab3cb09c, whose commit message stated
     this exact rule and then broke it: it darkened --mint/--amber/--sky
     themselves. Measured, those three are consumed as `background` 27 times
     and as `border*` 9 times in app.css alone -- every toggle ON state, every
     status dot, the overview progress bar, two avatar tones. Darkening them
     shipped her fills darker than her artboard, which is the OPPOSITE of what
     the owner ruled and of what the commit claimed to do.
     The glyph sites now read the -text twins instead: 81 `color:` declarations
     across app.css and css/* were repointed from --mint/--amber/--sky to
     --done-text/--waiting-text/--attention-text. That is a NO-OP in dark,
     where the dark :root defines each -text twin AS an alias of its hue token
     (--done-text: var(--mint)), so only the light palette moves. */
  --mint:#3FA66C;
  --done:#3FA66C;
  --done-text:#2F7B50;
  --done-accent:#3FA66C;
  --done-tint:#E3F5EA;
  --amber:#C2760A;
  --waiting:#C2760A;
  --waiting-text:#9D5E08;
  --waiting-accent:#C2760A;
  --waiting-tint:#FDF1DF;
  --sky:#6D5BD0;
  --attention:#6D5BD0;
  --attention-text:#6A57CF;
  --attention-accent:#6D5BD0;
  --attention-tint:#ECE8FB;
  --coral:#C0342C;
  --danger:#C0342C;
  --danger-text:#C0342C;
  --danger-accent:#C0342C;
  --danger-tint:#FCE4E4;

  /* Sidebar copy/indicator are 11.54/15.60:1 on navy. The specified 10% active
     fill is 1.35:1 as a fill alone, so the white 15.60:1 indicator carries the
     selected-state boundary. */
  /* See --surface-raised in the dark :root above for why this exists. #F1F3F6
     is the handoff's own neutral surface, measured off her artboards. */
  --surface-raised:#F1F3F6;
  /* Her in-app field fill, measured off her CheckinModal. LIGHT ONLY — see the
     --field-fill note in the dark :root above for what happened when this
     lived there instead. */
  --field-fill:#F5F7F9;
  --sidebar-bg:#16233F;
  --sidebar-inactive:#D7DEEB;
  --sidebar-active:rgba(255,255,255,.1);
  --sidebar-active-indicator:#FFFFFF;
  /* The rail's MUTED tone — group labels ("CHILDREN"/"HOUSEHOLD") and the
     identity footer's role line. Measured #8695B3 off the handoff. It had no
     token, so both were falling back to --haze, which is the LIGHT PAGE's
     secondary ink (#5B6472) and is nearly invisible on the navy rail; the
     group labels were instead rendering in --sidebar-inactive (#D7DEEB), the
     same tone as the nav entries themselves, so a static divider competed with
     the links it was supposed to separate. */
  --sidebar-muted:#8695B3;

  --radius:22px;
  --radius-pill:999px;
  --radius-button:999px;
  --radius-input:999px;
  --radius-card:22px;
  --radius-card-large:32px;
  --radius-control:14px;
  --radius-icon:14px;
  --shadow:0 1px 2px rgba(22,35,63,.03), 0 6px 18px rgba(22,35,63,.04);
  --shadow-card:var(--shadow);
  --ui:'Nunito', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-body:'Nunito', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-heading:'Quicksand', 'Nunito', system-ui, sans-serif;
}
:root:not([data-theme]) .wm b { color: #087EA4; }
:root:not([data-theme]) .pill.on { color: #0A6242; }
:root:not([data-theme]) .pill.off { color: #B23127; }
:root:not([data-theme]) .devlockbtn.lock { color: #8E2922; }
:root:not([data-theme]) .devlockbtn.unlock { color: #0A6242; }
:root:not([data-theme]) .devlockstate.failed { color: #B23127; }
:root:not([data-theme]) .banner.err { background: rgba(204,59,49,.10); border-color: rgba(204,59,49,.5); color: #B23127; }
:root:not([data-theme]) .banner.warn { background: rgba(242,169,59,.15); border-color: rgba(242,169,59,.55); color: #7A4F00; }
:root:not([data-theme]) .banner.ask { background: rgba(44,107,209,.10); border-color: rgba(44,107,209,.45); color: #1F4F9C; }
:root:not([data-theme]) .banner.info { background: rgba(44,107,209,.10); border-color: rgba(44,107,209,.45); color: #1F4F9C; }
/* .card's own box-shadow rule now reads var(--shadow) directly (see the
   .card elevation comment below), so this no longer needs its own
   box-shadow override — the token already carries the lighter, blue-tinted
   ambient shadow the reference mockup uses on white. */
/* Bare var(--coral) as TEXT (not the pill.off/devlockbtn.lock spots above,
   which already carry their own darker literal overrides): the mockup's
   canonical light coral (#D4483D) measures 4.38:1 on white, just under the
   4.5 an 11-14px label needs. #B23127 is not a new colour — .pill.off and
   .banner.err two lines up already use it for the identical "coral text on
   a light page" problem — and it measures 6.25:1 on white. Reused rather
   than re-derived. */
:root:not([data-theme]) .ghost.danger { color: #B23127; }
:root:not([data-theme]) .iconbtn.danger { color: #B23127; }
:root:not([data-theme]) .ss.off { color: #B23127; }

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  background:
    radial-gradient(circle at 8% -10%, rgba(101,216,255,.12), transparent 34rem),
    radial-gradient(circle at 100% 24%, rgba(136,167,255,.10), transparent 30rem),
    var(--bg);
  color: var(--cloud);
  font-family: var(--font-body);
  /* 15px/1.45 BELOW 900px, her 16px/1.5 AT AND ABOVE IT — see the media query
     right after this rule.
     Her body copy computes to Nunito 16px on a 24px line box on every
     artboard, and the old 15px/1.45 (rendering 21.75px) is where the
     "everything is slightly small and slightly tight" reading came from; it
     compounds, because most of this file's type is sized in em/rem off this
     base. But every one of her artboards is 1512px wide, so 16px is her
     DESKTOP value and she never drew a phone. Pushing it to every width
     crushed two real layouts — child names on a profile row at 390px, and the
     Kids screen at iPhone SE sizes — both caught by existing tests.
     OWNER DECISION 2026-09-20: hold her scale where she drew it and keep
     today's tighter scale narrow. Same rule already applied to her 34px
     buttons and 41px nav rows. */
  font-size: 15px;
  line-height: 1.45;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  padding: max(18px, env(safe-area-inset-top)) max(14px, env(safe-area-inset-right))
           max(48px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
  -webkit-text-size-adjust: 100%;
}

/* THE LIGHT PAGE IS FLAT WHITE, NOT WHITE-WITH-THE-DARK-GLOW.
   body's two radial-gradients are the dark theme's backdrop — a cyan and a
   periwinkle bloom sized in rem, painted at 12% and 10% alpha. They were never
   turned off for light, so the light dashboard was rendering a cyan wash over
   the top-left of every screen and a blue-violet one over the right. Measured
   on the live build via getComputedStyle: backgroundImage was still
   `radial-gradient(circle at 8% -10%, rgba(101,216,255,.12), ...)` with
   data-theme="light" active.
   The handoff is explicit that the page behind the cards is plain #FFFFFF and
   "not a tinted panel" (PAGE_SPEC, Visual Direction), so this is both the
   designer's intent and the removal of a straightforward bug. */
@media (min-width: 900px) {
  /* HER SCALE, AT THE WIDTH SHE DREW IT. See body's own comment above. */
  body { font-size: 16px; line-height: 1.5; }
}

:root[data-theme="light"] body {
  background: var(--bg);
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
}

.wrap { width: 100%; max-width: 480px; }

/* Tablets keep the phone navigation shell until the 900px rail breakpoint,
   but they should not inherit the phone's 480px reading column. This bounded
   canvas uses the available portrait width without turning the header/menu
   into the desktop sidebar early. */
@media (min-width: 700px) and (max-width: 899.98px) {
  .wrap { max-width: 820px; }
}

/* THE PHONE COLUMN IS UNCHANGED. Everything above is written for 440px and
   e2e/mobile.spec.cjs measures it at 390x844 and 360x800; none of that moves.
   What this adds is the case the design never had an answer for: a parent on a
   laptop, where the same column left roughly 840px of empty page on either side
   and pushed "Add a tablet" — the thing they came to do — about 1500px down.
   Two columns of cards, flowed by the browser, so a tall card and a short one
   pack instead of leaving a hole. `break-inside: avoid` is what stops a card
   being split across the gap; without it a card's header lands at the bottom of
   the left column and its buttons at the top of the right.

   KEYED ON `.screen`, NOT ON #devices/#policy/#inv, AND THAT IS THE POINT.
   Wave 0 turns three screens into twelve, and every later track adds content to
   one of the nine it did not build. Written as an id list this rule would have
   to be edited by a track that owns no CSS file, so the six new sections would
   silently render as one narrow column on a laptop while the three old ones
   stayed two. A class the section already wears cannot be forgotten. */
@media (min-width: 900px) {
  .screen > .card { break-inside: avoid; }
  /* REDESIGN 2026-08-29 — GRID, NOT `columns: 2`. CSS multi-column is a
     NEWSPAPER flow: it pours one continuous stream top-to-bottom down column
     one and then continues in column two, so card order reads down-then-over
     and card heights are decided by the flow rather than by the content. That
     is why this dashboard read as a jumbled pile next to the reference design,
     and it is also the root cause the `#overviewMount` comment below describes
     (a card's buttons orphaning into the next column). A real two-track grid
     places each card as a unit, in source order, left-to-right. */
  .screen { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-content: start; }
  /* The heading row, the sign-out note, the first-run card and a screen's own
     message line span both columns: they are about the page, not about one card
     in it. `#startCard` is still named because it is a card and would otherwise
     be caught by the rule above it. `.subnav` (GoodQA 13d59ad9's Apps/Websites/
     Categories row on those three screens) joins them for the same reason: it
     is not a card and has no break-inside protection of its own, so without
     this it could column-split into two half-rows of chips. */
  .screen > .toprow, .screen > .fineprint, .screen > #startCard,
  .screen > .muted, .screen > .subnav { grid-column: 1 / -1; }
  /* Grid `gap` owns the spacing now; a per-card margin would double it. */
  .screen > .card { margin-top: 0; margin-bottom: 0; }
  /* GOODQA 13d59ad9 — Home's per-child cards are `.screen > #overviewMount >
     .card`, one level deeper than every rule above reaches (`.screen > .card`
     is a DIRECT-CHILD selector and #overviewMount is a wave-0 mount div, not
     the section itself). MEASURED with a real Playwright screenshot: without
     this, a card's name+chip+limit line landed at the bottom of column one
     and its own Pause now/Bedtime buttons re-appeared, orphaned, at the top
     of column two — the exact class of bug `#profList .arow`'s own comment
     three sections up in this file describes ("nothing failed; the page just
     became unreadable... which is why the screenshot is part of the check").
     Scoped to #overviewMount specifically, same reasoning as that comment's
     own id-scoping: only this screen's cards are one level deeper. */
  /* #overviewMount is itself the grid now (it is a wave-0 mount div one level
     below `.screen`, so `.screen`'s own grid cannot place its children). The
     orphaned-buttons bug the paragraph above describes was a multi-column
     artefact and cannot occur in a grid, but the two-track layout it wanted is
     kept — that part was right. */
  /* SPANS THE PARENT GRID. #overviewMount is a child of `.screen`, which is
     itself a two-track grid — without this it would occupy ONE track and then
     split that half again for its own two columns, rendering quarter-width
     cards. Measured in a real screenshot before this line existed. */
  /* HER THREE BANDS (W4.1). A full-width head, a two-track middle with the
     parent's DECISIONS on the left and "Today at a glance" on the right, then
     full-width detail below.
     THE DEFECT THIS FIXES, measured on the live page: "Waiting for you"
     (.ovalerts) rendered LAST, with its top edge at y=1309 on a 1772px page --
     so the banner at y=239 told a parent three requests were waiting and the
     card holding them was ~1300px further down. Her Home puts pending requests
     in the upper-left, beside the children.
     1.3fr / 1fr, not 1fr / 1fr: her decision column is the wider one.
     `order` rather than explicit grid-row placement, because these children are
     CONDITIONAL -- .ovstrip only exists with pending requests, .ovnudge only
     when a child has no device -- so any row arithmetic would be wrong for
     most households. `dense` is what lets .ovchildren backfill the right-hand
     track beside the shorter left-hand items instead of starting a new row.
     Scoped to >=900px on purpose: below it the single column stacks in DOM
     order and none of this applies (V0.1 -- the narrow collapse is unverified,
     so it is deliberately left exactly as it was). */
  #overviewMount {
    grid-column: 1 / -1; display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    /* `dense` IS required, and the earlier chaos was not its fault.
       Sparse flow cannot place a column-1 item beside a column-2 item that was
       placed before it: once the cursor has passed column 2 on that row it
       only moves forward, so .ovquick fell BELOW .ovchildren (measured,
       y=1071 against y=421) instead of beside it.
       Dense placed the bands wrongly on the first attempt only because
       .ai-filter-prompt-host had no `order` and tied with .ovhead at 0. With
       every child explicitly ordered below, dense is deterministic. */
    grid-auto-flow: row dense;
    /* min-content rows, so the 45px quick-action row is 45px tall rather than
       half of .ovchildren's height. Without it the spanned right-hand column
       distributed its 633px across the two rows it spans and left a ~130px
       dead gap under the action row. */
    grid-auto-rows: min-content;
    gap: 18px; align-content: start;
  }
  #overviewMount > .card { margin-top: 0; margin-bottom: 0; }

  /* EVERY child gets an explicit order, and anything unlisted defaults to the
     BACK of the middle band rather than the front. An empty mount div
     (.ai-filter-prompt-host) has no order of its own, so with the default 0 it
     tied with .ovhead, was placed first, and grid's default `stretch` blew it
     up to the 633px height of the row beside it -- adding 300px to the page.
     Measured, not theorised: it went 0px tall to 633px tall.
     `align-self: start` on it as well, so an empty mount can never take height
     from a neighbour again. */
  #overviewMount > * { order: 50; }
  #overviewMount > .ai-filter-prompt-host { order: 10; grid-column: 1 / -1; align-self: start; }

  /* Band 1: full width. */
  #overviewMount > .ovhead    { order: 1; grid-column: 1 / -1; }
  #overviewMount > .ovstrip   { order: 2; grid-column: 1 / -1; }
  #overviewMount > .ovnudge   { order: 3; grid-column: 1 / -1; }

  /* Band 2: the two-track middle. NO `dense`.
     Dense backfills, and with conditional children it shuffled the bands
     out of order (measured: .ovstrip landed at y=871, below the quick row).
     Plain sparse flow is deterministic here PROVIDED the right-hand item is
     placed FIRST: .ovchildren takes column 2 at the band's first row, the
     cursor stays on that row, and .ovquick then fits beside it in column 1
     with .ovalerts falling to the row below. That is exactly her layout --
     decisions stacked on the left, "Today at a glance" on the right. */
  /* .ovchildren SPANS BOTH ROWS of the band so the two left-hand items stack
     beside it, which is her layout: quick actions above pending requests on
     the left, "Today at a glance" running the full height on the right.
     Without the span, .ovalerts was pushed to a THIRD row below the band
     (measured y=1071 against the band's y=421).
     `align-self: start` on .ovquick because grid's default `stretch` blew the
     45px action row up to the band's 633px height. */
  #overviewMount > .ovchildren { order: 20; grid-column: 2; grid-row: span 3; }
  #overviewMount > .ovquick    { order: 21; grid-column: 1; align-self: start; }
  /* THE STAT TILES RIDE IN THE LEFT TRACK, ABOVE the pending list.
     They are OUR functionality -- she draws no tiles -- and CLAUDE.md pins
     what they are for: "the Overview answers five questions in five seconds:
     protected? online? time left? anything waiting? next scheduled change?"
     reading-load-ui.test.js enforces that as a FOLD rule at 1440x900.
     Left in the full-width detail band below, the three-band restructure
     pushed them from y=798 to y=1071 -- below the fold -- and the test caught
     it: "2 of 2 headline numbers are below the fold". A real regression
     against a pinned product rule, not a layout preference.
     In the 667px left track the three tiles are ~210px each, comfortably
     above their own minmax(180px, 1fr) floor. .ovchildren spans three rows
     now rather than two, to cover quick-actions + tiles + pending. */
  #overviewMount > .ovtiles    { order: 22; grid-column: 1; align-self: start; }
  #overviewMount > .ovalerts   { order: 23; grid-column: 1; align-self: start; }

  /* Band 3: full-width detail. */
  #overviewMount > .adv       { order: 31; grid-column: 1 / -1; }
  #overviewMount > .ovweekly  { order: 32; grid-column: 1 / -1; }
  #overviewMount > .help, #overviewMount > .muted { order: 33; grid-column: 1 / -1; }

  /* THE SIDEBAR ITSELF — GoodQA 13d59ad9's "not pills across the top". Same
     breakpoint the two-column card rule above already uses, and the same
     story: below 900px there is no room for a persistent rail without eating
     the one column of content there is, so the nav below that width stays
     exactly the wrapping row #navRow's own comment describes, unmeasured by
     anything here.

     GRID, WITH grid-template-AREAS, RATHER THAN REORDERED MARKUP. `.pagehead`,
     `.nav` and `#main` are siblings in that document order — the nav's own
     skip link has to precede `#main` — and named areas place them into a
     header row plus a nav/main row below it without moving a single element.
     The alternative, flexbox `order`, changes paint order but not DOM order,
     so Tab and a screen reader would still reach the sidebar after the whole
     of `#main` instead of before it. */
  /* REDESIGN 2026-08-29 — a REAL application sidebar, not a 232px strip of
     pills floating inside a 1040px reading column. The reference build
     (docs/design + the Hands-On demo) puts a full-height panel flush against
     the viewport edge with the content beside it, which is what makes this
     read as an app rather than as a document. Three things change together
     and all three are load-bearing:
       1. `.wrap`'s max-width is released at this breakpoint (the 1040px cap
          below 900px is untouched — a phone still gets the reading column).
       2. The nav column becomes a panel: its own background, its own full
          -height scroll, flush left (negative margin cancels .wrap's padding).
       3. `columns: 2` goes away — see the `.screen` rule further down. */
  .wrap {
    display: grid;
    grid-template-columns: 230px 1fr;
    grid-template-areas: "head head" "nav main";
    column-gap: 0;
    /* `start` for the grid, and `stretch` on the RAIL ALONE (below).
       I shipped `align-items: stretch` here to give the rail full height, and
       it stretched the HEAD ROW with it: measured pre-hydration, `.pagehead`
       became 433px tall while its only two children are 48px and 44px. That
       is the giant empty white card in the owner's screenshot of the boot
       flash — my own regression, live in 590ab374.
       The rail gets its full height from `align-self` on itself, which is
       what should have been used in the first place. */
    align-items: start;
    max-width: none;
    width: 100%;
    padding-left: 0;
    padding-right: 0;
  }
  /* The rail carries the brand AND both chrome controls at this width, so the
     page header has nothing left to show and is hidden outright (see below).
     Below 900px there is no rail and this header remains the only place the
     brand, the theme toggle and the language select appear. */
  .pagehead { grid-area: head; margin-inline: 22px; }
  /* THE PAGE-LEVEL BAR IS GONE WHEREVER A RAIL CAN CARRY ITS CONTENTS.
     ---------------------------------------------------------------------
     It already dropped the wordmark at this width (the rail carries the
     brand), which left a full-width 48px+18px bar whose entire remaining job
     was to hold a theme toggle and a language select — two controls a parent
     touches once, charged against the top of every screen on every visit.
     renderNavChrome() now moves the language select into the rail's footer
     beside the Appearance control that was already there, so at this width
     there is nothing left in here to show.

     ONLY when a rail is actually on screen to carry them instead. The auth
     screens (sign-in, forgot, reset) hold `.nav.hide` — they have no rail and
     no footer, so this must not reach them: hiding the header there would
     leave the FIRST page a parent ever sees with no branding, no theme control
     and no language control at all. That regression was caught once already,
     in a real screenshot of the deployed sign-in page rather than by a test,
     which is why the predicate is kept rather than simplified. Below 900px
     there is no rail either (app.css hides `.navbrand`/`.navfoot` outright),
     so a phone keeps this header exactly as it was — brand, theme and
     language — and app.js's placeChromeControls() puts the select back. */
  .wrap:not(:has(.nav.hide)) .pagehead { display: none; }
  /* The greeting block states the screen; the generic "Overview" title above it
     would be the same word twice. Other screens keep their .toprow. */
  #overview > .toprow { display: none; }
  .nav {
    /* ONE SCROLL CONTEXT, AND THE RAIL IS PART OF THE PAGE.
       OWNER, using the deployed build: "i can awkwardly scroll the whole
       screen and the sidebar and the content .. it feels super wack".
       He is describing exactly this rule. `position: sticky` + `height: 100vh`
       + `overflow-y: auto` made the rail its OWN scroll container while the
       document scrolled behind it -- two scrollbars, two scroll positions, and
       a rail that detached from the content beside it.
       Measured against her Home artboard: her aside is `position: static`,
       x=0, 230x1030, and 1030 is EXACTLY her document height. The rail is a
       column of the page, stretched by the grid row, scrolling with
       everything else. `align-items` on `.wrap` is changed from `start` to
       `stretch` for the same reason -- `start` is what capped ours at the
       982px viewport against a taller page and left it visibly floating. */
    grid-area: nav; margin: 0;
    position: static;
    /* THE RAIL stretches, not the grid. See `.wrap`'s align-items note. */
    align-self: stretch;
    min-height: 100vh;
    /* GOODQA #467 — was var(--dusk), the same token `.card` paints itself
       with, so rail and page read as one surface (evidence: dark #101B2D
       rail on #09101D page, light #FFFFFF rail on #F2F6FB page, 1px --line
       the only edge). --railbg is a new, dedicated token: dark points at
       --dusk (today's value, unchanged pixel-for-pixel); light is the
       handoff's own navy, so only light actually repaints. */
    background: var(--railbg); border-right: 0;
    /* The rail no longer scrolls independently (see above), so these two are
       inert at this width. Kept because the SAME rule serves the narrow
       mobile-open drawer, which does still scroll. */
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--railtext) 42%, transparent) transparent;
    /* portal-revamp: trimmed from 18px 14px / gap 4px to recover vertical
       rhythm for the two new household destinations (Apps, Websites) added
       to this rail — nav-reference-parity.test.js's own footer-on-screen
       assertion measured Sign out pushed below the fold once those two rows
       landed. This and the .navlabel/.navbrand/.navfoot trims below are
       chrome spacing only; no row's own min-height (the 44px touch target)
       is touched.

       TRIMMED AGAIN, 10px 14px -> 6px 14px (redesign area "checkins",
       docs/OWNER-DECISIONS.md 2026-09-17): shipping the `engage` row to the
       nav adds a twelfth destination and the same assertion measured Sign
       out at 994 against a 950px viewport before this and the three trims
       below it. Same rule: chrome spacing only, no row's min-height.

       OWNER'S OWN LIVE FEEDBACK, 2026-09-18, on a screenshot side-by-side
       against docs/design/ui-prototype-01: "the feel of nav bar needs to be
       clean like the mockup." MEASURED the actual cause before touching
       anything: every row rendered with ZERO visual gap to the next
       (getBoundingClientRect() showed each .navbtn exactly 44px below the
       last, back-to-back) — `gap: 0` really did mean zero, not "the 10px
       padding still reads as separation" the older comment below assumed.
       TRIED a real gap and measured the actual remaining budget rather than
       guessing: even 2px pushed Sign out to bottom 951 against a 950px
       viewport in nav-reference-parity.test.js's own default (single-child)
       fixture — this rail's footer-reachability budget is spent down to
       the literal pixel, three trims deep already (see above), and adding
       vertical space anywhere in it is not safely available. Fixed the
       "cramped, back-to-back" feel with a ZERO-HEIGHT-COST hairline
       between rows instead (`#navRow .navbtn`'s own `box-shadow`, formerly
       below — see its own comment for what replaced it and why) —
       real visual separation that costs no pixels, so the reachability
       budget this comment protects stays exactly as spent as it was.

       REVERSED, 2026-09-19 — OWNER'S OWN LIVE FEEDBACK AGAIN, this time on
       the real signed-in portal rather than a static screenshot: "the mockup
       doesn't have the lines wrapping around each menu item and its menu
       sidebar is much cleaner." The hairline above was the "line wrapping
       around each item" he was pointing at: at `gap: 0` it sits flush
       against the padding of the row below it, so every boundary between
       two rows reads as a rule drawn across the rail, not as breathing room.
       `design_handoff_parentproof/pages/Home.dc.html` (still the binding
       handoff) draws no border or box-shadow on any top-level row at all —
       gone, not lightened; see `#navRow .navbtn`'s own comment.

       PADDING WIDENED IN THE SAME PASS (6px 14px -> 24px 16px), independently
       reached via the same handoff file during the follow-up comprehensive
       design pass: with the hairline gone, `gap: 0` needed the rail's own
       outer padding to carry more of the "not cramped" job the hairline used
       to. Re-measured against nav-reference-parity.test.js's footer-
       reachability budget with the new padding in place — still passes. */
    padding: 24px 16px;
    display: flex; flex-direction: column; gap: 0;
  }
  .nav::-webkit-scrollbar { width: 8px; }
  .nav::-webkit-scrollbar-track { background: transparent; }
  .nav::-webkit-scrollbar-thumb {
    border: 2px solid transparent;
    border-radius: 999px;
    background: color-mix(in srgb, var(--railtext) 42%, transparent);
    background-clip: content-box;
  }
  /* THE RAIL REACHES THE VIEWPORT EDGE, so body's inset moves onto #main.
     body carries `padding: 18px 14px 48px` for the phone layout, and at this
     width that padding was insetting the RAIL too -- measured live at x=14
     with an 18px gap above it, which is most of why the owner read it as
     "weirdly disconnected". Hers sits at x=0,y=0. The same inset is added back
     to #main here so the CONTENT keeps its breathing room.

     `padding-bottom` JOINS THE ZEROED SIDES HERE, 2026-09-20 — OWNER, on the
     live build: "you can still scroll too far to a white broken chunk".
     MEASURED (elementFromPoint at the bottom of the scroll, 1440x950): the
     rail's own navy (`--railbg`) stops and `body`'s plain white shows through,
     edge to edge, for the last ~66px of a page taller than one viewport.
     Two contributors, both now fixed:
       1. `.nav`'s own `margin: 0 0 18px` (below, the wrapping-chips phone nav's
          rule, unscoped) was winning the cascade over THIS block's `margin: 0`
          on the exact same `.nav` selector, at EVERY width — same specificity,
          later in the file always wins regardless of which media query reads
          as "more specific". See that rule's own comment for the fix.
       2. THIS: `body`'s base `padding-bottom: max(48px, safe-area-inset-bottom)`
          is a PHONE value (gesture-bar / home-indicator clearance) and was
          never zeroed for the rail layout the way left/top already are above.
          It sits OUTSIDE the `.wrap` grid entirely, so no amount of stretching
          `.nav` inside that grid can ever paint over it — the 48px was
          structurally unreachable by the rail, on every desktop screen, all
          the time, not just when content happens to be short.
     Kept as `env(safe-area-inset-bottom, 0px)` rather than a flat 0: a rail
     layout can still run on a tablet with a physical home-indicator gesture
     area (an iPad Pro in standalone/PWA mode), and that protection costs
     nothing when the inset is 0, which it is in an ordinary desktop browser. */
  body.signed-in { padding-left: 0; padding-top: 0; padding-bottom: env(safe-area-inset-bottom, 0px); }
  #main { grid-area: main; min-width: 0; padding: 18px 28px 60px; max-width: 1320px; }
  /* The row becomes the rail: the same five-plus-per-child chips, stacked
     instead of wrapped, each filling the sidebar's width so the current-
     screen fill (`.navbtn[aria-current="page"]`, above) reads edge to edge
     instead of as a pill floating in a wide empty row. */
  /* Stays gap: 0 — see .nav's own comment above for why a real gap is not
     affordable here. No line-based substitute either, as of 2026-09-19 —
     same comment, same reasoning. */
  /* gap 4px, NOT 0. OWNER, using the deployed build: the rail "feels a bit
     crowded compared to hers - its not just number of items, but spacing".
     Measured: `gap: 0` put our 44-47px rows flush against each other, while
     her 8-item household group is 369px tall for 41px rows -- roughly 5px of
     air between each. Fixing the ITEM COUNT (DF-5) would not have touched
     this, which is exactly the distinction he drew. */
  #navRow { flex-direction: column; align-items: stretch; gap: 4px; }
  /* In the rail a nav entry is a full-width ROW, not a pill: no border, square
     -ish corners, left-aligned, so the current-screen fill reads edge to edge.
     Below 900px none of this applies and the wrapping row of chips is
     unchanged — e2e/mobile.spec.cjs measures that at 390x844 and 360x800. */
  #navRow .navbtn {
    /* EXPLICITLY FLEX. `justify-content` was already here and did nothing: a
       <button> is not a flex container by default, so the row was relying on
       inline layout and the icon (a block-level <svg>) stacked ABOVE the label,
       making 44px rows 58px tall. `order: -1` below needs a flex parent too. */
    display: flex; align-items: center;
    text-align: left; justify-content: flex-start;
    border-color: transparent; background: transparent;
    /* 44px, NOT 42. `.chip` already establishes min-height: 44px and this rule
       used to shrink it back to 42 for the rail alone, putting all eleven
       destinations plus the three footer controls under the 44px target
       minimum on the one layout where they are the primary navigation. Two
       pixels bought nothing: `gap: 2px` and the 10px padding set the rhythm. */
    /* 9px and 14px are hers, measured off the handoff's own rail (a 198x41
       row at Nunito 14px/600, active 700). This was 10px/13px — close enough
       to look deliberate and wrong enough to read as a different product next
       to her artboards. min-height stays 44px, NOT her 41: see the note above,
       these are the primary navigation on the one layout where they are the
       only navigation, and 41 would put every destination under the target
       minimum to buy three pixels. */
    border-radius: 9px; padding: 10px 12px; min-height: 44px;
    font-size: 14px; font-weight: 600;
    /* NO PER-ROW HAIRLINE. This used to be `box-shadow: inset 0 -1px 0
       var(--line)` — a "zero-height-cost" seam meant to read as breathing
       room between rows without spending the vertical budget a real `gap`
       costs (see `.nav`'s own comment above for that budget). MEASURED
       against the reference: `design_handoff_parentproof/pages/Home.dc.html`
       (the 2026-09-16 handoff, BINDING) draws every top-level row with NO
       border and NO box-shadow at all — rows are separated only by a real
       2px flex `gap` and, once, by a single explicit divider `<div>` between
       the Activity and Settings groups. At `gap: 0` that seam sat flush
       against the padding of the row below it, on every single row, which
       is what actually painted "a line under every item" — Eric's own
       2026-09-19 report, on the live dashboard, that the sidebar looks
       "boxier" than the mockup and has "lines wrapping around each menu
       item". Removed rather than lightened: the mockup has none, and a
       fainter version is still a line the reference doesn't draw.

       `box-shadow: none` stated explicitly rather than left as an omission —
       independently reached the same way during the follow-up comprehensive
       design pass ("the binding rail is a clean list, not a stack of boxed
       cells"), so a later rule inheriting or re-adding one has to argue with
       a declaration instead of a gap. */
    box-shadow: none;
  }
  /* THE RAIL SHOWS THE REAL ICON, THE ONE ALREADY IN THE BUTTON.
     
     This rule used to be `content: attr(data-nav-icon)` in
     "Segoe UI Symbol", "Noto Sans Symbols 2", sans-serif — one character per
     destination out of ⌂ ☺ ✉ ♡ ▭ ▥ ≡ ＋ ⚙︎ ◉ ?. app.js's own
     PHONE_NAV_ICON_PATHS comment states the objection to exactly that: "a
     one-character font glyph is not a dependable app icon: its shape changes
     with the platform font". It wrote real inline SVGs for every destination
     and the PHONE dock got them; the rail kept painting the glyph over the top
     and set the SVG to `display: none`. MEASURED at 1440x950 before this: all
     eleven rail entries carried a `.navdockicon` <svg> in the DOM with
     computed `display: none`, and a `::before` box whose content was the
     glyph. So the real icons shipped, were never shown, and three of the
     eleven destinations (Overview/Account/Help) rendered as ⌂ ◉ ? — a house,
     a filled circle and a question mark that mean nothing together, and that
     Windows and Linux draw differently again.
     
     The SVGs are decorative (`aria-hidden`, `focusable="false"`, painted with
     `stroke: currentColor`), so the button's accessible name is still exactly
     its translated label and every route assertion reads the same text. */
  #navRow .navbtn > .navdockicon {
    display: block;
    /* `order: -1` because paintNavLabel() appends the icon AFTER the label
       span, which is right for the phone dock (icon above, label below, column
       flow) and backwards for a rail row. Ordering in CSS keeps ONE builder
       for both layouts — a second DOM shape per breakpoint is how the two
       drift. */
    order: -1;
    width: 18px;
    height: 18px;
    margin-right: 8px;
    color: var(--haze);
    flex: none;
  }
  #navRow .navbtn[aria-current="page"] > .navdockicon { color: currentColor; }
  #navRow .navbtn:hover { background: var(--dusk2); }
  /* GOODQA #467 — the rail's own text/icon colours (`.navlabel` above,
     `#navRow .navbtn`'s inherited body colour, `.navdockicon`,
     `.navbrand .nb-word`, the footer email below) all read fine against
     --dusk (a card-toned surface in both themes), which is the whole
     problem this ticket names. Once light's --railbg is navy (above), the
     SAME dark-ink text (var(--cloud), var(--haze)) that read fine on white
     is dark-on-near-black. Scoped to :root[data-theme="light"] .nav only —
     dark is untouched (its rail is still --dusk, so its existing text
     colours still apply). --railtext is #D7DEEB, 11.54:1 on --railbg
     (computed) — not marginal, this is the one adoption in this pass that
     needed no darkening. .navbrand .nb-word stays #FFFFFF: the brand word
     is the one line in the rail the prototype keeps at full white rather
     than the muted slate, and 15.6:1 clears the same floor with more room
     still. The current-page fill is defined in the specificity repair near
     the end of this file: the handoff's translucent white on navy, measured
     in Chromium and covered by the selected-child hover test. */
  /* :not([aria-current="page"]) on purpose — the active row keeps its own,
     already-verified white-on-translucent-navy fix (the later
     `.navbtn[aria-current="page"]` light override); this ID-scoped selector
     would otherwise outrank it and repaint the current screen's label back
     to slate. */
  :root[data-theme="light"] .nav .navlabel,
  :root[data-theme="light"] #navRow .navbtn:not([aria-current="page"]),
  :root[data-theme="light"] #navRow .navbtn:not([aria-current="page"]) > .navdockicon,
  :root[data-theme="light"] .navfoot .nu-mail {
    color: var(--railtext);
  }
  :root[data-theme="light"] #navRow .navbtn:not([aria-current="page"]):hover,
  :root[data-theme="light"] #navRow .navchild:not([aria-current="page"]):hover,
  :root[data-theme="light"] .navfootbtn:hover,
  :root[data-theme="light"] .navlangbtn:hover { background: rgba(255,255,255,.08); }
  :root[data-theme="light"] .nav .navfoot { border-top-color: rgba(255,255,255,.14); }
  :root[data-theme="light"] .navbrand .nb-sub,
  :root[data-theme="light"] .navfootbtn,
  :root[data-theme="light"] .navtheme .nf-ic,
  :root[data-theme="light"] .navtheme .nf-val,
  :root[data-theme="light"] .navlangbtn { color: var(--railtext); }
  :root[data-theme="light"] .navfootbtn,
  :root[data-theme="light"] .navlangbtn { border-color: rgba(255,255,255,.26); }
  :root[data-theme="light"] .navlangflag {
    box-shadow: 0 0 0 1px rgba(255,255,255,.26);
  }
  /* The brand lockup and the footer only exist in the rail. Padding trimmed
     alongside .nav's own (redesign area "checkins", 2026-09-17) to make room
     for the `engage` row without touching any 44px touch target. */
  .navbrand { display: flex; align-items: center; padding: 0 8px; margin-bottom: 18px; }
  .navbrand .nb-word { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.05; color: var(--cloud); }
  .navbrand .nb-logo { width: 154px; height: auto; display: block; object-fit: contain; }
  :root[data-theme="light"] .navbrand .nb-word { color: #FFFFFF; }
  /* `text-transform` because the word is no longer a hardcoded literal typed in
     capitals: it comes from t('brand.tag') now, in three languages, and
     "Control familiar" must wear the same eyebrow shape "SECURE" did. Same
     treatment `.wm small` already gives the page header's copy of this key. */
  .navbrand .nb-sub { display: block; font-size: 10px; line-height: 1.1; letter-spacing: .08em; color: var(--haze); font-weight: 700; text-transform: uppercase; }
  /* APPEARANCE + LANGUAGE, one cluster at the top of the footer — the reference
     build's own sidebar bottom. One element, not two: renderNavChrome() moves
     the page header's own `#langSel` in here rather than building a second
     select, so there is one language control on the page and no chance of two
     disagreeing about which language is on. */
  .navfootlook { display: grid; grid-template-columns: 44px auto; gap: 6px; align-items: center; }
  .navfootlook .langsel { width: max-content; }
  .navfootlook .navtheme { width: 44px; padding: 9px; justify-content: center; }
  .navfootlook .navtheme .nf-label,
  .navfootlook .navtheme .nf-val { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
  /* padding trimmed 6px 12px 4px -> 3px 12px 2px, same "checkins" rail-room
     pass as .navbrand above. */
  /* `line-height: 1.2` added (redesign area "checkins", 2026-09-17): this
     10px label was inheriting the rail's own ~1.5 body line-height, so each
     one rendered at ~27.5px tall — real height a static section divider
     never needed, and the biggest single line item once `engage` needed
     room. Shrinking it to fit its own text is not a touch target concern:
     this element is a non-interactive divider, never a button. */
  /* HER SPEC, measured: Quicksand 11px/700, .08em tracking, #8695B3, and the
     padding that puts 8px under the label rather than 2px. It was Nunito 10px
     at .12em in --haze — the wrong FAMILY (she reserves Quicksand for labels,
     headings and buttons, Nunito for body and nav entries; this file had the
     two swapped here), a size below her scale, and a colour that made the
     divider compete with the entries beneath it. */
  .navlabel { font-family: var(--font-heading); font-size: 11px; line-height: 1.5; letter-spacing: .08em; text-transform: uppercase; color: var(--sidebar-muted); padding: 0 12px 8px; margin-top: 14px; font-weight: 700; }
  /* No leading margin on the FIRST group label — the rail's own 24px top
     padding already sets that edge, and doubling it pushed the brand away. */
  .navlabel:first-of-type { margin-top: 0; }
  /* HER RULE BETWEEN THE TWO GROUPS. Her rail is Home/Requests/Check-ins/
     Devices/Activity, then a 1px rule, then Settings/Account/Help — two
     groups, not one list of ten. Ours ran all ten together, which is why it
     read as a wall. Scoped to the Settings entry so it lands in the same
     place regardless of how many household destinations precede it. */
  #navRow .navbtn[data-screen="preferences"] {
    margin-top: 14px;
    border-top: 1px solid rgba(255,255,255,.12);
    padding-top: 16px;
    border-radius: 0 0 9px 9px;
  }
  /* THE FOOTER CHROME SHRINKS WHERE A MOUSE IS DRIVING IT. Touch keeps 44px. */
  @media (pointer: fine) {
    .navfoot .navlangbtn { min-width: 28px; min-height: 28px; padding: 3px; border-radius: 8px; font-size: 13px; }
    .navfoot .navlangrow { gap: 5px; }
    .navfoot .navtheme, .navfoot .navthemebtn { min-height: 28px; padding: 3px 8px; }
    .navfoot .navtheme .nf-label { display: none; }
    .navfoot .nf-val { font-size: 11.5px; }
  }
  .navfoot { margin-top: auto; padding-top: 3px; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 3px; }
  .navfoot .navuser { display: flex; align-items: center; gap: 9px; padding: 2px 6px; min-width: 0; }
  .navfoot .navuser .nu-mail { font-size: 11px; color: var(--haze); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* The live dot on a child entry. Rendered ONLY when a device really reported
     in recently — absence means "not known", never a grey "probably offline". */
  .navdot { width: 7px; height: 7px; border-radius: 50%; background: var(--mint); flex: none; margin-left: auto; }
  /* AUTH SCREENS HAVE NO SIDEBAR TO SHOW: `.nav` carries `.hide` on all three
     (route()'s own comment: "nowhere it could take them"), and without this
     the grid still reserves 232px of empty rail beside the sign-in card. */
  .wrap:has(.nav.hide) {
    grid-template-columns: 1fr;
    grid-template-areas: "head" "main";
  }
}
/* RAIL-ONLY CHROME. renderNavChrome() inserts the brand lockup and the footer
   into the DOM at every width, but they are only ever STYLED inside the
   >=900px sidebar block above — so below that breakpoint they were rendering
   as unstyled blocks and adding real height to the top of every screen.
   Measured, not guessed: protection-honesty.test.js walks a 390x844 phone and
   found the "No web filtering is on" banner pushed 18px below the fold
   (bottom 862 vs viewport 844) — the exact off-the-fold failure that test was
   written to prevent. A phone has no rail, so this chrome does not exist there. */
@media (max-width: 899px) {
  .navbrand, .navfoot, .navlabel { display: none; }
}

.hide { display: none !important; }
/* `hidden` is a UA rule at specificity 0, so ANY author rule beats it — and
   `.rowline { display: flex }` is an author rule. #catBudgetEditRow is the one
   element on this page that uses the attribute, and it was rendering its
   on-exhaustion select before any budget had loaded, which is precisely what
   index.html says under it must not happen. Verified: one `hidden=` attribute
   in the whole document. */
[hidden] { display: none !important; }
/* `.screen` carries no shape of its own outside the two-column rule above — the
   sections were unclassed before and looked right. It is declared here so the
   token exists in the design vocabulary (style-vocabulary.test.js) and so the
   media query has something to key on. */
.screen { }

/* ---------------------------------------------------------------- brand mark */

.brand { display: flex; align-items: center; gap: 13px; margin: 0; }
.ap {
  position: relative; width: 44px; height: 48px; border-radius: 15px 15px 19px 19px; flex: none;
  border: 2px solid var(--glow); background: linear-gradient(145deg, rgba(101,216,255,.18), var(--dusk));
  box-shadow: 0 10px 28px -14px var(--glow), inset 0 1px rgba(255,255,255,.10);
  clip-path: polygon(50% 0, 94% 14%, 90% 68%, 50% 100%, 10% 68%, 6% 14%);
  transition: border-color .4s ease, box-shadow .4s ease;
}
.ap::after {
  content: ""; position: absolute; inset: 31%; border-radius: 50%;
  background: radial-gradient(circle at 42% 35%, #EFFFFF, var(--glow) 66%);
  transition: inset .4s cubic-bezier(.4,0,.2,1), background .4s ease;
}
.ap.off { border-color: var(--coral); box-shadow: 0 0 22px -4px var(--coral); }
.ap.off::after { inset: 20%; background: radial-gradient(circle at 50% 50%, rgba(255,107,94,.85), rgba(255,107,94,.28)); }
.ap.wait { border-color: var(--haze); box-shadow: none; }
.ap.wait::after { inset: 34%; background: radial-gradient(circle at 50% 50%, rgba(201,204,214,.5), rgba(201,204,214,.12)); }

/* The header identity mark: the brand bundle's symbol, sized to the box the
 * decorative `.ap` used to occupy so the header's rhythm is unchanged. It is a
 * transparent PNG and is deliberately NOT given a themed background — the same
 * file has to sit on --bg #0E0F13 and on the light --bg, and the artwork was
 * drawn to. `flex: none` because .brand is a flex row and the h1 beside it
 * would otherwise squeeze the image on a narrow phone. */
.brandmark { width: 48px; height: 48px; display: block; flex: none; }

/* The approved lockup is the visible identity. `.wm` remains the single h1,
   but its content is screen-reader-only so browser font metrics can never
   re-open the spacing defect in the artwork. */
.site-brand-logo {
  width: 160px; height: 40px; display: block; flex: none; object-fit: contain;
}

.wm { font-weight: 650; font-size: 24px; letter-spacing: -.65px; margin: 0; }
.wm b { color: var(--glow); font-weight: 800; }
:root[data-theme="light"] .wm b { color: #087EA4; } /* AA on the cloud page */
.wm small {
  display: block; font-size: 9px; font-weight: 750; letter-spacing: 3.2px;
  text-transform: uppercase; color: var(--haze); margin-top: -2px;
  /* `brand.tag` is two words now ("Family control", and longer in French), and
     at 3.2px tracking inside a 390px header it wrapped under the wordmark and
     pushed the whole brand block a line taller. It still fits on one line —
     measured at 390px: the eyebrow is narrower than "ParentProof" above it. */
  white-space: nowrap;
}

.pagehead {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  padding: 12px 14px; margin: 0 0 18px; border: 1px solid rgba(174,187,208,.16);
  border-radius: 20px; background: rgba(18,26,44,.72); backdrop-filter: blur(18px);
  box-shadow: 0 14px 44px rgba(0,0,0,.18);
}
:root[data-theme="light"] .pagehead { background: rgba(255,255,255,.82); box-shadow: 0 12px 35px rgba(30,52,86,.08); }
.controls { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin: 0; }
.themebtn {
  width: 54px; height: 30px; border-radius: 18px; position: relative; flex: none;
  border: 1px solid var(--line); background: var(--dusk2); cursor: pointer;
}
.themebtn::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px;
  border-radius: 50%; background: var(--glow); transition: left .2s;
}
.themebtn[aria-checked="false"]::after { left: 26px; }
.themebtn .ic { position: absolute; top: 50%; transform: translateY(-50%); font-size: 12px; line-height: 1; pointer-events: none; }
.themebtn .ic.sun { left: 7px; } .themebtn .ic.moon { right: 7px; }
.langsel {
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 10px;
  color: var(--cloud); font-family: var(--ui); font-size: 14px; padding: 7px 9px;
  cursor: pointer; min-height: 44px;
}

/* -------------------------------------------------------------------- banners */

.banner {
  border-radius: 16px; padding: 14px 16px; margin-bottom: 16px; font-size: 13px;
  display: flex; align-items: flex-start; gap: 9px;
}
.banner b { display: block; }
.banner.err { background: rgba(255,107,94,.13); border: 1px solid rgba(255,107,94,.5); color: #FFB4AC; font-weight: 600; }
.banner.warn { background: rgba(255,194,75,.12); border: 1px solid rgba(255,194,75,.45); color: #FFD98A; }
/* A child asking for more time is a REQUEST, not a fault — it must be
   impossible to miss but must not read as an alarm, so it takes the sky accent
   rather than err-red or warn-amber. */
.banner.ask { background: rgba(108,166,255,.13); border: 1px solid rgba(108,166,255,.45); color: #B7D2FF; }
/* THE PROTECTION STATUS IS A FACT, NOT AN ALARM AND NOT A CONGRATULATION.
   #webBanner is the only banner on this page that is ALWAYS up once a policy has
   loaded, so it cannot take `.err` or `.warn` in its best state: a red or amber
   bar a parent sees on every visit is a bar they stop seeing, and it would then
   be missing from the two states (`off`, `blind`) where it is the whole point.
   It cannot take mint either — everywhere else on this page mint means "on, and
   that is the good state", and the best thing this build can do to the web is
   partial. So the ON state takes the sky accent, which this page already uses
   for "here is something true you did not ask about", and renderWebBanner()
   swaps in `.err` for the two states where nothing is filtering at all. The
   words carry the claim in every case; the colour never carries it alone. */
.banner.info { background: rgba(108,166,255,.13); border: 1px solid rgba(108,166,255,.45); color: #B7D2FF; }
/* THE LIGHT PAGE FAILS THE SAME CHECK IN THE OTHER DIRECTION, and both halves
   were measured with axe, not guessed. --mint on the light page is #0E7C52,
   which over the 18%-mint wash on --dusk2 is 4.08:1 — under the 4.5 an 11px bold
   label needs, by a margin small enough that nobody would ever see it by eye.
   #0A6242 is the same hue two steps darker and measures 5.9:1. Kept off --mint
   itself, which is also a switch fill (.sw.on) where a darker green would read
   as a different state rather than the same one. */
:root[data-theme="light"] .pill.on { color: #0A6242; }
:root[data-theme="light"] .pill.off { color: #B23127; }
:root[data-theme="light"] .banner.err { background: rgba(204,59,49,.10); border-color: rgba(204,59,49,.5); color: #B23127; }
:root[data-theme="light"] .banner.warn { background: rgba(242,169,59,.15); border-color: rgba(242,169,59,.55); color: #7A4F00; }
:root[data-theme="light"] .banner.ask { background: rgba(44,107,209,.10); border-color: rgba(44,107,209,.45); color: #1F4F9C; }
:root[data-theme="light"] .banner.info { background: rgba(44,107,209,.10); border-color: rgba(44,107,209,.45); color: #1F4F9C; }
/* GoodQA #457/#462. `.muted.err`/`.muted.ok` (app.css, near `.muted`) are
   bare text on --bg, not a tinted wash like `.pill.on`/`.pill.off` above —
   so they need the same measured-safe literal those already reuse, not the
   raw --coral/--mint that fails small bold text at 4.04:1 / 4.48:1 here
   (see this file's --danger fallback comment). #B23127/#0A6242 measure
   5.76:1 / 6.82:1 against this theme's --bg (#F2F6FB) and are not new
   colours — reused from .pill.off/.pill.on two lines up. */
:root[data-theme="light"] .muted.err { color: #B23127; }
:root[data-theme="light"] .muted.ok { color: #0A6242; }
.banner .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: currentColor; margin-top: 5px; }
/* The two-line banner: a bold claim, then the paragraph that qualifies it. The
   wrapper is what keeps the dot aligned to the FIRST line rather than to the
   middle of a five-line block, and `min-width: 0` is what stops a long resolver
   hostname from pushing the whole bar wider than the 390px column. */
.bantext { flex: 1; min-width: 0; }
.bantext b { display: block; margin-bottom: 3px; }
/* `.banner.err` sets font-weight 600 on the whole bar, which is right for the
   one-line failures it was written for and wrong here: MEASURED at 390x844, the
   `off` state is a six-line paragraph, and six bold lines read as shouting
   rather than as the calm sentence this status is supposed to be. The claim
   stays bold; the explanation under it goes back to body weight. */
.bantext > span { font-weight: 400; }

/* ---------------------------------------------------------------------- cards */

/* ELEVATED to the reference mockup's own card language: var(--radius) is
 * that file's 20px (was a hardcoded 24px here — one fewer bespoke number in
 * a file that otherwise scatters 24/22/21/16/14/13/10px radii by hand), and
 * var(--shadow) is its actual ambient shadow, themed at the :root level (see
 * the token block at the top of this file) rather than overridden per
 * selector — dark gets that file's `0 24px 70px #0007`, light gets its own
 * blue-tinted `0 24px 70px #29445d20`, both lifted verbatim from its :root
 * rather than invented, so this rule needs no light-theme override of its
 * own any more. */
/* HER CARD, MEASURED: border rgba(22,35,63,.08), radius 22px, and a TWO-LAYER
   soft shadow. The separation in her design comes from the SHADOW, not from
   the border -- which is why ours read as flat white on flat white with the
   owner's .16 edge doing all the work and failing at it.
   Note the split her own design makes and that this now follows: her CARD
   edges are .08 (--border-subtle) and her CONTROL edges are .16 (--line).
   Cards are separated by depth; controls are outlined. Using .16 on both
   flattened the two into one weight.
   --shadow-card already existed and pointed at --shadow (a dark-theme
   `0 24px 70px #0007`); light now gives it her own value. */
.card {
  background: linear-gradient(145deg, rgba(255,255,255,.025), transparent 42%), var(--dusk);
  border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: 22px; margin-bottom: 16px;
  box-shadow: var(--shadow-card);
}

.sect {
  font-size: 12px; font-weight: 750; letter-spacing: .2px;
  color: var(--haze); margin: 0 2px 12px; display: flex; align-items: center; gap: 8px;
}
.sect::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, var(--line), transparent); }

label.fld, .fld { display: block; font-size: 12px; font-weight: 600; color: var(--haze); margin: 0 2px 7px; }
.help { font-size: 12px; color: var(--haze); margin-bottom: 10px; }
/* THE ONE LINE ON THE ASSISTANT SCREEN THAT IS A NAME RATHER THAN A SENTENCE.
   js/assistant.js's blocked card prints the exact deployment setting that is
   missing, and an environment variable's name is a single unbreakable word:
   PP_CLASSIFY_PROMPT_LOGGING_EXPIRES is 34 characters with no space in it, and
   the field's own validator permits 64. At 375px (iPhone SE) that is the width
   of the column, so without this the honest message that names the setting is
   also the message that turns the whole dashboard into a horizontal scroller.
   MEASURED by test/assistant-se-layout.test.js, which sets a 64-character name
   into this line and asserts nothing in the mount is wider than the mount. */
.ask-setting { overflow-wrap: anywhere; }
.muted { color: var(--haze); font-size: 13px; text-align: center; padding: 10px 0; }
.muted:empty { display: none; padding: 0; }
/* GoodQA #457. Auth-card failures (bad password, taken username, expired
   token) used to land in this exact node with the exact same styling as a
   success message — nothing told a parent apart "that worked" from "that
   didn't". `.err` recolors and left-aligns so a failure reads as one, and
   the "!" glyph gives it a non-color signal too (WCAG 1.4.1). var(--danger)
   has no value yet — the 2026-09-18 five-colour token pass is defining it —
   so it falls back to --coral, today's nearest reserved error hue. */
.muted.err {
  color: var(--danger, var(--coral)); text-align: left; font-weight: 600;
}
.muted.err::before { content: "! "; }
/* GoodQA #462. "Password updated" folds into this same status line rather
   than getting the prototype's own routed screen (see showSignin's 'ok'
   variant) — --mint is this design system's existing "done, applied"
   meaning, so a completed reset gets that treatment instead of reading as
   one more grey status update. */
.muted.ok { color: var(--done-text); text-align: left; font-weight: 600; }
.muted.ok::before { content: "✓ "; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.fineprint { font-size: 12px; color: var(--haze); margin-top: 10px; }
/* The same collapse `.muted:empty` already has, and needed for the same reason:
   the switcher fills two fineprint lines from data (the local-pause warning and
   the undo note) and both are empty most of the time. Without this they cost
   10px of margin each on a card that is already the tallest thing above the
   fold on a 390x844 phone. */
.fineprint:empty { display: none; margin: 0; }
/* A general small-text utility, one step below .fineprint (12px). Only ever
   existed as the scoped `.reqpolicy-who .tiny` rule until the requests area's
   redesign rewrote that section away, orphaning desktop-preview.js's own
   bare `.tiny kd-stateslabel` usage -- a real class, not a placeholder (see
   test/desktop-preview-ui.test.js's app.css-parity check). Defined as a real
   base rule here instead of re-scoping it to one caller. */
.tiny { font-size: 11px; }

/* 16px is not a style choice. MEASURED on iOS Safari: an input whose font-size is
 * under 16px makes the browser ZOOM the page on focus, and the parent then has to
 * pinch back out to see the rest of the card. The dashboard sets 15px and has that
 * bug. Everything a thumb types into here is 16px. */
/* L1, owner on the live build: "fix the contrast of form fields".
   MEASURED on production: fill #FFFFFF on a card that is also #FFFFFF = 1.00:1,
   with a 1px rgba(22,35,63,.16) border compositing to about 1.38:1. A parent
   could not see where a field began.
   --field-fill is her own #F5F7F9 in light (measured on her CheckinModal
   textarea) and --dusk2 in dark, where a white-on-white problem does not
   exist. Her auth fields ARE white, but they sit on a white card over a grey
   page and we have no such layering, so the fill does the work here.
   Her 12px radius, too — ours was 14. */
.inp, .limitin, .timein {
  background: var(--field-fill); border: 1px solid var(--line); border-radius: 12px;
  color: var(--cloud); font-family: var(--ui); font-size: 16px; padding: 13px;
  width: 100%; min-height: 48px;
}
.inp::placeholder { color: var(--haze); opacity: 1; }
/* `a:focus-visible` BELONGS HERE AND WAS MISSING. The rule named buttons and
   four input classes, so every LINK on the page fell back to the browser's own
   `outline: 1px auto` — including `#navAddDeviceLink`, which is an <a> wearing
   `.chip .navbtn` and sitting in the rail between ten buttons that all show the
   2px glow ring. One row of the same list answering focus differently from the
   other ten is the kind of thing only a keyboard user ever sees. */
.inp:focus, .limitin:focus, .timein:focus, .langsel:focus,
button:focus-visible, a:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--glow); outline-offset: 1px;
}
select.inp { cursor: pointer; }
/* GoodQA #457. `aria-invalid` (a real ARIA state, not a presentational class)
   is how doSignin/doForgot/doResetPassword now mark the specific field a
   rejected value came from — previously no input ever carried a failure
   state and the message above it was the only clue. var(--danger) falls
   back to --coral until the token-layer pass defines it (see .muted.err). */
.inp[aria-invalid="true"] { border-color: var(--danger, var(--coral)); }

/* A <textarea class="inp"> inherits the fill, border and 16px from the rule
   above, but not a shape: the browser default is a MONOSPACE box sized in
   `rows`, which is why the bug-report card read as a different product from
   the two inputs above it. `resize: vertical` and not `both`, because
   horizontal resize breaks out of a 440px column. */
textarea.inp {
  font-family: var(--ui); line-height: 1.45; min-height: 92px;
  resize: vertical; display: block;
}

/* A code the parent reads off the screen and types somewhere else — the
   co-parent invite, and any short secret we show once. Monospace and spaced so
   0/O and 1/l are distinguishable out loud, and `user-select: all` so one click
   selects the whole thing rather than a word of it. */
.pairout {
  display: inline-block; background: var(--dusk2); border: 1px solid var(--line);
  border-radius: 10px; padding: 8px 12px; margin-right: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 16px; font-weight: 700; letter-spacing: .12em; color: var(--cloud);
  user-select: all; word-break: break-all;
}

/* A CONTROL THAT LOOKS EDITABLE AND IS NOT IS WORSE THAN THE BUG IT FIXES.
   The policy screen disables every writer while the policy is still loading
   (setPolicyControlsEnabled in app.js), because a name typed into that window
   used to be dropped with no request and no message. A disabled control that
   still LOOKS live would move that confusion rather than remove it — the
   parent would press it, get nothing, and be told nothing. These rules are
   what make "not yet" visible.

   .limitin and .timein are here because they set their own background and
   color, which beats the browser's default disabled greying — the first
   version of this block covered only .inp/.ghost, and the committed loading
   screenshot showed the minutes box as the most editable-looking control on
   the page while being dead. If a new input class ever gets its own colors,
   it needs a line here too. */
.inp:disabled { opacity: .5; cursor: default; }
.ghost:disabled { opacity: .5; cursor: default; }
.limitin:disabled { opacity: .5; cursor: default; }
.timein:disabled { opacity: .5; cursor: default; }

/* GOODQA #470 — was 13px; the handoff's dominant button radius is a full
   pill (259 of its own uses). `--radius-pill` is a new token, not a rename:
   `.iconbtn`/`.navbtn`/`.sw`/`.themebtn` etc. all carry their OWN more
   specific border-radius already, so this only reshapes plain buttons and
   `.primary`/`.ghost`, which had none of their own. */
button { font-family: var(--ui); font-weight: 700; cursor: pointer; border: none; border-radius: var(--radius-pill); }
.primary {
  width: 100%; background: linear-gradient(135deg, var(--glow), #8FE7FF); color: var(--ink);
  padding: 14px; font-size: 16px; margin-top: 12px; min-height: 50px;
  box-shadow: 0 10px 24px rgba(101,216,255,.18);
}
/* GOODQA #465/#468 — light --glow moved teal -> brand blue (:root[data-theme
   ="light"] above), and dark ink on the new blue is 3.50:1 (computed): a
   regression from teal's already-marginal 4.08:1. Every other
   background:var(--glow);color:var(--ink) combo in this file already had a
   light override to white text (`.navbtn[aria-current="page"]`,
   `.mobilemenubtn[aria-expanded="true"]`); this one did not, because it
   never needed one before. White on solid #2B6CB0 is 5.42:1; the gradient's
   pale end (#8FE7FF, unchanged) is 13.58:1, so white stays safe across the
   whole button, not just one stop — verified, not assumed, per this file's
   own rule for every colour above. */
:root[data-theme="light"] .primary { color: #FFFFFF; }
.primary:disabled { opacity: .55; cursor: default; }
.ghost {
  background: rgba(255,255,255,.025); border: 1px solid var(--line); color: var(--haze);
  padding: 12px 14px; font-size: 14px; min-height: 44px;
}
.ghost.wide { width: 100%; margin-top: 8px; }
.ghost.danger { color: var(--coral); border-color: rgba(255,107,94,.45); }
/* Bare var(--coral) as light-theme TEXT measures 4.38:1 on white — see the
   matching comment beside the @media(prefers-color-scheme:light) block near
   the top of this file, which explains and reuses this exact #B23127. */
:root[data-theme="light"] .ghost.danger { color: #B23127; }

/* FOUND 2026-09-22, child-overview-apps-web portal lane (KidWeb): the Web
   tab's "Filter unwanted / Only favorites" segmented control (app.js's
   renderFavorites()) writes `fav-mode-on` onto whichever button is the
   child's current browser mode -- its own comment says "the active one
   marked... Blue/primary marks the choice" -- but no rule anywhere in this
   file (or any per-feature stylesheet) ever painted that class. Both options
   in the pair rendered as the same plain `.ghost.wide`, so a parent had no way
   to see which mode a child's browser was actually in: exactly the
   "category/app chip with no legible selected state" defect class, just on a
   two-item radiogroup rather than a chip row. Reuses `.chip[aria-pressed]`'s
   own selected pair (`--glow`/`--dusk`) rather than inventing a second
   selected-state colour for the same meaning. */
.ghost.fav-mode-on { background: var(--glow); color: var(--dusk); border-color: var(--glow); }

/* An <a> — OR A REAL <button>, see below — that has to look and behave like
   plain text with a click target. Anchors do not inherit the button
   element's centring or its box, so without these three properties the
   first-run links render as underlined text wedged against the left edge of
   a full-width border.

   `background: none; border: none;` ADDED 2026-09-21, for the one call site
   that puts this class on a real `<button>` (js/family-controls.js's
   "‹ Back to devices" crumb, ctx.btn('btnlink', ...)) rather than an `<a>`.
   An anchor has no UA background or border to begin with, so this rule went
   unnoticed for every other user of the class; a `<button>` does — Chromium's
   default button face — and nothing here ever cleared it. Invisible-looking
   in light theme (a pale grey box on a near-white page) and a real WCAG
   failure in dark theme: axe-core measured the text's own --haze colour
   against that leftover #efefef button face at 2.25:1, not the card behind
   it, against a 4.5:1 requirement — MEASURED by
   zip-parity-child-cluster.test.js's "the Apps screen fills the width it is
   given" contrast audit, `.child-headtop > .btnlink[type="button"]`, dark
   theme, 1280px. */
.btnlink {
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; box-sizing: border-box;
  background: none; border: none;
}

/* The first-run steps. A plain <ol> indents by a browser-chosen amount and puts
   the marker outside the text column, which at 440px costs about 8% of the
   width; this puts the number in a fixed 26px gutter instead so the three step
   texts share one left edge. */
.startlist { list-style: none; counter-reset: fbstep; margin: 12px 0 4px; padding: 0; }
.startlist li {
  counter-increment: fbstep; position: relative; padding-left: 30px; margin-bottom: 12px;
}
.startlist li::before {
  content: counter(fbstep); position: absolute; left: 0; top: 1px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--glow); color: var(--ink);
  font-size: 13px; font-weight: 800; line-height: 22px; text-align: center;
}
.startlist b { display: block; font-size: 15px; }
.startlist .fineprint { margin: 2px 0 0; }

.rowline { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
.rowline .inp { margin: 0; }
.rowline .ghost { flex: none; }
.limitin { width: 84px; flex: none; text-align: center; }

/* ------------------------------------------------------------------- top rows */

.toprow { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.toprow .who { flex: 1; min-width: 0; }
.toprow .who .nm { font-weight: 700; font-size: 17px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.toprow .who .id { font-size: 11px; color: var(--haze); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ------------------------------------------------------------------- devices */

.devrow {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(112px, 132px);
  align-items: stretch; gap: 8px; margin-bottom: 10px;
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 14px;
  overflow: hidden;
}
.devbtn {
  width: 100%; display: flex; align-items: center; gap: 10px; min-width: 0;
  text-align: left; background: transparent; color: var(--cloud);
  border: 0; border-radius: 13px; padding: 12px 4px 12px 14px; min-height: 76px;
}
.devbtn .devnm { flex: 1; min-width: 0; }
/* maintenance-grant-system (GoodQA 13d59ad9 Frame 6): the Protected pill
   rides beside the name, not inside its own truncated text run — FOUND BY
   HAND from a real screenshot: appending a `.pill` as a sibling of the bare
   name TEXT NODE inside the old single `.devnm` (which carried nowrap/
   ellipsis directly) put the pill INSIDE that one inline formatting
   context, so a long device name's ellipsis silently ate the pill's own
   text too ("Protec…" instead of a name AND a full "Protected" pill). The
   name's own truncation now lives on `.devnmtext` alone; the pill is a
   `flex: none` sibling in the same row and is never touched by it. */
.devnmrow { display: flex; align-items: center; gap: 6px; min-width: 0; }
.devnmtext { font-weight: 600; font-size: 15px; min-width: 0; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.devnmrow .pill { flex: none; }
.devbtn .devsub { font-size: 11px; color: var(--haze); margin-top: 2px; font-weight: 400; }
.devopen { color: var(--haze); font-size: 24px; line-height: 1; flex: none; }
.devlock {
  display: flex; flex-direction: column; justify-content: center; gap: 5px;
  padding: 8px 9px 8px 0; min-width: 0;
}
.devlockbtn {
  width: 100%; min-height: 48px; padding: 9px 8px; border: 1px solid var(--line);
  color: var(--cloud); background: var(--track); font-size: 13px;
}
.devlockbtn.lock { color: #FFDED9; border-color: rgba(255,107,94,.55); background: rgba(255,107,94,.16); }
.devlockbtn.unlock { color: var(--done-text); border-color: rgba(79,209,161,.5); background: rgba(79,209,161,.13); }
.devlockstate {
  min-height: 2.4em; color: var(--haze); font-size: 10.5px; font-weight: 600;
  line-height: 1.2; text-align: center; overflow-wrap: anywhere;
}
.devlockstate.applied { color: var(--cloud); }
.devlockstate.failed { color: #FFB4AC; }
:root[data-theme="light"] .devlockbtn.lock { color: #8E2922; }
:root[data-theme="light"] .devlockbtn.unlock { color: #0A6242; }
:root[data-theme="light"] .devlockstate.failed { color: #B23127; }
/* The "Applied ✓" pill treatment: fully-rounded (20px on an ~19px-tall pill
   is already the reference mockup's pill shape, so var(--radius) doubles as
   its border-radius with no new number needed), tracked slightly wider so a
   short all-caps-weight label like "HEALTHY" or "Applied ✓" reads as a
   badge rather than as squeezed body text, and never wraps mid-word. */
.pill { font-size: 11px; font-weight: 700; padding: 4px 9px; border-radius: var(--radius); flex: none; letter-spacing: .2px; white-space: nowrap; }
/* The waiting-requests badge is TWO elements — see requestsBadge() in app.js.
   `.pill-n` is the count, which survives at every width; `.pill-word` is the
   word, which the phone tab bar hides because a 90px tab has no room for it.
   The margin is what keeps "1 waiting" reading as two words wherever both are
   painted: they are separate elements now, so the space between them has to be
   a style rather than a character in a string. */
.pill .pill-n { font-weight: inherit; }
.pill .pill-word { margin-left: .35em; }
.pill.on { background: rgba(79,209,161,.18); color: var(--done-text); }
/* NOT var(--coral) ON THE DARK PAGE, AND THIS WAS MEASURED RATHER THAN CHOSEN.
   axe-core, 390x844, dark: `.pill` — "Elements must meet minimum color contrast
   ratio thresholds", serious. #FF6B5E on the 18%-coral wash over --dusk2 is
   3.7:1, and 11px bold is not large text, so the threshold is 4.5. The pill was
   already on the device list saying "Resting"; the Sites card is what put an
   axe run over it.
   #FFB4AC is not a new colour — .banner.err has used it for the same reason
   since it was written — and it measures 6.4:1 on the same wash. The LIGHT theme
   keeps a dark ink for the identical reason in the other direction, so the
   colour is overridden per theme rather than pushed into --coral, which is also
   a border and a glow (.ap.off, .ghost.danger) where it already passes. */
.pill.off { background: rgba(255,107,94,.18); color: #FFB4AC; }
/* GOODQA #466/#469, OWNER-DECISIONS.md §0.4, decision #510 (locked) — the
   Background Apps "Unsafe" classification (invSafetySpec() in app.js) reused
   `.pill.off` (coral) before this: coral is DESTRUCTIVE/PAUSED
   (UI-DIRECTION §3), and "this app is unsafe" is a different claim than
   "this is turned off", so it was borrowing a meaning it did not have —
   the exact thing `.pill.neutral`'s own comment two rules down already
   argues against doing. --danger is new for exactly this. Same per-theme-
   literal pattern as `.pill.off` above (not the bare token): #EE5864
   (--danger dark) on its own 18%-wash over --dusk2 is 3.82:1 — computed,
   axe would fail it the same way it failed `.pill.off`'s naive dark
   version — so the pill gets its own lighter literal, #F68E96, 5.67:1 on
   that wash. Light's own wash is closer to safe (4.65:1 on a white card)
   but 4.40:1 on --dusk2, so it gets the same treatment one step down:
   #BB231B, same hue, darkened, 4.92:1 on the tighter background. */
.pill.danger { background: rgba(238,88,100,.18); color: #F68E96; }
:root[data-theme="light"] .pill.danger { background: rgba(192,52,44,.12); color: #BB231B; }
/* GOODQA #466, OWNER-DECISIONS.md §0.4, decision #510 (locked) — --indigo's
   first consumer. No screen wires this yet (grep found no "needs attention"
   call site to migrate — the state this token is FOR does not exist as UI
   yet), so this is the token layer the next ticket that needs it should
   reach for rather than re-deriving the same numbers: same measured,
   per-theme-literal pattern as `.pill.danger`/`.pill.off` above. Dark
   --indigo (#A792FC) on its own 18% wash over --dusk2 is 4.53:1 already
   (unlike danger's, this one clears it without a further literal — kept
   anyway, at #BBAAFD/5.71:1, for the same safety margin its siblings get
   rather than shipping the one hue that is only just over the line). Light
   --indigo (#6D5BD0) on its own tint is 4.31:1 — under 4.5 — so light gets
   the darkened literal #4F39C6, 6.53:1 on that same tint. */
.pill.attn { background: rgba(167,146,252,.18); color: #BBAAFD; }
:root[data-theme="light"] .pill.attn { background: rgba(109,91,208,.12); color: #4F39C6; }
/* GOODQA 13d59ad9 — Home's neutral chip: a live, working device with no
   stronger claim to make (not confirmed seen in the last two minutes, but
   not paused and not overdue for a check-in either — see js/overview.js's
   childStatus()). Deliberately its own look, not on/off/wait: those three
   are reserved (UI-DIRECTION-2026-08-22 §3 — mint/coral/amber each carry one
   meaning), and painting a fourth real state in one of their colours would
   borrow a meaning it does not have. */
.pill.neutral { background: var(--dusk2); color: var(--haze); }

/* --------------------------------------------------------------- status card */

/* Sensitive actions use a real modal instead of moving focus to an inline
   banner at the top of whichever long page happens to be open. The dialog is
   deliberately compact: one reason, one password field, one decision. */
.stepup-dialog {
  width: min(470px, calc(100vw - 24px));
  max-width: none;
  margin: auto;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 22px;
  background: var(--dusk);
  color: var(--cloud);
  box-shadow: 0 24px 70px rgba(0,0,0,.34);
}
.stepup-dialog::backdrop {
  background: rgba(3,9,18,.7);
  backdrop-filter: blur(4px);
}
:root[data-theme="light"] .stepup-dialog::backdrop,
:root:not([data-theme]) .stepup-dialog::backdrop { background: rgba(10,16,32,.42); }
.stepup-shell {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr);
  gap: 14px;
  padding: 22px;
}
.stepup-mark {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border: 1px solid color-mix(in srgb, var(--glow) 46%, var(--line));
  border-radius: 15px;
  background: color-mix(in srgb, var(--glow) 12%, var(--dusk2));
  color: var(--glow);
}
.stepup-mark svg {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.stepup-copy { min-width: 0; }
.stepup-title { margin: 1px 0 0; font-size: 21px; line-height: 1.2; }
.stepup-message { margin: 7px 0 17px; color: var(--haze); font-size: 14px; line-height: 1.5; }
.stepup-label { margin: 0 0 7px; color: var(--cloud); }
.stepup-copy > .inp { width: 100%; margin: 0; }
.stepup-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  grid-column: 1 / -1;
  margin-top: 4px;
}
#stepUpRow .primary, #stepUpRow .ghost {
  width: 100%;
  min-height: 44px;
  margin: 0;
}
@media (max-width: 380px) {
  .stepup-shell { grid-template-columns: 42px minmax(0, 1fr); gap: 11px; padding: 18px; }
  .stepup-mark { width: 42px; height: 42px; border-radius: 13px; }
  .stepup-title { font-size: 19px; }
}
@media (forced-colors: active) {
  .stepup-dialog, .stepup-mark { border-color: CanvasText; }
  .stepup-mark { color: Highlight; }
}

.statuscard {
  display: flex; align-items: center; gap: 14px;
  background: linear-gradient(135deg, var(--statustop), var(--statusbot));
  border: 1px solid var(--line); border-radius: 22px; padding: 18px; margin-bottom: 16px;
  box-shadow: inset 0 1px rgba(255,255,255,.04), 0 14px 36px rgba(0,0,0,.12);
}
.statustext { flex: 1; min-width: 0; }
.st { font-weight: 700; font-size: 18px; }
.ss { font-size: 12px; font-weight: 600; color: var(--done-text); margin-top: 2px; }
.ss.off { color: var(--coral); }
/* Same 4.38:1-on-white fix as .ghost.danger above, same reused #B23127 — see
   that rule's comment. */
:root[data-theme="light"] .ss.off { color: #B23127; }
.ss.wait { color: var(--haze); }

.toggle { width: 58px; height: 32px; border-radius: 20px; position: relative; flex: none; transition: background .2s; }
.toggle.on { background: var(--mint); } .toggle.off { background: var(--track-switch); }
.toggle::after {
  content: ""; position: absolute; width: 26px; height: 26px; border-radius: 50%;
  background: #fff; top: 3px; transition: left .2s, right .2s; box-shadow: 0 2px 6px rgba(0,0,0,.4);
}
.toggle.on::after { right: 3px; } .toggle.off::after { left: 3px; }
.toggle:disabled { opacity: .5; cursor: default; }

/* ----------------------------------------------------------------- app rows */

/* Padding/gap opened up 2026-09-18 (owner review: "the feel and polish of
 * the mockups is nicer [css spacing, layout]") -- 11px/13px read as cramped
 * next to the reference mockup's own list generosity once cards were
 * already at its 22px/20px-radius language. Deliberately NOT copied from
 * the mockup's literal `.item{padding:13px 0}` verbatim: that pattern is a
 * borderless flat list with a divider line, ours is a bordered/filled row
 * (background + border + radius already read as separated), which needs
 * less inter-row gap than a divider-only list to avoid looking like loose
 * floating chips. `.pw-row` (policy-workspace.css) fully overrides padding
 * and margin for the Background Apps / Apps dense tables that share this
 * base class, so this change does not reach those two screens' rows at
 * all -- confirmed by reading policy-workspace.css before this edit. */
.arow {
  display: flex; align-items: center; gap: 12px; background: var(--dusk2);
  border: 1px solid var(--arowline); border-radius: 14px; padding: 14px 16px; margin-bottom: 10px;
}
.arow .an { font-size: 14px; font-weight: 600; flex: 1; min-width: 0; overflow-wrap: anywhere; }
.arow .asub { font-size: 11px; color: var(--haze); font-weight: 400; }
/* MEASURED 35px in every language: #copNameEdit ("Change"/"Cambiar"/"Modifier")
   and the account card's own Sign out both sat nine pixels under the touch
   minimum, and `.iconbtn` had no min-height at all — 12px text plus 8px of
   padding was the whole height. `inline-flex` with centred items comes with
   the floor because a bare `min-height` on an inline-block button leaves its
   label riding the top edge of the taller box; three sibling rules
   (`.schedule-item-actions .iconbtn`, both `.reqactions .iconbtn` copies)
   already spell exactly this shape out locally. */
.iconbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  background: transparent; border: 1px solid var(--line); color: var(--haze);
  /* HER SPEC, measured off the handoff's Approve/Deny pair: a 999px pill,
     Quicksand 12.5px/700, 8px/14px padding. It was a 10px-radius 12px Nunito
     control -- and --radius-button (999px) ALREADY EXISTED in this file and
     was simply not consumed here, which is most of why the live build reads as
     boxy beside her work.
     min-height: 44px is kept as the TOUCH floor and relaxed to her 34px only
     at >=900px -- see the media query below. Her artboards are all 1512px
     wide, so 34px is her DESKTOP value; applying it to phones would not be
     following her design, it would be extrapolating it onto a case she never
     drew, under the pointer where a 10px miss actually costs something. */
  border-radius: var(--radius-button); padding: 8px 14px; min-height: 44px;
  font-family: var(--font-heading); font-size: 12.5px; font-weight: 700; flex: none;
}
@media (min-width: 900px) and (pointer: fine) {
  /* Her 34px, only where she drew it and only for a precise pointer. */
  .iconbtn { min-height: 34px; }
}
.iconbtn.danger { color: var(--coral); }
/* Same 4.38:1-on-white fix as .ghost.danger above, same reused #B23127. */
:root[data-theme="light"] .iconbtn.danger { color: #B23127; }

.sw { width: 46px; height: 26px; border-radius: 14px; position: relative; flex: none; transition: background .2s; }
.sw.on { background: var(--mint); } .sw.off { background: var(--track-switch); }
.sw::after { content: ""; position: absolute; width: 20px; height: 20px; border-radius: 50%; background: #fff; top: 3px; transition: left .2s, right .2s; }
.sw.on::after { right: 3px; } .sw.off::after { left: 3px; }
.sw:disabled { opacity: .5; cursor: default; }
/* A BLOCK IS NOT MINT. Everywhere else on this page `.sw.on` / `.toggle.on` is
   mint and means "this is on, and that is the good state" — the tablet is
   awake, a protection is enabled. The inventory switch is the one control whose
   ON position TAKES something away from a child, and painting it in the same
   green as "protection enabled" is a colour that argues against the label. The
   coral is the same one .ghost.danger and .pill.off already use for "this is
   the direction with a cost", so a parent has seen it before they get here.
   Colour is never the only signal: the row also carries the word (`blocked` /
   `not blocked` / `you allowed this`) and the switch carries role="switch" with
   aria-checked, which is what a screen reader reads. */
.sw.blk { background: var(--coral); }

/* The time column. Tabular figures so "2 h 40 m" and "6 h 05 m" line up down
   the list — a column of times that jitters is one a parent cannot scan, and
   scanning is the only thing this column is for. `flex: none` so a long app
   name shortens instead of squeezing the number onto two lines. */
.arow .atime {
  font-size: 13px; font-weight: 600; color: var(--haze); flex: none;
  text-align: right; min-width: 62px; font-variant-numeric: tabular-nums;
  /* CAPPED AND ALLOWED TO WRAP, and both halves were measured at 390x844.
     "2 h 40 m" is 62px and "under a minute" is 96px — uncapped, that one row's
     time column stole a third of the name column and squeezed the identifier
     and the two-line platform sentence underneath it into a ribbon, while the
     row above it (a short time) stayed wide. A list whose left edge is a
     different width on every row is a list nobody can scan, and scanning is the
     only thing this column exists for. */
  max-width: 84px;
}
/* An inventory row stacks a name over four or five short facts, so it is taller
   than the allow-list rows this class was written for and its controls must sit
   at the top rather than floating in the middle of the block. */
#invList .arow { align-items: flex-start; }
#invList .arow .asub { margin-top: 2px; }

/* GoodQA #447 (2026-09-16 handoff, epic #437). Activity's per-child "no
   device linked" notice — .arow's own row shape, plus a trailing link in the
   same trusted/primary --glow every other in-row action on this page already
   uses (.arow .an/.asub are reused as-is for the two lines of text). */
.activity-no-device { margin-bottom: 16px; }
.activity-no-device a { color: var(--glow); font-weight: 700; font-size: 13px; white-space: nowrap; flex: none; }

/* ------------------------------------------- GOODQA 13d59ad9 Frame 2: Apps
   New on his tablet / Allowed / Everything else. Scoped to #invList and its
   own new classes only — nothing here touches .arow/.sw/.chip/.pill's shared
   rules above, so the flat (searched/filtered) view they still serve is
   pixel-identical to before this pass. Tokens reused, not invented: --glow is
   this app's existing "trusted / primary action" cyan (matches the dashboard
   token table teal/blue = trusted, primary), --dusk2/--line/--haze are the
   same neutral chip/card vocabulary overview.js's own pill.neutral reuses. */

.invGroupHead { margin-top: 22px; }
.invGroupHead:first-child { margin-top: 0; }

/* A left rail in the primary/trusted colour, not a background wash — a row
   awaiting a decision is not a warning (amber is reserved for that, per the
   colour table), it is simply new and worth a glance. */
.arow.new { border-left: 3px solid var(--glow); flex-wrap: wrap; }

/* No app-icon tile. GoodQA 8b07d2b2: `icon_ref` used to exist as a schema
   column (0010-inventory) that nothing ever wrote a non-null value to — it
   was dropped outright (0038-drop-unused-icon-ref) rather than left implying
   a feature nobody built. If a real icon pipeline ships later (device-side
   capture + content-addressed upload, per that ticket's own "if populating"
   plan) it is new work, not a resurrection of this column. A fabricated
   letter tile would claim a visual identity this data does not have; the
   name text carries it instead, the same way every other row on this page
   already does. */

.invchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }

/* flex: 1 0 100% — NOT width:100%. This is a THIRD item in `.arow`'s row
   alongside `.an` and `.sw`, and `.arow` itself is nowrap by default
   (`#invList .arow` sets only align-items). width:100% on a flex child in a
   nowrap row does not create a new line — it stakes the child's PREFERRED
   size at the full row width while the row still tries to fit everything on
   one line, so the OTHER two items (the name column, which has `flex:1;
   min-width:0`) are squeezed to nothing and "Minecraft" wraps one letter per
   line down the card. FOUND BY RENDERING IT (verification/dashboard-apps-
   13d59ad9/01, before this fix). `.arow.new`'s own `flex-wrap: wrap` above
   is what actually allows a second line to exist; `flex-basis: 100%` here is
   what forces this item onto it, after `.an` and `.sw` on the first. */
.invRowActions { display: flex; flex-wrap: wrap; gap: 7px; flex: 1 0 100%; margin-top: 8px; }
.invRowActions .ghost { padding: 8px 11px; font-size: 12px; min-height: 38px; }

/* "Everything else", collapsed by default in a native <details> rather than
   a bespoke JS-toggled panel. */
details.invGroup { margin-top: 14px; }
details.invGroup summary {
  cursor: pointer; font-size: 12px; font-weight: 750; color: var(--haze);
  padding: 8px 2px; list-style: none;
}
details.invGroup summary::-webkit-details-marker { display: none; }
details.invGroup summary::before { content: "▸ "; }
details.invGroup[open] summary::before { content: "▾ "; }

/* THE SYSTEM-COMPONENT FOLD (§3.25b). Auto-approved rows are inert, not
   urgent — the opposite of an amber "needs attention" row — so this is a
   quieter variant of .invGroup, not a louder one: the summary sits a touch
   dimmer until opened, matching the "auto-approved" fact it names, and the
   rows inside stay full-strength .arow markup so Allow/Block still reads and
   works exactly as it does above the fold. */
details.invFoldGroup { margin-top: 18px; opacity: 0.86; }
details.invFoldGroup[open] { opacity: 1; }
details.invFoldGroup summary { color: var(--haze); font-weight: 650; }

/* A PROFILE ROW NOW CARRIES THREE BUTTONS AND MUST BE ALLOWED TO WRAP.
   FOUND BY RENDERING IT, not by reading it. `.arow` is a single flex line whose
   name column is `flex: 1; min-width: 0`, which means the name is the only thing
   that can give — so adding "Apps & programs" beside Rename and Remove pushed
   the three buttons past 390px and the browser shrank the name column to ONE
   CHARACTER PER LINE. Leo's row was a vertical column of letters 30 lines tall.
   Nothing failed; the page just became unreadable on the viewport it is designed
   for, which is why the screenshot is part of the check and the markup test is
   not enough.
   Scoped to #profList so the allow-list, limits, schedule and inventory rows —
   which fit on one line and are meant to — are untouched. 55% is the smallest
   name column worth reading at 390px; below it the buttons take their own line,
   where all three fit with room to spare. */
#profList .arow { flex-wrap: wrap; }
#profList .arow .an { min-width: 55%; }

/* THE SECURITY CARD'S RECOVERY-CODES ROW, SAME SHAPE, SAME FIX (round-5
   audit auth[0] / GoodQA #409). `statusRow()` (consent.js) builds this row
   from a label plus a pill plus an `.iconbtn`, and at 320px those two
   flex:none children leave `.an` (`flex:1; min-width:0;
   overflow-wrap:anywhere` above) about 15px wide — not zero, but too
   narrow for a whole word, so "Recovery codes" broke one character per
   line down the card (MEASURED: 320x568 -> 223px tall / 11 line-tops for
   14 characters; 375x667 control -> 40.6px / 2 lines). `.codesrow` is a
   marker class consent.js stamps on this one row only, so the
   email/password/2FA rows sharing #securityRows — which fit on one line at
   320px today — are untouched; nothing here removes `.arow .an`'s
   overflow-wrap:anywhere, which is still what keeps a single unbreakable
   token from overflowing the card at any width. */
.arow.codesrow { flex-wrap: wrap; }
.arow.codesrow .an { min-width: 55%; }

/* THE CONTINUOUS-DELETE OFFER (redesign decision idx4, settings-account,
   2026-09-17). offerMoveThenRemove's rows in #profMoveOffer reuse .arow's card
   look but add a <select>, which is `.inp`'s `width:100%` — a flex item that
   wants the whole row on its own. Wrap and let the select and button share
   the line at every width down to 390px instead of overflowing it. */
#profMoveOffer .arow { flex-wrap: wrap; }
#profMoveOffer .arow select.inp { width: auto; flex: 1 1 140px; min-width: 0; min-height: 40px; padding: 8px 10px; }

/* ------------------------------------------------------- the profile switcher
 *
 * ROUND, AND NEVER A TAB. PARENT-APP-SURFACES §2.1: the control that says which
 * profile a DEVICE IS RUNNING must not be confusable with a control that says
 * which profile a parent is LOOKING AT — one is harmless and the other hands a
 * child a different machine. So this vocabulary is deliberately unlike .chip
 * (the day picker) and unlike anything on the tablet list: a circle with an
 * initial, its name under it, and the running one lit in the brand glow.
 *
 * NO ::before HIT EXPANDER, unlike .sw / .toggle / .iconbtn above. Those are
 * 26-35px tall and borrow an invisible 44px surface; a face is 62x66 on its own,
 * which is over the Apple HIG 44pt and the Material 48dp floors without help. A
 * control that is genuinely big enough should not carry an invisible box that
 * can overlap its neighbour — and its neighbours here are OTHER PROFILES, so an
 * expander that stole a tap would put a tablet on the wrong child's rules. */
.faces { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px; }
.face {
  background: transparent; border: 1px solid var(--line); border-radius: 16px;
  color: var(--haze); font-family: var(--ui); font-size: 11px; font-weight: 600;
  padding: 9px 8px; min-width: 62px; min-height: 66px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.face .fi {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  background: var(--dusk2); border: 2px solid var(--line); color: var(--cloud);
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 700;
}
.face .facelbl { max-width: 70px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.face[aria-pressed="true"] { border-color: var(--glow); color: var(--cloud); }
.face[aria-pressed="true"] .fi { background: var(--glow); border-color: var(--glow); color: var(--ink); }
/* GOODQA #465/#468 — same regression and same fix as `.primary` above: light
   --glow is now brand blue, and dark ink on it is 3.50:1 at this initial's
   15px/700 (not large text, needs 4.5). White is 5.42:1. */
:root[data-theme="light"] .face[aria-pressed="true"] .fi { color: #FFFFFF; }
/* The one profile that removes every rule at once should not look like the three
   next to it. A hint only — the confirm sheet in doSwitch is the protection. */
.face.parent .fi { border-style: dashed; }
.face:disabled { opacity: .5; cursor: default; }
.swrun { font-weight: 700; font-size: 15px; margin-top: 2px; }

/* ---------------------------------------------------------------- schedules */

.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 4px; }
/* GOODQA #470 — was 20px; see `button`'s own comment above on --radius-pill.
   `.card` (--radius, 20px) is untouched: the ticket's own evidence found it
   already on-spec, the delta is confined to the button/chip/input tier. */
.chip {
  border: 1px solid var(--line); background: var(--dusk2); color: var(--haze);
  border-radius: var(--radius-pill); padding: 9px 13px; font-size: 13px; font-weight: 600;
  cursor: pointer; font-family: var(--ui); min-height: 44px; min-width: 48px;
}
/* --glow is bright cyan in dark mode and a dark AA-safe cyan in light mode.
   --dusk flips with it (dark ink / white ink), keeping selected 13px tabs above
   4.5:1 in both palettes; fixed --ink failed on light #087EA4. */
.chip[aria-pressed="true"] { background: var(--glow); color: var(--dusk); border-color: var(--glow); }

/* ------------------------------------------------------- schedule list/editor
   The list remains in the Time page; the long editor moves into the browser's
   top layer so adding one routine never turns the entire settings page into a
   second, deeply stacked form. */
.schedule-card-head { align-items: center; margin-bottom: 12px; }
.schedule-card-head > .sect { margin-bottom: 0; }
.schedule-add { display: inline-flex; align-items: center; gap: 6px; flex: none; }
.button-glyph { display: inline-grid; place-items: center; font-size: 17px; line-height: 1; }
.schedule-list { display: grid; gap: 8px; }
.schedule-list .arow { margin: 0; }
.schedule-item { align-items: flex-start; padding: 14px 15px; }
.schedule-item .an { padding-top: 3px; }
.schedule-item .asub { margin-top: 4px; line-height: 1.45; }
.schedule-item-actions { display: flex; align-items: center; gap: 7px; flex: none; }
.schedule-item-actions .iconbtn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 40px; }
.schedule-empty {
  display: flex; align-items: center; gap: 10px; min-height: 66px;
  padding: 12px 14px; border: 1px dashed var(--line); border-radius: 14px;
  background: var(--dusk2);
}
.schedule-empty-icon { color: var(--glow); font-size: 24px; line-height: 1; }
.schedule-empty .muted { margin: 0; padding: 0; text-align: left; }

.schedule-dialog {
  width: min(720px, calc(100vw - 24px)); max-width: none;
  max-height: min(780px, calc(100dvh - 24px)); margin: auto; padding: 0;
  overflow: hidden; border: 1px solid var(--line); border-radius: 22px;
  background: var(--dusk); color: var(--cloud); box-shadow: 0 24px 70px rgba(0,0,0,.36);
}
.schedule-dialog::backdrop { background: rgba(3,9,18,.7); backdrop-filter: blur(4px); }
:root[data-theme="light"] .schedule-dialog::backdrop,
:root:not([data-theme]) .schedule-dialog::backdrop { background: rgba(10,16,32,.42); }
.schedule-dialog-shell { max-height: min(780px, calc(100dvh - 24px)); overflow: auto; padding: 22px; }
.schedule-dialog-head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 18px; }
.schedule-dialog-head > div { flex: 1; min-width: 0; }
.schedule-dialog-kicker {
  margin-bottom: 4px; color: var(--glow); font-size: 10px; font-weight: 800;
  letter-spacing: .12em; text-transform: uppercase;
}
.schedule-dialog-head h2 { margin: 0; font-size: 23px; line-height: 1.2; }
.schedule-dialog-close { width: 44px; height: 44px; padding: 0; font-size: 23px; }
.schedule-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.schedule-field { min-width: 0; }
.schedule-field > .fld { color: var(--cloud); }
.schedule-field-days, .schedule-field-times, .schedule-field-apps { grid-column: 1 / -1; }
fieldset.schedule-field { margin: 0; padding: 0; border: 0; }
.schedule-field-days .chips { margin-bottom: 0; }
.schedule-field-times { margin: 0; }
.schedule-app-list {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px;
  max-height: 230px; overflow: auto; padding: 8px;
  border: 1px solid var(--line); border-radius: 14px; background: var(--dusk2);
}
.schedule-app-list .consentRow { margin: 0; min-width: 0; }
.schedule-dialog-msg { text-align: left; padding-bottom: 0; }
.schedule-dialog-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 16px; }
.schedule-dialog-actions .primary, .schedule-dialog-actions .ghost { width: 100%; min-height: 48px; margin: 0; }

@media (max-width: 560px) {
  .schedule-card { padding: 17px; }
  .schedule-card-head { align-items: flex-start; }
  .schedule-add { min-height: 42px; padding: 9px 11px; }
  .schedule-item { display: grid; grid-template-columns: minmax(0, 1fr); gap: 11px; }
  .schedule-item-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
  .schedule-item-actions .iconbtn { width: 100%; min-height: 44px; }
  .schedule-dialog { width: calc(100vw - 12px); max-height: calc(100dvh - 12px); border-radius: 18px; }
  .schedule-dialog-shell { max-height: calc(100dvh - 12px); padding: 17px; }
  .schedule-form-grid { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .schedule-field-days, .schedule-field-times, .schedule-field-apps { grid-column: 1; }
  .schedule-app-list { grid-template-columns: minmax(0, 1fr); }
}

@media (forced-colors: active) {
  .schedule-dialog, .schedule-empty, .schedule-app-list { border-color: CanvasText; }
}

/* THE BILLING-PERIOD SWITCH (Settings -> Billing; PARITY-DECISIONS §97/§100).
   Monthly / Every 3 months / Annual, rendered from the catalog's own intervals
   and only when it returns more than one — billing.js draws no control at all
   below that, so this rule can go unused without leaving an empty frame on the
   page. Selected state borrows `.chip[aria-pressed="true"]`'s exact pair
   (--glow on --dusk) rather than a second cyan: UI-DIRECTION-2026-08-22 §4 has
   cyan meaning "where you are", and one selected control on this page must not
   look like a different colour from another. It wraps, like every other row of
   controls here. */
.billingtoggle { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.billingtoggle button[aria-pressed="true"] { background: var(--glow); color: var(--dusk); border-color: var(--glow); }

/* ---------------------------------------------------------------------- nav */
/* A WRAPPING ROW OF CHIPS, because that is what this page already does with too
   many controls — `.chips`, `.faces` and `#profList .arow` all wrap — and there
   is no horizontal scroller anywhere in it. This was measured at nine entries
   wrapping to two rows at 390px and three at 360px, against the e2e viewports;
   GoodQA 13d59ad9 cut the static count to five and added one per child, so the
   exact row count moved with household size, but the wrap itself did not — see
   its own comment on #navChildren below.

   NO position:sticky and no fixed bar, AS DRAWN HERE. The page used neither
   when this comment was written. A phone build now DOES pin `.nav` as a fixed
   bottom dock (`body.signed-in #nav`, `@media (max-width: 899px)`, below) —
   an id selector, so it always wins the cascade over this block's margin
   regardless of source order, and that rule zeroes the margin itself. This
   wrapping-row shape is therefore not reachable by any signed-in phone today;
   it is kept, scoped, for whatever un-docked/no-js state still renders it.

   SCOPED TO <900px, 2026-09-20 — it was NOT scoped before, and that was a
   real bug, not a style preference. `.nav`'s own `margin: 0` two screens up
   (`@media (min-width: 900px)`, the flush full-height rail) and this
   `margin: 0 0 18px` are the SAME SPECIFICITY on the SAME SELECTOR — the one
   later in the file wins at every width, including >=900px, no matter which
   media query "sounds" more specific. This one is later, so it was quietly
   reinstating an 18px bottom margin the rail fix explicitly zeroed out.
   MEASURED: `getComputedStyle(nav).marginBottom` was `18px` on the live
   >=900px rail, `.wrap` (the grid row) was exactly 18px TALLER than `.nav`,
   and OWNER, on the live build: "you can still scroll too far to a white
   broken chunk" — this 18px was one of the two contributors (see #main's
   sibling rule at this file's `@media (min-width: 900px)` block for the
   other, `body`'s own leftover phone `padding-bottom`). Scoping this rule is
   the fix: at <900px nothing changes (the id-selector dock rule already wins
   here regardless), and at >=900px the rail's own `margin: 0` now finally
   applies uncontested. */
@media (max-width: 899px) {
  .nav { margin: 0 0 18px; }
}
.navrow { display: flex; flex-wrap: wrap; gap: 6px; }
/* Shape comes entirely from `.chip`. Declared so the token is in the vocabulary
   and so there is a place to hang nav-only rules when one is genuinely needed. */
.navbtn { }
/* The current screen, marked with aria-current="page" rather than a class, so
   the paint and the accessible name can never disagree. Same fill `.chip
   [aria-pressed="true"]` uses one rule up: cyan is "where you are", per
   UI-DIRECTION §3. */
.navbtn[aria-current="page"] {
  background: var(--glow); color: var(--ink); border-color: var(--glow);
}
/* Light theme darkens --glow to #087EA4; dark ink on that fill is only 3.8:1
   at the chip's 14px text. White restores 4.7:1 on both primary and per-child
   sub-navigation, which the Sites browser suite measures in the live screen. */
:root[data-theme="light"] .navbtn[aria-current="page"] { color: #FFFFFF; }
:root[data-theme="light"] .navbtn[aria-current="page"] { color: #FFFFFF; }
/* The skip link. `.sr-only` already takes it out of the flow visually; this
   only stops it reserving a line box above the nav row. */
.navskip { position: absolute; }

/* …AND IT COMES BACK WHEN IT IS FOCUSED, which is the whole point of a skip
   link and was not happening.
   
   MEASURED, in both themes, at every width from 320 to 3840: Tab once from the
   page and the first stop is this link, still 1px x 1px, still clipped to
   `rect(0,0,0,0)`, wearing the browser's default `outline: 1px auto` — an
   outline drawn around a one-pixel box. A sighted keyboard user's focus
   vanished on the first keystroke of every visit and reappeared on the second.
   That fails WCAG 2.4.7 Focus Visible outright, and it quietly defeats 2.4.1
   Bypass Blocks too: the mechanism exists, but nobody who can see the screen
   can tell they have landed on it or what pressing Enter would do.
   
   `.sr-only` sets nine properties and every one of them has to be undone here,
   not just `width`/`height` — `clip: rect(0,0,0,0)` alone keeps the box blank
   at any size, and the `margin: -1px` pulls it back under the header.
   
   :focus AS WELL AS :focus-visible. Chromium matches :focus-visible on a link
   reached by Tab, but `.focus()` from script (and Safari's older behaviour)
   does not always, and a skip link that is invisible for one of the two ways of
   arriving is a skip link that is invisible when it matters. */
.navskip:focus,
.navskip:focus-visible {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 60;
  width: auto;
  height: auto;
  min-height: 44px;
  margin: 0;
  padding: 11px 16px;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: nowrap;
  border: 1px solid var(--glow);
  border-radius: 12px;
  background: var(--dusk);
  color: var(--cloud);
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 700;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .45);
}
/* ATTENTION — a request is waiting. `.pill.on` / `.pill.off` deliberately do not
   use --mint / --coral either (measured 3.7:1 against the card); this is checked
   the same way and uses the token rather than a fourth literal copy. */
.pill.wait { background: rgba(255,194,75,.14); color: var(--waiting-text); }
/* The two semantic nav groups carry no box on compact layouts. `display:
   contents` drops their wrappers out of the layout, so their buttons behave as direct
   children of #navRow's own flex row (wrapped chips on a phone, a stacked
   rail at the sidebar breakpoint below) exactly like the static entries
   around them. The wrappers give assistive technology labelled Children and
   Household groups; #navChildren is also the stable node renderChildNav()
   can rebuild without replacing unrelated controls. */
#navChildren,
#navHousehold { display: contents; }
/* Apps / Websites / Categories, inside one child's own screens (GoodQA
   13d59ad9 — see the comment on renderChildSubnav() in app.js). Same chip
   shape as the primary nav on purpose: this is the same three destinations
   the top nav used to offer, just scoped to the child already chosen, not a
   new vocabulary. Less bottom margin than `.nav` carries, because this row
   sits under a screen's own title rather than under the page header. */
.subnav {
  position: sticky;
  top: 8px;
  z-index: 11;
  margin: 0 0 14px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: color-mix(in srgb, var(--dusk) 94%, transparent);
  backdrop-filter: blur(14px);
}
@media (min-width: 900px) {
  .subnav { top: 12px; }
}
/* #invMsg is the very next element after #appsSubnav in index.html.
   invSay()'s `scrollIntoView({block:'nearest'})` has no idea `.subnav` is
   pinned there by `position:sticky` — "nearest" is satisfied by a position
   the sticky bar already occupies, so the message lands right underneath it.
   Same fix, same reason, as `#policy [data-policy-section]` below. */
#invMsg { scroll-margin-top: 76px; }

.sched-days-label { margin-top: 12px; }
.dns-label { margin-top: 16px; }
/* GOODQA #95: the on-demand screenshot. Capped to the card's own width (never
 * wider, whatever resolution the tablet reports) and given the same border/
 * radius language `.arow`/`.card` already use so it reads as part of this
 * page rather than a pasted-in image. */
.ssimg { display: block; width: 100%; max-width: 100%; height: auto; border-radius: 10px; border: 1px solid var(--line); margin: 8px 0; }
/* The second field in the sign-in card. `.fld` has no top margin because it is
 * normally the first thing in a card; a label that follows an input needs the
 * gap or the password label sits flush against the email box and reads as its
 * helper text. A CLASS, not a style="" — see the note at the foot of this file:
 * the page is served under style-src 'self' with no 'unsafe-inline', so an
 * inline style is dropped by the browser and nothing errors. */
.pw-label { margin-top: 14px; }
/* `flex-wrap: wrap` plus `min-width: 0` on every child — the same pair
   `.pagehead > .brand`/`.controls` already use for this exact defect class.
   A native `<input type="time">` has a rendering floor it will not shrink
   below, and a flex item's default `min-width: auto` takes that floor as a
   hard minimum regardless of `.timein`'s own `width: 100%`. child-settings.js's
   flat "Add a window" row (two time inputs, no wrapping `<label>` around
   each like the device-mode #schedForm dialog uses) sits in plain page flow
   with nothing to catch the resulting overflow — unlike that dialog, whose
   `.schedule-dialog-shell` has its own `overflow: auto` — so it pushed the
   whole page into horizontal scroll at a 320px viewport. */
.timerow { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0 4px; }
.timerow label { flex: 1; font-size: 12px; font-weight: 600; color: var(--haze); }
.timerow > * { min-width: 0; }
.timein { margin-top: 6px; }

/* -------------------------------------------------------- touch target floor
 * Apple HIG says 44pt, Material says 48dp, WCAG 2.2 SC 2.5.8 (AA) says 24px is
 * the hard floor.
 *
 * THIS COMMENT USED TO CLAIM the controls above were all sized to clear 44px on
 * their own, and that borrowing the dashboard's invisible hit-surface trick was
 * unnecessary. Then the page was MEASURED in a real browser at 390x844
 * (verification/parent-ui/shots.mjs) and five kinds of control were under it:
 *
 *   #themeToggle  54x30      #powerToggle  58x32      .sw          46x26
 *   .iconbtn ✕    34x35      .iconbtn Edit 47x35
 *
 * All above the AA floor of 24, all well under a thumb. So the trick is borrowed
 * after all: a hit surface is grown with an absolutely-positioned ::before while
 * the visible chrome stays compact. `min(...)` is not used — the expander is
 * always at least 44 in both axes and never smaller than the control itself.
 *
 * It is verified, not asserted: the audit credits a pseudo-element box only
 * after probing its edge midpoints with elementFromPoint and confirming the
 * browser really hit-tests to this element there, so an expander covered by a
 * neighbour earns nothing. That probe is lifted from e2e/mobile.spec.cjs, which
 * learned it the same way. */
.themebtn, .toggle, .sw, .iconbtn { position: relative; }
.themebtn::before, .toggle::before, .sw::before, .iconbtn::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: max(100%, 44px); height: max(100%, 44px);
  /* Purely a hit surface. Nothing is painted, and it must not intercept the
   * pointer for anything other than its own control. */
  background: transparent;
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

@media (max-width: 560px) {
  /* THE WHOLE PAGE SCROLLED SIDEWAYS ON A PHONE, and this header was why.
     MEASURED at 390x844 on a signed-in dashboard: document.scrollWidth 453
     against a 390px viewport, and the two elements past the right edge were
     `.pagehead > .controls` (Menu + theme toggle + language select, 234px,
     ending at x=453) and the `#langSel` inside it. `.pagehead` is a flex row
     whose two children — the brand block and the controls — both refuse to
     shrink, so 205 + 18 + 234 simply did not fit and the BODY grew instead.
     Every screen inherited it: inventory-ui.test.js's "the page scrolls
     horizontally at 390px" was red at 20fcac5d for this reason, on a screen
     whose own row it was measuring was fine.

     WRAPPING, not truncating and not hiding. The brand cannot be ellipsised
     (it is the product's name) and the language select cannot move to the
     rail here — below 900px there is no rail, and placeChromeControls() puts
     the select back in this header ON PURPOSE for exactly that reason. One
     row is arithmetically impossible at 390px, so the controls take their own
     right-aligned line: nothing is cut off, nothing is hidden, and the page
     stops scrolling sideways. `min-width: 0` on both children lets the wrap
     happen at all — a flex item's default `min-width: auto` is what pushes a
     row past its container instead of breaking it. */
  .pagehead { padding: 10px 11px; border-radius: 17px; flex-wrap: wrap; row-gap: 10px; }
  .pagehead > .brand { min-width: 0; }
  .pagehead > .controls { min-width: 0; flex: 1 1 100%; }
  .brand { gap: 9px; }
  .ap { width: 38px; height: 42px; }
  .brandmark { width: 38px; height: 38px; }
  .wm { font-size: 19px; }
  /* THE HEADER SCROLLED EVERY SCREEN OF THE PRODUCT SIDEWAYS ON A PHONE.
   *
   * MEASURED IN CHROMIUM at a 390px viewport, signed in, on all eight screens:
   * `.pagehead`'s own box was 362px and correct, and `.controls` inside it ran
   * from x=219 to x=453 — 91px out through the right-hand wall of its own
   * parent. document.scrollWidth was 453 against a 390 clientWidth on Overview,
   * Requests, Check-ins, Devices, Processes, Activity, Account and Help alike.
   * test/inventory-ui.test.js's "the page scrolls horizontally at 390px" has
   * been red on exactly this.
   *
   * THE CAUSE IS FLEXBOX'S AUTO MINIMUM SIZE, not any one control being fat.
   * `.pagehead` is a flex row; `.brand` (175px) and `.controls` (234px) plus the
   * gap and padding want 449px, and neither child may shrink below its content,
   * so the row simply exceeds the box. Trimming controls does not fix it — the
   * numbers do not reach at 390px even with every control shrunk, and they can
   * never reach at 320px or once a longer language name is on screen.
   *
   * SO IT WRAPS, which is the only version of this that is true at every width
   * and in every language: one row while everything fits, brand over controls
   * when it does not, and never a sideways page. `.controls` keeps
   * `justify-content: flex-end`, so on its own row it sits under the brand and
   * against the right wall rather than drifting left.
   *
   * THE THREE CONTROLS ALL STAY, and that is deliberate rather than lazy. The
   * language select is in the header at phone width ON PURPOSE — a phone has no
   * rail, app.css hides `.navbrand`/`.navfoot` outright below the rail
   * breakpoint, and test/nav-reference-parity.test.js pins it: "a phone has no
   * language control at all" is the failure it guards. Moving either control
   * into the drawer would fit the row by taking a control away.
   *
   * `min-width: 0` on both children so a long wordmark or a long option label
   * can never re-create the overflow inside a row that is now allowed to wrap. */
  .pagehead { flex-wrap: wrap; gap: 8px 12px; }
  .brand { min-width: 0; flex: 1 1 auto; }
  .controls { min-width: 0; flex: 1 1 auto; flex-wrap: wrap; gap: 6px; }
  .langsel { max-width: 104px; font-size: 13px; padding: 7px 7px; }
  .card { padding: 18px; border-radius: 21px; }
}

@media (hover:hover) {
  .card, .statuscard, .arow, .devrow, .ghost, .primary { transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
  .card:hover, .statuscard:hover { border-color: color-mix(in srgb, var(--glow) 34%, var(--line)); }
  .primary:hover { transform: translateY(-1px); box-shadow: 0 14px 30px rgba(101,216,255,.24); }
  .ghost:hover, .arow:hover, .devrow:hover { border-color: color-mix(in srgb, var(--sky) 40%, var(--line)); }
}

/* Toggled from app.js. A CLASS AND NOT AN INLINE STYLE: the page is served under
   a CSP with no 'unsafe-inline' for styles, so a style="" attribute is silently
   dropped by the browser — the element would simply stay visible. A test asserts
   the markup carries no style attributes, and it caught exactly that mistake. */
.is-hidden { display: none !important; }

/* EGRESS CONTROL spacing.
   These were inline `style="margin-top:..."` attributes and never applied: the
   page is served under `style-src 'self'`, so Chromium drops inline style
   attributes and logs "Applying inline style violates the following Content
   Security Policy directive". The control rendered flush against the DNS block
   above it. Caught by reading the browser console, not by any suite — nothing
   in the test tree evaluates CSP. */
.fld.vpn-sep { margin-top: 14px; }
.inp.vpn-resolver { margin-top: 8px; }

/* Privacy choices (§11). Each consent is its own line with the box to the left;
   without this the three <label>s flow inline and read as one run-on sentence.
   External stylesheet, so it is allowed under style-src 'self' (an inline style
   would be dropped — see the note above). */
.consentRow { display: flex; align-items: flex-start; gap: 8px; margin: 8px 0; }
.consentRow input { margin-top: 3px; flex: 0 0 auto; }

/* ------------------------------------------------------------- the app picker
   ONE OVERLAY, FIVE CALLERS (index.html's #pickOverlay). The panel is the same
   440px column the rest of the page is, capped at 80vh with the LIST as the
   only thing that scrolls — a filter box that scrolls off the top is a filter
   box a parent stops using at row forty, which is exactly the size of list this
   control exists for.

   No inline styles anywhere: the page is served under `style-src 'self'` and a
   style="" attribute is dropped silently by the browser, which is how the
   egress control below once shipped flush against the card above it. */
.pickwrap {
  position: fixed; inset: 0; z-index: 40; display: flex; align-items: flex-end;
  justify-content: center; background: rgba(9,16,29,.72);
  padding: max(18px, env(safe-area-inset-top)) 12px max(18px, env(safe-area-inset-bottom));
}
.pickpanel {
  background: var(--dusk); border: 1px solid var(--line); border-radius: 21px;
  padding: 18px; width: 100%; max-width: 440px; max-height: 84vh;
  display: flex; flex-direction: column; box-shadow: 0 24px 60px rgba(0,0,0,.45);
}
.pickpanel .toprow { margin-bottom: 10px; }

.device-menu-panel { max-width: 560px; }
.device-menu-panel .toprow .who .id {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  line-height: 1.35;
}
/* GoodQA 427 — .pickpanel (this panel's own class, above) caps at max-height:
   84vh as a flex column, exactly like the app picker's .pickpanel does, but
   unlike .picklist below, THIS grid had no overflow/sizing rule of its own —
   at a phone width, 11 actions plus the header well exceed 84vh, and with no
   overflow-y anywhere the excess just rendered off the bottom of the
   viewport instead of scrolling: "More settings" and "Remove device" were
   never reachable at 320x568 or 375x667, cursor-scroll and a scrollTop write
   both did nothing (overflow-y stayed 'visible'). flex/min-height/overflow-y
   here are the identical three properties .picklist already uses to solve
   this for the app picker's own list. */
/* ONE SPACING SCALE FOR THE WHOLE DRAWER — 2026-09-21, owner brief 8 ("Gaps
   between headings, fields, cards, and sections vary unpredictably. Use one
   spacing scale and one declared vertical gap for equivalent relationships.")
   MEASURED BEFORE, and identical at 320/375/768/1440: the real distance
   between consecutive top-level blocks took THREE values — 8px, 14px and
   16px — from three sources that never knew about each other. The grid's own
   `gap: 8px`; `.ghost.wide`'s `margin-top: 8px`, which applies to the BUTTON
   rows and so gave 8+8=16 between two buttons but only 8 between a button and
   a `.dqa` block (a <div>, not a .ghost); and `.device-menu-section`'s
   `margin: 6px 2px 0`, giving 8+6=14 above a heading. Declared once here and
   consumed below, so "two rows inside a section" and "a heading and whatever
   is above it" each have exactly one answer, on one scale. */
.device-menu-panel {
  --dm-gap: 10px;         /* between sibling rows and cards */
  --dm-section-gap: 10px; /* ADDED above a section heading, on top of --dm-gap */
}
.device-menu-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--dm-gap);
  margin-top: 12px;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  -webkit-overflow-scrolling: touch;
  /* STABLE INNER PADDING, AND NO CONTROL UNDER THE SCROLLBAR — owner brief 9
     ("Content runs too close to the scrollbar and bottom edge... The drawer
     needs stable inner padding, with no controls underneath the scrollbar").
     `scrollbar-gutter: stable` reserves the thin scrollbar's own track in
     BOTH states, so the two columns do not resize the instant the list grows
     past the fold. The bottom padding is this file acting on the note
     device-setup.css's own reverted mask-image fade left behind, verbatim:
     "give the scroll container bottom padding so a row is never bisected at
     rest, rather than fading the rows themselves". The 3px on the remaining
     sides is the FOCUS RING's: `overflow-y: auto` forces overflow-x to
     `auto` as well, so the 2px `outline` with `outline-offset: 1px` that
     app.css gives every control was being clipped by this very box on the
     first, last and left-hand rows. */
  scrollbar-gutter: stable;
  padding: 3px 8px 14px 3px;
  /* THE ONE DECLARATION THAT MAKES EVERY ROW CONTENT-DRIVEN, AND THE REASON
     `display: flex` on the row below is not enough on its own.

     These rows are GRID ITEMS, and `align-items` here resolves to `normal`
     -> `stretch`. A stretched grid item is given its TRACK's height — and
     Blink sizes an `auto` track from a stretched item's contribution in a way
     that, for these rows, bottomed out at their 48px `min-height` floor
     instead of their real wrapped content. So `min-height` was silently
     acting as a FIXED height, which is precisely the owner's "Row height must
     be determined by content, not a fixed value based on shorter labels".

     MEASURED, by mutating this one property through the CSSOM on the live
     menu at 1440 and reading every row back (six-way matrix, kept in
     .scratch/device-menu-drawer/ — `dmdrawer-whyheight.mjs`):

       track sizing          picker  locks  pause  time  ragged  overflow  chip
       auto (before)             48     48     48    48       0      18px  -1.5
       align-items:start         78     64     49    71       1       0px   -13
       grid-auto-rows:min-content 78     64     64    71       0       0px   -13

     `align-items: start` also frees the height, but it un-stretches the cells
     and the two halves of a row stop matching (locks 64 beside pause 49) —
     which would re-introduce the ragged two-column rhythm a previous pass
     already fixed once for the chips. `min-content` tracks fix the
     measurement while LEAVING the stretch on, so both cells still fill the
     row: equal, non-overlapping cells AND content-driven height, which is
     exactly what the brief asks for and what the last column shows (zero
     overflow anywhere, and the chip back inside its button's 12px padding
     instead of 1.5px outside its border). */
  grid-auto-rows: min-content;
}
/* EVERY ROW IS A REAL FLEX CONTAINER, AND THAT IS THE WHOLE OF DEFECTS 1, 2
   AND 5 — ONE MECHANISM, NOT THREE. MEASURED on this build at 1440, the
   "Adjust remaining time" row (.scratch/device-menu-drawer/report-before.json
   and diag-os-dark.png):

     button height        48px   (used value, exactly its own min-height)
     button scrollHeight  55px   vs clientHeight 46px
     content actually wanted  69px  (label 19 + 4 + chip 20 + 24 padding + 2 border)
     the "OPENS A SCREEN" chip's bottom edge sat 8px BELOW the button's border

   A bare <button> is not a block container: Blink lays its children out in an
   anonymous, vertically-CENTRED box whose height does not feed the button's
   own intrinsic height the way a div's content does. So the row stayed on the
   48px floor `min-height` gave it, its two lines of content were centred
   inside a 22px content box, and the overflow spilled ~8px past the border at
   BOTH ends — straight into the 8px grid gap, i.e. precisely onto the
   neighbouring card. That single fact is the owner's first, second and fifth
   complaints at once: "controls visually sit on top of adjacent cards and
   section boundaries", "its layout row does not grow cleanly with it", and
   "the badge intersects the button border and crowds the label".

   `display: flex` replaces that anonymous box with a real flex container, so
   the row's height is its content's height again and `min-height` goes back
   to being a FLOOR (the 48px touch target) instead of a ceiling. Nothing here
   sets `height`, nothing is positioned absolutely, and no negative margin or
   z-index is involved — the brief's "normal document flow / content-driven
   height / remove fixed heights from ordinary form controls", applied at the
   one place that was breaking all three. */
.device-menu-action {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 6px;
  min-height: 48px;
  text-align: left;
}
/* The grid's own gap is the single declared vertical rhythm. `.ghost.wide`'s
   8px top margin is for a button standing alone at the foot of a card, and
   here it double-counts against that gap — see the --dm-gap note above. */
.device-menu-actions > .ghost.wide { margin-top: 0; }
/* THE STATEFUL DEVICE MENU (2026-09-19). Owner, on the live portal: "the
   device menu modal on each has no stateful display of each settging" — it
   was eleven identical buttons, none of which said what the setting it leads
   to is currently set to. A section heading spans the whole grid so the two
   kinds of row (what is true right now vs. where to go) stop reading as one
   undifferentiated list. */
.device-menu-section {
  grid-column: 1 / -1;
  /* PADDING, NOT MARGIN, and on one declared step. A margin here stacked on
     top of the grid's own gap for a total of 14px above a heading while two
     ordinary rows sat 8px apart and two button rows sat 16px apart — three
     values for what a reader sees as one relationship. The heading now owns
     its own separation: --dm-gap from the grid + --dm-section-gap of its own,
     so a section break is always exactly one step wider than a row break. */
  margin: 0;
  padding: var(--dm-section-gap) 2px 0;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--haze);
}
.device-menu-section:first-child { padding-top: 0; }
/* Label left, current value hard right — the value is the reason this row
   exists, so it gets the fixed edge and the label takes the slack. */
.device-menu-stateful {
  display: flex;
  /* `.device-menu-action` above now stacks a label and its badge in a COLUMN;
     a stateful row is the other shape — label left, current value hard right —
     so it names its own direction rather than inheriting one. */
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
/* Current-state controls are primary decisions, not half-width directory
   tiles. The two-column grid below still suits destination links, but a lone
   Pause or kiosk-lock row in its first column leaves a blank right cell and
   forces the long lock label into a tall, narrow stack. Give these direct
   controls the same full row already used by the mode picker and lock panel. */
#deviceMenuActions > button.device-menu-stateful { grid-column: 1 / -1; }
.device-menu-stateful > .dm-label { min-width: 0; flex: 1 1 auto; }
.device-menu-stateful > .dm-state { flex: 0 0 auto; margin: 0; }

/* THE ONE ROW WHOSE CONTROL IS A <select>, AND THE ONE CONTROL IN THIS MENU
   THAT NEVER GOT THE `min-width: 0` TREATMENT. MEASURED BEFORE at 1440:

     row     y 404.0  height 48
     select  y 402.0  height 52   <- 2px proud of its own row at the TOP and
                                     2px at the BOTTOM, at every width tested
     select  min-width: auto, flex: 0 1 auto, used width 345.98px

   `.inp`'s base `width: 100%` is a SPECIFIED size, so `min-width: auto` on a
   flex item resolves to it and the select cannot shrink at all: it holds the
   row open at its own full width and the label beside it is what gives way
   (at 320 the label "Parent mode or kid mode" overflowed its own row by
   6.5px). This is the identical failure `.dqa-form > .inp` already records
   and fixes over in device-setup.css — the base menu's own picker simply
   never got the same two lines.

   `flex-wrap` is the brief's "collapse multi-column groups at a breakpoint
   before their content becomes cramped", expressed as the CONTENT's own
   breakpoint rather than a guessed viewport one: the label and the select
   share a line while 200px is available for the select, and the select takes
   its own line when it is not. No media query can know that, because this
   panel's width barely tracks the viewport's (it is capped at 560px from
   584px upwards — measured: the two columns are 247px at 768 AND at 1440). */
.device-menu-picker { flex-wrap: wrap; row-gap: 8px; }
.device-menu-picker > .dm-picker {
  flex: 1 1 200px;
  width: auto;
  min-width: 0;
  margin: 0;
}
.devgroupactions { display: flex; justify-content: flex-end; padding: 8px 12px 0; }
.devgroupactions > button { min-height: 44px; }
@media (max-width: 620px) {
  .device-menu-actions { grid-template-columns: minmax(0, 1fr); }
}
.picklist { overflow-y: auto; margin-top: 10px; flex: 1; min-height: 0; -webkit-overflow-scrolling: touch; }

/* CONFIRM-DELETE MODAL (GoodQA #494/#495). Layered onto .pickpanel exactly
 * like .device-menu-panel above it does — a max-width tweak on the shared
 * shell, not a new one.
 *
 * `--danger` IS A NEW TOKEN, NOT YET DEFINED. OWNER-DECISIONS §0.4
 * (2026-09-18) rules a fifth semantic colour into existence — "danger/unsafe"
 * — alongside the four this file's :root already carries (cyan/mint/amber/
 * coral). The ruled light value is #C0342C/#FCE4E4; no dark-mode value has
 * been measured yet, which is exactly the token-layer pass's job, not this
 * ticket's. Every rule below reads `var(--danger, var(--coral))` so this
 * renders correctly today (falling back to the existing destructive hue)
 * and repaints itself the moment :root defines --danger — no follow-up edit
 * needed here when that pass lands. */
.cdm-panel { max-width: 420px; text-align: center; }
.cdm-panel .toprow { text-align: left; }
.cdm-badge {
  width: 52px; height: 52px; margin: 0 auto 12px; display: grid; place-items: center;
  border-radius: 50%; color: var(--danger, var(--coral));
  background: color-mix(in srgb, var(--danger, var(--coral)) 16%, transparent);
}
.cdm-badge .ds-icon { width: 26px; height: 26px; }
#cdmBody { text-align: left; }
.cdm-items {
  list-style: none; text-align: left; margin-top: 10px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: 14px; background: var(--dusk2);
  display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--haze);
}
.cdm-items li { padding-left: 16px; position: relative; }
.cdm-items li::before { content: "•"; position: absolute; left: 2px; color: var(--danger, var(--coral)); }
.cdm-check {
  display: flex; align-items: flex-start; gap: 10px; text-align: left; margin-top: 12px;
  font-size: 13px; color: var(--cloud); cursor: pointer;
}
.cdm-check input { margin-top: 2px; flex: none; }
.cdm-typewrap { text-align: left; margin-top: 12px; }
.cdm-danger {
  width: 100%; min-height: 50px; padding: 14px; font-size: 16px; margin-top: 16px;
  border-radius: 13px; border: 1px solid var(--danger, var(--coral));
  color: var(--danger, var(--coral));
  background: color-mix(in srgb, var(--danger, var(--coral)) 14%, transparent);
}
.cdm-danger:disabled { opacity: .5; cursor: default; }

/* A WHOLE-ROW TARGET, not a name with a checkbox somewhere to its right. This
   is a thumb on a phone choosing between forty near-identical rows, and §3's
   "touch-first, bigger targets" is the child surface's rule for a reason that
   applies just as hard to a parent standing in a kitchen. */
.pickrow {
  display: block; width: 100%; text-align: left; background: var(--dusk2);
  border: 1px solid var(--arowline); border-radius: 13px; padding: 11px 13px;
  margin-bottom: 8px; color: var(--cloud);
}
.pickrow .an { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.pickrow .asub { font-size: 11px; color: var(--haze); font-weight: 400; }
.pickrow[aria-pressed="true"] { border-color: var(--glow); }
.pickrow:disabled { opacity: .5; cursor: default; }

/* AMBER, AND ONLY EVER FOR THIS. UI-DIRECTION §3 reserves amber for "attention,
   a request waiting"; an app that carries the open web inside itself is the one
   fact on a picker row a parent must not skim past. Not coral — allowing it is
   a legitimate choice a parent makes every day, not a destructive one. */
.asub.web { color: var(--waiting-text); }

/* GoodQA fc23fc22: the one .asub that is a way OUT of the state it names —
   "blocked for this child" is one click from Apps & programs, not a dead
   end. Button-reset because .asub is a <div> everywhere else in this file;
   this is the sole place it has to act like the text it looks like. */
.asub.action {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-decoration: underline;
  cursor: pointer; display: block; text-align: left;
}
.asub.action:hover, .asub.action:focus-visible { color: var(--glow); }

/* The advanced disclosures — "Add by identifier", "Block something that is not
   on this list". Deliberately quiet: the picker above each of them is the way
   in, and a disclosure that competes with the primary button for attention has
   undone the point of building the picker. */
.adv { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
/* A SUMMARY IS A CONTROL, AND THIS ONE WAS 17px TALL. `.adv` is the disclosure
   vocabulary app.css defines for index.html and js/overview.js alike, and the
   rule set its type and its marker but never its size — so the row a parent
   presses to open "Why a card can say 'Not reported'" was exactly the
   line-height of 12px text. MEASURED at all seven widths in all three
   languages: 17px tall, up to 1524px wide.
   That fails WCAG 2.5.5 at 44px, and it also fails 2.5.8 — the 24x24 MINIMUM
   at AA in WCAG 2.2 — so it is not merely short of an enhanced target. The
   inline-target exception does not apply: this is a standalone full-width row,
   not a link inside a sentence.
   `flex` with centred items rather than a bare `min-height`, because the
   `::before` marker and the label would otherwise sit at the top of a taller
   box; `gap` replaces the trailing space the marker's content used to carry. */
.adv > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  font-size: 12px;
  color: var(--haze);
  cursor: pointer;
  list-style: none;
}
.adv > summary::-webkit-details-marker { display: none; }
.adv > summary::before { content: "▸"; }
.adv[open] > summary::before { content: "▾"; }
.adv .inp { margin-top: 8px; }

/* ------------------------------------------------------- requests inbox
 * GoodQA 13d59ad9 Frame 5. Appended at the end of this file on purpose —
 * additive, Requests-screen-scoped selectors only, to keep a merge with any
 * concurrent work on another screen (e.g. the Home/Overview redesign) a
 * clean append on both sides rather than an interleaved diff.
 *
 * ONE BORDERED CARD PER PENDING REQUEST, not the thin single-line `.arow`
 * this row used before the redesign — Frame 5 asks for a card, and a request
 * here carries THREE decisions (Allow / Allow scoped / No), one more than
 * `.arow` was built for. Same tokens as everywhere else on this page (no
 * second palette): `--dusk2`/`--arowline` are exactly what `.arow` itself
 * uses, and 14px matches this file's own radius language for a NESTED
 * element inside a 24px `.card` (see `.inp`/`.pairout`, both 14px/10px). */
.reqcard {
  background: var(--dusk2); border: 1px solid var(--arowline); border-radius: 14px;
  padding: 13px 14px; margin-bottom: 10px;
}
.reqhead .an { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.reqhead .asub { font-size: 11px; color: var(--haze); font-weight: 400; margin-top: 2px; }
.reqhead .asub:first-of-type { margin-top: 4px; }
/* Three buttons, and — the same lesson `#profList .arow`'s own comment
   records for its third button — a single flex line cannot take a third
   control without crushing the text beside it. This row is ALREADY its own
   line under the card's header rather than sharing one, so wrapping here is
   the graceful case (two buttons this line, one the next on a narrow phone),
   never the crushed one. */
.reqactions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
/* AN APP REQUEST'S THREE DECISIONS — a COLUMN, where `.reqactions` above is
   a wrapping row. Its own class rather than a `:has()` variant of that one, so
   the layout does not depend on selector support this file otherwise never
   relies on.
   Two of the three actions are called "allow", and the sentence under each is
   what tells them apart ("opens for 15 minutes" vs "stays on your child's
   list"). That sentence does not survive being squeezed into a third of a
   phone's width, so each button takes the full line and its consequence sits
   directly beneath it rather than in a shared paragraph a parent has to map
   back onto three controls. The extra vertical distance between the two
   allows is a second, deliberate benefit: "always" is the tap least worth
   making by accident. */
.reqdecisions { display: flex; flex-direction: column; align-items: stretch; gap: 12px; margin-top: 10px; }
.reqdecision { display: flex; flex-direction: column; gap: 4px; }
.reqdecision > .asub { font-size: 11px; color: var(--haze); line-height: 1.45; }
/* The Answered card's rows are read-only history, not a decision waiting —
   very slightly receded so the pending card above it stays the thing a
   parent's eye lands on first. */
.reqcard.answered { opacity: .82; }

/* GoodQA #445/#446/#448 (2026-09-16 handoff, epic #437). The
   Pending/Approved/Denied/All filter row reuses `.chips`/`.chip` verbatim
   (Activity's own filter chips two screens over use the exact same pair),
   so no new chip styling lives here — only the row/card visibility toggle
   applyRequestsFilter() sets, and the empty-state card's own layout.

   `.requests-filtered` is a class OF ITS OWN, never `.hide` — `.hide` is
   already the flag renderOpenAsks/renderAnsweredAsks/renderAsksHistory/
   renderHelpAsksHistory use for "this card's own data says there is nothing
   to show", and toggling that same class from the filter would erase which
   of the two reasons is why a card is hidden, so a data refresh that clears
   `.hide` could not tell whether the filter still wants it hidden. Both
   classes share the exact same declaration for the same reason `.hide`
   itself is `!important`: nothing else on this row may win against either. */
.requests-filtered { display: none !important; }

/* The empty-state card (RequestsEmpty.dc.html, PAGE_SPEC Page 43) — a
   centred icon, headline and one explaining line, same shape `.card` already
   gives every other panel on this screen. `--haze` is this file's own muted/
   secondary text colour (`.asub`, `.help`, `.ghost`'s default label) and
   `--dusk2` is `.arow`/`.reqcard`'s own neutral tile fill, so the icon reuses
   tokens rather than inventing a third neutral. */
.requests-empty-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; border-radius: 50%; background: var(--dusk2);
  color: var(--haze); margin: 8px auto 18px;
}
#requestsEmptyCard { text-align: center; padding: 40px 24px; }
.requests-empty-title { font-size: 16.5px; font-weight: 800; margin-bottom: 6px; }
.requests-empty-sub { font-size: 13.5px; color: var(--haze); max-width: 360px; margin: 0 auto; }

/* The one survivor of the Packet 5 authoring block. The rest of it
   (`.packet5-grid`, `-question`, `-choice`, `-history-title`, `-history-more`,
   `-message`) styled the check-in composer and quiz builder js/requests.js used
   to draw a SECOND time on the Requests screen; that surface is gone (see
   js/requests.js's own header) and its rules went with it, because a selector
   nothing emits is a rule that cannot be checked by looking at the product.
   This one stays: the per-child Request-handling card still labels every
   control with it. */
.packet5-field { display: block; margin: 12px 0; }
.packet5-field > .fld, .packet5-field > .fineprint { display: block; margin-bottom: 5px; }

/* ------------------------------------------------------- websites & categories
 * GoodQA 13d59ad9 Frame 3. Appended at the end of this file, the same
 * convention Frame 5's requests-inbox block above uses — additive,
 * #sites/#categories-scoped selectors only, so a merge with concurrent Apps
 * (Frame 2) / Time (Frame 4) work on this same file is a clean append on
 * every side.
 *
 * Reuses this page's existing tokens throughout: --mint/--coral/--amber for
 * good/attention/blocked states (the same meaning `.pill.on`/`.pill.off`/
 * `.pill.wait` already give them), --glow for the "selected"/focus state
 * `.chip[aria-pressed]` already uses, --dusk2/--line/--arowline for
 * surfaces and borders. No new palette, no new hex codes. */

/* The device-confirmed status line ("Web filter on · working") both cards
 * show — see renderWebFilterStatusLine()/childWebFilterConfirmed() in
 * app.js. Undecorated/neutral by default (the 'checking'/'none' states,
 * neither of which is bad news on its own), coloured only for the two
 * states that ARE a claim: confirmed (mint — "protection is on", `.pill.on`'s
 * own colour) or unconfirmed (amber — "still waiting", never coral; a
 * tablet that has not confirmed yet is not a failure, just not proven). */
.statusline { font-size: 12px; font-weight: 600; color: var(--haze); margin: 4px 0 10px; }
.statusline-good { color: var(--done-text); }
.statusline-warn { color: var(--waiting-text); }

/* The letter-monogram "favicon" — hostAvatar() in app.js, deliberately NOT a
 * real favicon fetch (see that function's own comment). A fixed-size circle
 * so it sits inline with `.arow`'s existing flex row (gap: 12px) without
 * changing that row's own layout rules — it renders on the Websites card AND
 * the Browsing History card on the policy screen, since both share `.arow`
 * via siteRow(). */
.favicon {
  flex: none; width: 28px; height: 28px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: #fff;
}

/* ------------------------------------------------------ .avatar / .badge
 * FOR OTHER TRACKS. Neither class has a caller yet — added here, ahead of
 * need, as the reusable vocabulary this design-v2 pass (see the DESIGN
 * TOKENS v2 comment at the top of this file for the source) asks every
 * track to reach for instead of inventing its own circle-with-initials or
 * its own count pill. style-vocabulary.test.js only fails on a class markup USES that the
 * stylesheet lacks, never on one the stylesheet defines and nothing yet
 * wears, so declaring these ahead of a consumer is safe.
 *
 * .avatar — a circular initials mark for a PERSON (a household member, a
 * co-parent, a support-thread sender — anything that is not a child's own
 * profile switcher, which stays `.face`/`.fi` above: PARENT-APP-SURFACES
 * §2.1 already reserves that rounded-square shape for "which profile a
 * DEVICE is running" specifically, and this circle must never be
 * confusable with it). Two lines of markup: `<span class="avatar"
 * data-tone="…">LM</span>`, initials as the text content.
 *
 * DETERMINISTIC TONE, NEVER A NAME. The reference mockup paints exactly one
 * child's avatar by name (`.face.maya`) — a class that works for exactly one
 * household and breaks the moment a second family has a Maya, or the first
 * one renames herself. `data-tone` is a small closed set (1-5 below) the
 * CALLER picks with a stable hash of something that outlives a rename — a
 * profile id, an account id, never the display name — e.g.
 * `1 + hashCode(profileId) % 5`. Two children who happen to share a name
 * still get two different tones (different ids); one child who gets renamed
 * keeps the same tone (same id). No CSS here ever names a person. */
.avatar {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ui); font-weight: 800; font-size: 13px; letter-spacing: .2px;
  color: var(--ink); background: var(--dusk2);
}
.avatar.sm { width: 28px; height: 28px; font-size: 11px; }
.avatar.lg { width: 46px; height: 46px; font-size: 16px; }
/* Five tones, one two-stop gradient each, built only from this file's own
   accent tokens (no new hex) — the same "warm gradient per person" motif the
   reference mockup uses, generalised past one hardcoded name. */
.avatar[data-tone="1"] { background: linear-gradient(135deg, var(--glow), var(--sky)); }
.avatar[data-tone="2"] { background: linear-gradient(135deg, var(--mint), var(--glow)); }
.avatar[data-tone="3"] { background: linear-gradient(135deg, var(--amber), var(--coral)); }
.avatar[data-tone="4"] { background: linear-gradient(135deg, var(--sky), var(--coral)); }
.avatar[data-tone="5"] { background: linear-gradient(135deg, var(--mint), var(--amber)); }
/* WHITE INITIALS ON A TONED AVATAR IN LIGHT, and this is a measurement, not a
   taste call. The five tones are built from the accent tokens, and in LIGHT
   every one of those is a darkened, text-safe value (--mint #13815C, --coral
   #D4483D, --glow #087DA9, --sky #5D4DD1) — so `.avatar`'s `color: var(--ink)`
   was dark ink on a dark gradient. Measured in a real browser before this rule
   existed: 3.11 (sky) to 3.87 (amber), every tone below AA.
   RE-DERIVING --amber made that worse on tones 3 and 5 specifically — the amber
   stop went 3.87 -> 3.01 — which is what surfaced it, so it is fixed here
   rather than left as collateral. White measures 4.38 (coral) to 7.13 (amber)
   against the same stops: better on all five, and the amber stop this pass
   moved is now the BEST of them instead of dragging.
   NOT AA EVERYWHERE, AND SAYING SO: the coral stop is 4.38, still 0.12 under
   the 4.5 an 11-16px label needs. That is a `--coral` derivation question, not
   an amber one, and it is not this slice's to answer. These initials are
   `aria-hidden="true"` decoration duplicating the name printed beside them.
   `[data-tone]` ONLY: a toneless `.avatar` keeps `--dusk2` behind it, and white
   on that pale fill would be unreadable. */
:root[data-theme="light"] .avatar[data-tone] { color: #FFFFFF; }

/* .badge — a small count pill, for a nav entry or a section heading that
 * needs to say how many without a sentence ("Requests · 3"). Neutral by
 * default (reuses the exact look `.devgroupcount` above already has, so a
 * plain count never claims more than "here is a number"); `.badge.attn`
 * is the ONE colour this ever takes — amber, UI-DIRECTION-2026-08-22 §3's
 * "waiting for the parent" — for a count that means specifically "N things
 * need you," such as the Requests nav entry's pending count. Never coral or
 * mint: a badge is a fact about quantity, not a health or danger claim, and
 * those two colours are reserved for exactly those (§3, and the ATTENTION
 * comment on --amber at the top of this file). NEVER FABRICATE: whatever
 * number a caller puts in a `.badge` must be the length of the same real
 * list the count claims to summarise, not a separately-maintained tally
 * that can drift from it — see .devgroupcount's own caller in app.js for
 * the existing pattern this one is meant to match. */
.badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  background: var(--dusk2); color: var(--haze);
  font-size: 11px; font-weight: 800; line-height: 1; flex: none;
}
.badge.attn { background: rgba(255,196,75,.18); color: var(--waiting-text); }

/* The dashed add-row — the ticket's own mockup note names this shape
 * ("dashed add-row with [Block][Allow]"). Dashed, not `.card`'s solid
 * border, so it reads as "type here to add a new one" rather than as
 * another finished rule sitting in the list above it. */
.addrow-dashed {
  border: 1.5px dashed var(--line); border-radius: 14px; padding: 12px;
  margin-top: 10px; display: flex; flex-direction: column; gap: 8px;
}
.addrow-dashed .inp { margin: 0; }
.addrow-actions { display: flex; gap: 8px; }
.addrow-actions .ghost { flex: 1; }

/* Typed website-rule builder. The candidate cards keep the full canonical
   address visible and the two actions large enough for the 390px phone lane;
   on wider parent screens the repeated fields form a compact two-column grid. */
.sites-builder { margin-top: 16px; }
.sites-builder-field { margin-top: 12px; }
.sites-results-title { margin-top: 20px; }
.sites-candidates { display: grid; gap: 10px; }
.sites-candidate {
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 14px;
  padding: 13px; min-width: 0;
}
.sites-candidate-head { display: flex; align-items: flex-start; gap: 10px; }
.sites-candidate-names { flex: 1; min-width: 0; }
.sites-candidate-names strong, .sites-candidate-names span {
  display: block; overflow-wrap: anywhere;
}
.sites-candidate-names .fineprint { margin-top: 2px; }
/* THE CERTAINTY LINE — the chip and the sentence that replaced
   "{confidence}% confidence · {source}" on every candidate (js/sites.js's
   CERTAINTY comment carries the owner report and the measurement).

   `flex-wrap` and `min-width: 0` are not decoration. The chip is `.pill`,
   which is `white-space: nowrap; flex: none`, and the sentence beside it is a
   full sentence, so at the 320px width sites-ui.test.js measures for a
   horizontal scrollbar an unwrapped row would be the thing that produces one.
   Wrapping puts the sentence under the chip instead; `min-width: 0` is what
   lets the paragraph shrink inside the flex line at all. The `.fineprint`
   margin is dropped because it is a row here, not a paragraph after a block. */
.sites-candidate-certainty {
  display: flex; align-items: flex-start; flex-wrap: wrap;
  gap: 8px; margin-top: 10px;
}
.sites-candidate-certainty .fineprint { margin-top: 0; flex: 1 1 14ch; min-width: 0; }
/* `.pill.neutral`'s fill IS `--dusk2` (app.css:911) and `.sites-candidate`'s
   background is ALSO `--dusk2` — so the chip on this card would be a chip with
   no edge, which is the whole reason `.child-headtop > .pill.neutral` already
   carries this exact border. Same fix, same token, same reason: the certainty
   label has to look like a label, or the sentence beside it reads as the only
   content and the scan-down-a-list-of-five case loses its anchor. */
.sites-candidate-certainty .pill.neutral { border: 1px solid var(--line); }
.sites-builder-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.sites-builder-actions .ghost { min-width: 0; text-align: center; }
.sites-check {
  display: flex; align-items: center; gap: 9px; margin-top: 10px;
  color: var(--cloud); font-size: 14px; cursor: pointer;
}
.sites-check input { width: 20px; height: 20px; accent-color: var(--glow); }
.sites-ask-policy {
  border: 1px solid var(--line); border-radius: 14px; padding: 12px;
  margin: 14px 0 0; min-width: 0;
}
.sites-ask-policy .sect { padding: 0 6px; margin: 0; }
.sites-preview {
  background: color-mix(in srgb, var(--glow) 8%, var(--dusk2));
  border: 1px solid color-mix(in srgb, var(--glow) 30%, var(--line));
  border-radius: 14px; margin-top: 14px; padding: 13px;
}
.sites-preview p { margin: 5px 0 0; overflow-wrap: anywhere; }

@media (min-width: 680px) {
  .sites-ask-policy { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
  .sites-ask-policy .sect { grid-column: 1 / -1; }
}

/* The 3-level filter (Relaxed / Standard / Strict) — a segmented control
 * built from three `.chip` buttons (the same base look every other chip on
 * this page already has) laid out as equal columns rather than a wrapping
 * row, since there are always exactly three and never more. */
.catlevel { display: flex; gap: 8px; margin: 4px 0 8px; }
.catlevelbtn { flex: 1; text-align: center; }

/* Category chips — categoryRow() in app.js. THE INPUT IS STILL A REAL
 * CHECKBOX (see that function's own comment for why); it is visually hidden
 * and its sibling `.catchip-pill` is painted from the checkbox's OWN
 * `:checked` state, which is what lets the existing test DOM shape
 * (`input[type=checkbox][data-category]`) stay unchanged under a chip
 * restyle. `:focus-visible` on the (hidden) input still outlines the pill,
 * so keyboard/switch-access navigation is never silently lost the way it
 * would be if the checkbox were `display:none` instead of visually hidden.
 *
 * OVERLAID, NOT SHRUNK TO 1px. A first version used the classic "clip to
 * 1x1px" visually-hidden recipe, and categories-ui.test.js's own
 * toggleCategory() — which clicks the INPUT directly, by design, because
 * that is what a real assistive-tech interaction also targets — timed out:
 * a 1px box positioned by normal flow (no `position:relative` on the
 * label) can land outside the scrollable list entirely, and even where it
 * doesn't, the visible `.catchip-pill` sitting on top of it intercepts the
 * click. `inset:0` on a `position:relative` label makes the invisible
 * input's clickable area EXACTLY the visible pill's box, so a click on
 * either lands on the same element real assistive tech would target. */
.catchip { display: inline-flex; position: relative; margin: 0 6px 6px 0; cursor: pointer; }
.catchip-input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0;
  opacity: 0; cursor: pointer;
}
.catchip-pill {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); background: var(--dusk2); color: var(--haze);
  border-radius: 20px; padding: 9px 13px; font-size: 13px; font-weight: 600;
  min-height: 44px; box-sizing: border-box;
}
/* CORAL FOR CHECKED, matching `.pill.off`'s own reasoning at the top of this
 * file: a checked chip means this category IS blocked, and coral is already
 * this page's colour for "the direction that costs the child something". */
.catchip-input:checked + .catchip-pill {
  background: rgba(255,107,94,.18); color: var(--coral); border-color: var(--coral);
}
.catchip-input:focus-visible + .catchip-pill { outline: 2px solid var(--glow); outline-offset: 2px; }
/* `adult` disabled by categoryRow() (app.js) — the checkbox can never be
 * un-checked, so it reads visibly locked rather than looking like every
 * other, interactive chip. Same dimmed-and-not-allowed treatment
 * policy-workspace.css already uses for a disabled state control
 * (`.pw-state-option:disabled { cursor: wait; opacity: .58 }`), not a new one. */
.catchip-input:disabled { cursor: not-allowed; }
.catchip-input:disabled + .catchip-pill { opacity: .58; cursor: not-allowed; }
.catchip:has(.catchip-input:disabled) { cursor: not-allowed; }
.catchip-icon { font-size: 15px; line-height: 1; }

/* GoodQA 13d59ad9 Frame 3 fix: a site row carrying `sites.notCarriedAllow`/
 * `notCarriedScope` (an allow rule, or a scoped block — see siteRow()'s own
 * comment) has THREE stacked `.asub` lines instead of one, and this ticket's
 * "Always allowed" relabel (was "Allowed") widened `.pill.on` from ~59px to
 * ~97px — together enough to squeeze `.an` (flex:1; min-width:0) down to a
 * genuinely unreadable ~78px, wrapping "this site and every address on it"
 * and the notCarried sentence into a near-vertical ribbon. MEASURED live
 * (`.an`'s own computed width, not eyeballed): 335px row, favicon 28px +
 * pill 97px + button 69px + gaps leaves .an only 78px of the 335. Same root
 * cause as the Apps redesign's own flex-squeeze fix two sections up in this
 * file (a row with more content than `.arow`'s original two-fixed-siblings
 * shape was sized for) — same remedy: wrap the row so the pill and Remove
 * button drop to their own line instead of stealing width from the text
 * column that actually needs it. Scoped to the two lists that render
 * siteRow() output (the Sites card and the Browsing History card's own
 * rules list, which reuses siteRow() — see URLHIST_RULES_CTX's comment). */
#sitesList .arow, #urlhistRulesList .arow { flex-wrap: wrap; }
#sitesList .arow .an, #urlhistRulesList .arow .an { flex-basis: 200px; }

/* -------------------------------------------------- Time (daily limit + week)
 * GoodQA 13d59ad9 Frame 4. Appended at the end of this file on purpose, same
 * reasoning the Requests-inbox block above already states: additive,
 * Time-scoped selectors only (`.dial*`, `.week*`, `#timeWeekCard`), so a
 * merge with concurrent work on the Apps/Web/Sites/Categories screens is a
 * clean append on both sides rather than an interleaved diff.
 *
 * NO NEW COLORS. `.weekBand.pause` and `.weekBand.restrict` reuse
 * `--coral`/`--glow` — this file's own UI-DIRECTION-2026-08-22 §3 comment
 * near :root already states the mapping ("coral is destructive/paused,
 * cyan/blue is trusted+primary"): Resting/bedtime IS a pause, and Homework
 * mode is the more assertive, primary-feeling of the two real schedule
 * actions. Every other new rule below reads `--line`/`--dusk`/`--dusk2`/
 * `--haze`/`--ink`, the same tokens `.card`/`.chip`/`.arow` already use.
 *
 * CONTAINER QUERY, NOT A VIEWPORT ONE. At `min-width:900px` `.screen` above
 * becomes a 2-column CSS multi-column layout (`columns:2`), so `#timeWeekCard`
 * — a `.screen > .card` direct child, `break-inside:avoid` already covers it
 * — renders at roughly HALF of `.wrap`'s width, not the viewport's. A
 * `@media` breakpoint keyed on viewport width would flip this card to a
 * side-by-side layout while it is still column-narrow on a laptop, which is
 * exactly the "nothing failed; the page just became unreadable" class of bug
 * the sidebar rule's own comment two sections up describes. A container
 * query answers the question this card actually needs answered: how wide is
 * MY OWN box right now.
 */
#timeWeekCard { container-type: inline-size; }
.weekCols { display: flex; flex-direction: column; gap: 22px; }
@container (min-width: 620px) {
  .weekCols { flex-direction: row; align-items: flex-start; }
  .weekColLeft { flex: 0 0 260px; }
  .weekColRight { flex: 1 1 auto; min-width: 0; }
}

.dialWrap {
  display: flex; flex-direction: column; align-items: stretch; gap: 9px;
  padding: 16px; border: 1px solid var(--line); border-radius: 16px;
  background: var(--dusk2);
}
.dial {
  width: 100%; height: 30px; margin: 0; cursor: pointer;
  accent-color: var(--glow);
}
.dial:focus-visible { outline: 2px solid var(--glow); outline-offset: 4px; }
/* var(--cloud) is already this file's own theme-aware body text colour
   (see `body { color: var(--cloud) }` above) — no override needed here. */
.dialReadoutHtml {
  font-size: 22px; line-height: 1.15; font-weight: 780;
  color: var(--cloud); text-align: left; font-variant-numeric: tabular-nums;
}
.timeLimitScale {
  display: flex; justify-content: space-between; margin-top: -7px;
  color: var(--haze); font-size: 10px; font-weight: 650;
}
.timeLimitPresets {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px; margin: 2px 0 0;
}
.timeLimitPresets .chip { min-width: 0; padding-inline: 8px; }
.timeQuickActions { margin-top: 10px; }
#timeWeekBonusLink { width: 100%; margin-top: 8px; }

.weekSectionHead { display: flex; align-items: center; gap: 10px; margin-bottom: 2px; }
.weekSectionHead > .fld { flex: 1; margin: 0; }
.weekSectionHead .ghost { display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
.weekGridFrame { min-width: 0; }
.weekHours {
  display: grid; grid-template-columns: repeat(3, 1fr);
  padding: 0 2px 3px 38px; color: var(--haze);
  font-size: 9px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.weekHours span:nth-child(2) { text-align: center; }
.weekHours span:last-child { text-align: right; }

.weekGrid { display: flex; flex-direction: column; gap: 6px; }
.weekRow { display: flex; align-items: center; gap: 8px; }
.weekDayLabel { flex: 0 0 30px; font-size: 11px; color: var(--haze); font-weight: 600; }
.weekTrack {
  position: relative; flex: 1 1 auto; height: 34px; background: var(--dusk2);
  border: 1px solid var(--line); border-radius: 8px; touch-action: none; cursor: crosshair;
  overflow: hidden;
}
/* pointer-events:none — purely decorative, and without this a drag that
   happens to start on the exact pixel of an hour line would hit the tick
   instead of the track underneath it (wireTrackCreate's own `ev.target !==
   track` guard would then silently refuse to start a create-drag there). */
.weekTick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line); opacity: .6; pointer-events: none; }

.weekBand {
  position: absolute; top: 3px; bottom: 3px; border-radius: 6px;
  display: flex; align-items: center; overflow: hidden; cursor: pointer;
  min-width: 6px;
}
.weekBand.pause { background: color-mix(in srgb, var(--coral) 32%, transparent); border: 1px solid var(--coral); }
.weekBand.restrict { background: color-mix(in srgb, var(--glow) 30%, transparent); border: 1px solid var(--glow); }
.weekBand.selected { outline: 2px solid var(--cloud); outline-offset: 1px; }
.weekBand.locked { cursor: default; opacity: .82; }
.weekBand.ghost { background: color-mix(in srgb, var(--haze) 35%, transparent); border: 1px dashed var(--haze); }
.weekBandLabel {
  /* var(--cloud), NOT var(--ink): a band's fill is a translucent coral/glow
     tint OVER .weekTrack's --dusk2 background (color-mix at 30-32%), so the
     effective surface stays dark in dark theme — --ink is near-black and
     made this unreadable in that theme, same "text and background from two
     different tokens" mistake .dialReadoutHtml's own comment two rules up
     exists to avoid. */
  font-size: 10px; font-weight: 600; color: var(--cloud); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; padding: 0 6px; pointer-events: none;
}
.weekHandle {
  position: absolute; top: 0; bottom: 0; width: 8px; cursor: ew-resize;
}
.weekHandle.left { left: 0; }
.weekHandle.right { right: 0; }

.weekSelection { margin-top: 10px; padding: 12px; }
.weekCreatePop {
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 10px;
  padding: 12px; margin: 4px 0 10px; display: flex; flex-direction: column; gap: 8px;
}

/* 44px, NOT 34. MEASURED across the portal at 320-3840 in en/es/fr: every
   `.ghost.small` a parent can press rendered 34px tall — #activityRefreshBtn
   ("Actualizar"/"Actualiser") and all four `.preference-destination-open`
   buttons among them. `.arow.procrow .ghost.small` already carried a local
   `min-height: 44px` override, so the fix was known and had been applied to
   exactly one of the places that needed it; this moves it to the class that
   defines the control. The padding rises with it so the label keeps its
   optical centre instead of floating at the top of a taller box. */
.ghost.small { padding: 10px 12px; font-size: 12px; min-height: 44px; }

/* Frozen during a write, on the CARD rather than on each control. The
   `if (W.busy) return` guard at the top of each drag-start handler is the
   enforcement for the timeline; this is the visible half of it. */
#timeWeekCard.busy .dial,
#timeWeekCard.busy .weekTrack,
#timeWeekCard.busy .weekBand { pointer-events: none; opacity: .6; }

/* The timeline is useful with pointer room, but it becomes seven dense rows
   at phone width. The exact schedule list and its Add button stay visible;
   phones use the same full editor in a modal instead of a fragile drag target. */
@container (max-width: 520px) {
  .weekGridFrame { display: none; }
  .weekColRight > .help { margin: 7px 0 0; }
}

/* --------------------------------------------------------------- activity
 * GoodQA 13d59ad9 Frame 7. Appended at the end of this file, the same
 * convention Frame 3/Frame 5's own blocks above use — additive, #activity-
 * scoped selectors only, so a merge with concurrent work on this same file
 * is a clean append on every side.
 *
 * Reuses this page's existing tokens throughout: `.chip`/`.chips` (already
 * defined above) for the child/type filter rows, `.arow`/`.an`/`.asub` for
 * each event row — the exact vocabulary the Household-activity ledger
 * already uses for the identical shape ("one sentence, one timestamp
 * underneath"). No new palette, no new hex codes: `.actday` below is the
 * one genuinely new element (a day-group heading), styled off `--haze` and
 * `--line`, both already this page's own tokens. */
.actday {
  font-size: 12px; font-weight: 700; color: var(--haze); text-transform: uppercase;
  letter-spacing: .04em; margin: 18px 0 8px; padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.actday:first-child { margin-top: 4px; }

/* ------------------------------------------------- maintenance-grant-system */
/* GoodQA 13d59ad9 Frame 6 — the Devices card's extra facts (Protected chip,
   model, battery) and the two new per-device actions ([See screen],
   [Open setup window]), plus the Setup-window modal. Every color reuses the
   existing --mint/--coral/--amber/--glow vocabulary (UI-DIRECTION-2026-08-22
   §3) rather than a new one, and .pill/.chip/.card/.primary/.ghost/.inp are
   the SAME classes the rest of the page already uses — nothing here invents
   a second design system. */

/* The devlock column already stacks buttons in a flex column (app.css:442);
   these two ride the same column, so [Pause] [See screen] [Open setup
   window] read as one action stack rather than a bolted-on extra row. */
.devactionbtn {
  width: 100%; min-height: 40px; padding: 8px; border: 1px solid var(--line);
  color: var(--cloud); background: var(--track); font-size: 12px; font-weight: 700;
}
.devactionbtn.setup { color: var(--glow); border-color: rgba(101,216,255,.5); background: rgba(101,216,255,.13); }
:root[data-theme="light"] .devactionbtn.setup { color: #0A6E8C; }

/* Model/battery appended to the existing .devsub line — same font, same
   --haze color, no new visual weight. See renderDevices() in app.js. */
.devmeta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* A CAUTION ON ONE LINE, where a `.banner warn` box used to sit.
   The dev recovery PIN's warning ("typed on the tablet, this PIN ENDS
   supervision") was drawn as a full bordered banner INSIDE each device card —
   three of them stacked down a three-tablet screen, each box taller than the
   card content it annotated, which is most of why that screen scrolled. The
   warning itself is not negotiable, so this keeps the amber and drops the box:
   same words, same colour, one line, at `.devsub` weight so it sits with the
   other secondary facts on the card instead of interrupting them. Used for the
   PIN's own "unconfirmed" note too, which is the same kind of caution. */
/* NO LIGHT-THEME OVERRIDE, and that is checked rather than assumed: the
   `[data-theme="light"]` block darkens `--amber` to #7A4F00 for exactly this
   problem. RE-MEASURED 2026-09-06 in a real browser, worst pixel under the
   glyphs: 7.13:1 on white, 6.39:1 on --dusk2 and 6.25:1 on --bg — AA with
   margin at every background a device card sits on. A second hardcoded literal
   here would be a colour nobody could find from the token.
   THE NUMBERS THAT USED TO BE ON THIS COMMENT (4.94/4.67/4.55, for #9B6500)
   WERE STALE, and that is the whole lesson: they were measured against
   --dusk2/--bg as they stood before the light-theme live polish pass of
   2026-09-04 darkened both, after which the same colour measured 4.42 and 4.33 and this
   line was quietly failing AA. A comment carrying a number has to be re-run
   when the ground under it moves. */
.devsub.warnnote { color: var(--waiting-text); }
/* THE OTHER TWO `.warnnote` SHAPES, WHICH HAD NO RULE AT ALL — and this is a
   correction, not an addition. `.devfact.warnnote` (app.js: "behind the stable
   build", "this recovery PIN is stale") and `.fineprint.warnnote` (js/security.js,
   three stale-PIN lines) sit beside `.devsub.warnnote` above and say the same
   kind of thing, but nothing ever painted them: they inherited `--haze`.
   In LIGHT they nevertheless came out amber, because the scoped `#8F5D00` patch
   at the end of this file selected a bare `.warnnote` and swept them up. So the
   product has been showing them amber in light and muted in dark, from a rule
   written for something else. Measured, before and after, with the patch
   removed and this rule in place:

     .devfact.warnnote   light  #8F5D00 -> #7A4F00     dark  #91A2BB -> #FFC44B
     .fineprint.warnnote light  #8F5D00 -> #7A4F00     dark  #91A2BB -> #FFC44B

   Light keeps the amber a parent already sees, dark stops disagreeing with it,
   and neither depends on a patch any more. No new colour: this is the token its
   own sibling one line up already uses, for the meaning UI-DIRECTION §3 gives
   amber. `.card.setupcode.sc-critical .fineprint` is more specific and still
   wins, so fine print inside that amber card keeps inheriting the card. */
.devfact.warnnote,
.fineprint.warnnote { color: var(--waiting-text); }

/* "Remove this tablet", under the silence banner that describes the row it
   removes. A leftover row — the tablet was re-provisioned, so it minted a new
   id and a new row, and this one will never be heard from again — could only be
   cleared from the device's OWN screen, two clicks away, under a heading about
   unlinking rather than about the dead card in front of the parent.
   Coral, per UI-DIRECTION §3's "coral = destructive". 44px of height because it
   is a real tap target inside a card the rest of which is one big button, and
   `align-self: start` so it is only as wide as its words — a full-width
   destructive control directly under a full-width Pause is a mis-tap. */
.devforget {
  display: inline-flex; align-items: center; align-self: flex-start;
  min-height: 44px; padding: 4px 10px; margin-top: 2px;
  font-size: 11.5px; font-weight: 700; cursor: pointer;
  color: var(--coral); border-radius: 8px;
  text-decoration: underline; text-underline-offset: 2px;
}
/* BARE var(--coral) AS TEXT FAILS AA ON LIGHT, and this file already knows it:
   the mockup's canonical light coral (#D4483D) measures 4.38:1 on white,
   4.15:1 on --dusk2 and 4.04:1 on --bg — under the 4.5 an 11.5px label needs.
   #B23127 is not a new colour; `.ghost.danger`, `.iconbtn.danger`, `.pill.off`
   and `.banner.err` all already carry it for this identical problem, and it
   measures 6.25:1 on white. Reused rather than re-derived, and applied on BOTH
   light paths — the explicit toggle and the no-attribute one — because the
   overrides above it are written that way for the same reason. */
:root[data-theme="light"] .devforget { color: #B23127; }
:root:not([data-theme]) .devforget { color: #B23127; }
.devforget:hover { background: color-mix(in srgb, var(--coral) 14%, transparent); }
.devforget:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }

/* -------------------------------------------------------------- modal shell */
/* No modal existed anywhere on this page before this feature (checked: zero
   `.modal`/backdrop rules in this file). #stepUpRow (app.css's own devices
   section, index.html) is the closest precedent — a top-level `.statuscard`
   sibling toggled by `.hide` — but that is an inline banner, not a
   backdrop+dialog, and the ticket's own "modal" language plus a multi-step
   flow (scope picker -> confirm -> live countdown) genuinely needs one. */
.spbackdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 40;
  display: flex; align-items: flex-end; justify-content: center; padding: 0;
}
:root[data-theme="light"] .spbackdrop { background: rgba(10,16,32,.4); }
.spmodal {
  background: var(--dusk); border: 1px solid var(--line); border-radius: 24px 24px 0 0;
  box-shadow: 0 -16px 44px rgba(0,0,0,.28); padding: 20px; width: 100%; max-width: 480px;
  max-height: 88vh; overflow-y: auto;
}
@media (min-width: 560px) {
  .spbackdrop { align-items: center; padding: 20px; }
  .spmodal { border-radius: 24px; }
}
.spmodal h2 { font-size: 16px; margin: 0 0 4px; color: var(--cloud); }
.spmodal .help { margin-bottom: 14px; }
.spscopes { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.spscope {
  display: flex; align-items: center; gap: 10px; text-align: left;
  border: 1px solid var(--line); background: var(--dusk2); border-radius: 14px;
  padding: 12px 14px; min-height: 52px; color: var(--cloud); font-family: var(--ui);
}
.spscope[aria-pressed="true"] { border-color: var(--glow); background: rgba(101,216,255,.1); }
.spscope .spscopename { font-weight: 700; font-size: 14px; }
.spscope .spscopedesc { font-size: 12px; color: var(--haze); font-weight: 400; }
.spcountdown {
  text-align: center; padding: 18px 12px; margin-bottom: 14px;
  background: rgba(79,209,161,.1); border: 1px solid rgba(79,209,161,.35); border-radius: 16px;
}
.spcountdown .sptime { font-size: 32px; font-weight: 800; color: var(--done-text); font-variant-numeric: tabular-nums; }
.spcountdown .splabel { font-size: 12px; color: var(--haze); margin-top: 2px; }
.spfooter { font-size: 12px; color: var(--haze); text-align: center; margin-top: 14px; line-height: 1.5; }
.spclose {
  position: absolute; top: 14px; right: 14px; background: transparent; color: var(--haze);
  font-size: 20px; min-height: 32px; min-width: 32px; border-radius: 8px;
}
.spmodal { position: relative; }

/* GoodQA 1ef0e1b1 [pp.zip Packet 2] — Devices screen redesign: collapsible
 * Desktop/Tablets groups (native <details>/<summary>, app.js sets .open and
 * listens for 'toggle' so state survives renderDevices()'s full rebuild),
 * per-group All/child filter tabs (plain `.chips`/`.chip` — the SAME classes
 * the Activity screen's own child/type filters already use, no new tab CSS
 * needed) and the compact per-device health capsule below.
 *
 * NO NEW COLOR MEANING (UI-DIRECTION-2026-08-22 §3, binding: tokens are
 * reserved for the meanings listed). `.healthcap-red`/`.hcdot-red` reuse the
 * IDENTICAL #FFB4AC `.pill.off`/`.banner.err` already use a few hundred lines
 * up for "reported, and reported bad" — not a new hue. `.healthcap-amber`
 * reuses `--amber` (`.banner.warn`'s own token); `.healthcap-green` reuses
 * `--mint` (`.pill.on`'s own token).
 */
.devgroup { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.devgroup:first-child { border-top: none; padding-top: 0; margin-top: 0; }
.devgrouphead {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  font-weight: 700; font-size: 14px; color: var(--cloud); padding: 6px 0; min-height: 32px;
}
.devgrouphead::-webkit-details-marker { display: none; }
.devgrouphead::before { content: "▸"; color: var(--haze); font-size: 11px; flex: none; }
.devgroup[open] > .devgrouphead::before { content: "▾"; }
.devgrouptitle { flex: 1 1 auto; }
.devgroupcount {
  font-size: 11px; font-weight: 700; color: var(--haze); background: var(--dusk2);
  border-radius: 20px; padding: 2px 9px; flex: none;
}
.devgroupbody { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }

.healthcap {
  display: flex; align-items: center; gap: 4px; flex: none; padding: 7px 9px;
  border-radius: 12px; border: 1px solid var(--line); background: var(--dusk2);
  min-height: 32px;
}
.healthcap-green { border-color: rgba(79,209,161,.35); }
.healthcap-amber { border-color: rgba(255,194,75,.4); }
.healthcap-red { border-color: rgba(255,107,94,.45); }
.hcdot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.hcdot-green { background: var(--mint); }
.hcdot-amber { background: var(--amber); }
.hcdot-red { background: #FFB4AC; }

/* "Want to add another device?" swaps for the required disabled notice below
 * the same 560px breakpoint the setup-window modal already keys off
 * (app.css:1233, .spbackdrop) — adding a device needs a USB cable and the
 * desktop installer/wizard, which is not a phone flow. Pure CSS, no JS: the
 * two elements are never toggled by app.js, only laid out oppositely here. */
#addAnotherDeviceMobileMsg { display: none; }
@media (max-width: 560px) {
  #addAnotherDeviceLink { display: none; }
  #addAnotherDeviceMobileMsg { display: block; }
}
/* OWNER, 2026-09-19: "[Add Device] [Pair]" is the header the owner asked
   for, and #pauseAllBtn already has a second home: paintPauseAllControls()
   (app.js) paints the SAME enabled/disabled state onto it and the
   persistent rail's #navPauseAllBtn together, and that rail exists
   precisely >=900px (app.css's own "RAIL-ONLY CHROME" rule hides
   .navfoot below that width — imprecisely "no rail" in an earlier draft
   of this comment; #nav.mobile-open's own phone sheet can still reveal the
   SAME #navPauseAllBtn below 900px, harmlessly, since nothing here hides
   it there). So the swap is tied to the PERSISTENT rail's own breakpoint,
   not #addAnotherDeviceLink's — the two are answering different questions
   (that one is "does this need a desktop to use", this one is "does a
   fallback control already exist elsewhere"). Below 900px there is no
   persistent rail, so #pauseAllBtn stays the header's job and
   #pairDeviceLink hides — pairing a device is exactly as desktop-bound
   as adding one, so it inherits that pattern rather than getting a third
   independent breakpoint.

   `!important` on the hide: `#devices #devTopActions > #pauseAllBtn` gets an
   explicit `display: inline-flex` of its own inside this SAME >=900px
   breakpoint elsewhere in this file (three ID selectors, source order after
   this rule) — the exact same same-media-query, higher-specificity collision
   the "TIGHT DEVICE ROWS" section further down hit and documented; making
   the win explicit here rather than fighting cascade order a second time.

   NOT `#pauseAllMsg` — a fresh adversarial review caught this: an earlier
   draft force-hid it here too, on the wrong assumption that it was purely
   the header button's own companion text. It is not. renderPauseAllResult()
   and pauseAllDevices() (app.js) write the SAME element for BOTH triggers —
   the header's #pauseAllBtn AND the rail's #navPauseAllBtn, which routes
   back to this exact screen specifically so its result has somewhere to
   render (app.js's own comment: "the screen the result is about"). Force-
   hiding it at >=900px meant a desktop parent pressing Pause-all from the
   rail got a real fan-out with zero visible confirmation, zero per-device
   breakdown and zero visible error text — aria-live="polite" cannot announce
   an element that is display:none. Its own pre-existing rule
   (`.ovhead-actions #pauseAllMsg:empty { display: none; }`, this file) is
   the correct, already-established way to hide it: on emptiness, not on
   viewport width. Left alone here. */
#pairDeviceLink { display: none; }
@media (min-width: 900px) {
  #devices #devTopActions > #pauseAllBtn { display: none !important; }
  #pairDeviceLink { display: inline-flex; }
}
.pauseAllResults { list-style: none; margin-top: 6px; text-align: left; }
/* No padding override here on purpose: renderPauseAllResult()'s <li> also
   carries .arow (elevated to match the rest of the alert-row vocabulary),
   whose padding: 11px 13px already applies. .pauseAllResultRow declared
   later in source with equal specificity used to zero that padding via the
   shorthand while .arow's border/background/radius still rendered —
   confirmed in real Chromium: a bordered card with no horizontal padding.
   This class now only ever contributes font-size/color. */
.pauseAllResultRow { font-size: 12px; color: var(--haze); }
.pauseAllResultRow[data-pause-status="unsupported"] { color: var(--haze); font-style: italic; }

/* GoodQA #293 — curated child-home Theme Studio. The browser receives the
   catalog from the same static origin and buttons contain only catalog ids;
   there is deliberately no upload or URL-entry surface. Its selected border is
   a parent intent, not an Applied claim: the per-tablet mint/amber pills below
   remain the acknowledgement authority. */
#childMount .theme-current { color: var(--cloud); font-size: 14px; font-weight: 750; margin: 2px 0 14px; }
#childMount .theme-discovery {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 12px;
  margin: 0 0 14px; padding: 12px; border: 1px solid var(--line); border-radius: 12px;
}
#childMount .theme-discovery legend { padding: 0 4px; color: var(--cloud); font-size: 13px; font-weight: 750; }
#childMount .theme-filter-label { color: var(--haze); font-size: 12px; font-weight: 650; }
#childMount .theme-filter { min-width: 0; }
#childMount .child-theme-choices,
#childMount .theme-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; margin-top: 14px; }
#childMount .theme-grid-cell { min-width: 0; }
#childMount .child-theme-choice,
#childMount .theme-choice {
  display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 44px;
  padding: 0; overflow: hidden; text-align: left; color: var(--cloud);
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 16px;
  font-family: var(--ui);
}
#childMount .child-theme-choice[aria-pressed="true"],
#childMount .theme-choice[aria-pressed="true"] { border-color: var(--glow); box-shadow: 0 0 0 1px var(--glow); }
#childMount .theme-choice[data-theme-busy="true"] { cursor: wait; opacity: .65; }
#childMount .child-theme-preview,
#childMount .child-theme-color,
#childMount .theme-preview {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  object-fit: cover; background: var(--track); border-bottom: 1px solid var(--line);
}
#childMount .theme-preview--cutout { object-fit: contain; background: #EAF2FB; }
#childMount .theme-hero-variants {
  margin: 16px 0 0; padding: 12px; border: 1px solid var(--line); border-radius: 12px;
}
#childMount .theme-hero-variants legend { padding: 0 4px; color: var(--cloud); font-weight: 750; }
#childMount .theme-child-pick {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 8px 12px; min-width: 0; margin-top: 12px;
}
#childMount .theme-child-pick .fineprint { min-width: 0; flex: 1 1 210px; }
#childMount .theme-seasonal {
  min-width: 0; margin: 16px 0 0; padding: 12px;
  border: 1px solid var(--line); border-radius: 12px;
}
#childMount .theme-seasonal legend { padding: 0 4px; color: var(--cloud); font-weight: 750; }
#childMount .theme-seasonal .theme-filter { width: 100%; max-width: 360px; margin-top: 6px; }
#childMount .theme-holidays { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
#childMount .theme-holidays summary { cursor: pointer; color: var(--cloud); font-weight: 750; }
#childMount .child-theme-copy,
#childMount .theme-choice-body { display: flex; flex-direction: column; gap: 6px; padding: 12px; }
#childMount .theme-choice-head { display: flex; align-items: center; gap: 8px; }
#childMount .theme-choice-name { flex: 1 1 auto; font-size: 14px; font-weight: 750; }
#childMount .theme-selected { flex: none; }
#childMount .child-theme-description,
#childMount .theme-choice-desc { color: var(--haze); font-size: 12px; font-weight: 400; line-height: 1.45; }
#childMount .theme-choice-meta { color: var(--cloud); font-size: 12px; font-weight: 650; line-height: 1.35; }
#childMount .theme-delivery { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
#childMount .theme-delivery-title { color: var(--cloud); font-size: 14px; margin: 0 0 6px; }
#childMount .theme-device-list { list-style: none; margin: 0; padding: 0; }
#childMount .theme-device { padding: 4px 0 10px; border-bottom: 1px solid var(--line); }
#childMount .theme-device:last-child { border-bottom: none; padding-bottom: 0; }

@media (max-width: 560px) {
  #childMount .theme-discovery { grid-template-columns: 1fr; }
  #childMount .theme-grid { grid-template-columns: 1fr; }
  #childMount .theme-choice { display: grid; grid-template-columns: 116px minmax(0, 1fr); }
  #childMount .theme-preview { height: 100%; min-height: 126px; aspect-ratio: auto; border-bottom: 0; border-right: 1px solid var(--line); }
}

@media (hover:hover) {
  #childMount .theme-choice { transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
  #childMount .theme-choice:hover:not(:disabled) { transform: translateY(-1px); border-color: var(--glow); }
}

/* ==========================================================================
   OVERVIEW / HOME — REDESIGN 2026-08-29
   The greeting header, attention strip, hero child card and stat-tile row the
   reference design has and this dashboard did not. Scoped to #overviewMount so
   no other screen inherits them. Every value these render is real and
   device-confirmed; see js/overview.js for which field feeds which element.
   ========================================================================== */
.ovhead { margin-bottom: 4px; }
/* THE SCREEN HEADER, AT HER MEASURED VALUES. Every number here came off the
   handoff's own Home artboard via getComputedStyle, not from a screenshot:
     eyebrow  Quicksand 12.5px/700, 1px tracking, brand blue, 18.75px line
     greeting Quicksand 32px/700, 48px line, -0.32px tracking
     lede     Nunito 16px/400, 24px line
   It was 11px/800/.12em, 27px/800/-.02em and 14px. The greeting being five
   pixels short and a weight heavy, over a lede two pixels short, is most of
   why the live header read as cramped next to hers -- individually each looks
   like a reasonable choice, which is exactly why it survived review.
   Tracking is stated in px, as she specifies it: -.02em at 27px happens to be
   -0.54px, but at her 32px the same em value would be -0.64px, twice her
   actual -0.32px. An em here would silently drift with any future size change. */
.ovhead .ov-eyebrow { font-family: var(--font-heading); font-size: 12.5px; font-weight: 700; letter-spacing: 1px; line-height: 1.5; text-transform: uppercase; color: var(--brand); }
.ovhead .ov-greet { font-family: var(--font-heading); font-size: 32px; font-weight: 700; letter-spacing: -.32px; line-height: 1.5; margin: 0 0 8px; color: var(--text-primary); }
.ovhead .ov-sub { font-family: var(--font-body); font-size: 16px; line-height: 1.5; color: var(--text-secondary); }

/* Attention strip — amber, "something is waiting for you" (UI-DIRECTION §3). */
.ovstrip {
  display: flex; align-items: center; gap: 12px;
  border: 1px solid color-mix(in srgb, var(--amber) 45%, var(--line));
  background: color-mix(in srgb, var(--amber) 12%, transparent);
  border-radius: 14px; padding: 13px 16px;
}
.ovstrip .ovstrip-txt { flex: 1; min-width: 0; font-size: 14px; color: var(--cloud); }
.ovstrip .ovstrip-txt b { color: var(--waiting-text); }

/* GoodQA ticket 483 — the add-a-device nudge banner. INDIGO, not amber:
   "needs attention" (a thing the PARENT should go do), not "waiting" (a
   thing the parent has not yet answered). --indigo/--indigo-tint are not
   yet defined in this file's :root — see UI-DIRECTION-2026-08-22 §3 and
   OWNER-DECISIONS §0.4 for the pinned meaning; the parallel token-layer
   pass owns picking and contrast-checking the hex values these resolve to. */
.ovnudge {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  border: 1px solid color-mix(in srgb, var(--indigo) 45%, var(--line));
  background: color-mix(in srgb, var(--indigo) 12%, transparent);
  border-radius: 14px; padding: 13px 16px;
}
.ovnudge-txt { flex: 1; min-width: 200px; font-size: 14px; color: var(--cloud); }

/* Hero child card. */
.ovcard { display: flex; flex-direction: column; gap: 13px; }
.ovcard .ovc-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.ovcard .ovc-who { flex: 1 1 auto; min-width: 0; }
.ovcard .ovc-head > .pill { flex: none; margin-left: auto; }
.ovcard .ovc-name { font-size: 18px; font-weight: 750; color: var(--cloud); }
.ovcard .ovc-state { font-size: 12.5px; color: var(--haze); display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.ovcard .ovc-state .ovc-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--mint); flex: none; }
.ovcard .ovc-timelabel { font-size: 12px; color: var(--haze); }
.ovcard .ovc-time { font-size: 30px; font-weight: 800; letter-spacing: -.01em; color: var(--cloud); font-variant-numeric: tabular-nums; }
/* `.help`'s own `margin-bottom: 10px` (app.css:858) was written for block
   layouts, where a following element's margin can collapse into it. `.ovcard`
   is `display: flex` with `gap: 13px` (right above), and flex items never
   collapse margins — so every `.help` line inside a hero card was paying
   BOTH: measured 2026-09-19 at exactly 23px between two consecutive `.help`
   lines (13px gap + the 10px margin, stacked, not merged), against 13px
   everywhere else in the same card. `.ovcard .ovpick > .help` a few lines
   down already zeroes this same margin for the same reason, inside the app
   picker; this is that fix's sibling, for the card's own limit/absence lines
   ("No device has reported screen time yet.", "No daily limit set") — the
   two-help-line household is exactly the shape that pushed the stat tiles a
   few pixels under the 900px fold. No line of text moves or is cut; the
   space between them does. */
.ovcard .help { margin-bottom: 0; }
/* The bar renders ONLY when remaining AND the daily limit are both really
   known — a bar with nothing behind it would imply a measurement. */
.ovbar { height: 7px; border-radius: 99px; background: var(--track); overflow: hidden; }
.ovbar > i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--mint), var(--glow)); }

/* ADDITIVE, 2026-08-30 — the per-app "+15 min" control (PARITY-DECISIONS §2).
   Two rules, both about a phone.

   1. `.rowline` is `display:flex` with NO wrap and `.rowline .ghost{flex:none}`,
      which was fine for the two buttons this card used to carry. It now carries
      three, and a multi-device child's are "Pause now (Leo's tablet)" and
      "Bedtime for Leo's tablet ›" — comfortably wider than a 390px card. Without
      wrapping they would overflow the card rather than stack. Scoped to
      `.ovcard` so no other screen's rowline changes behaviour.
   2. `.ovpick` is the app chooser that opens INSIDE the card. It reuses
      `.pickrow` verbatim (app.css's own whole-row target for the dashboard's
      app picker), so the only thing needed here is a bound on the ROWS' height:
      a child with ninety apps must not push the next child's card, and the stat
      tiles under it, off the screen. The bound is on `.ovpicklist` and not on
      `.ovpick` itself so the Cancel beneath it stays put — a way out that has
      to be scrolled to is not a way out on a phone. */
.ovcard .rowline { flex-wrap: wrap; }
/* ONE SHAPE PER CHILD CARD — owner review of the Portal Overview ("the two
   child cards are different shapes").
   ---------------------------------------------------------------------------
   Two things made two cards for the same kind of thing look like two different
   objects. The first was the labels, and js/overview.js has fixed it: a
   multi-device child's controls no longer read "Pause now (Leo's Galaxy Tab
   A9)". The second is here — `.rowline .ghost { flex: none }` a thousand lines
   up sizes every button to its own text, so a card carrying four controls and
   its sibling carrying three ended up with buttons of six different widths and
   a ragged wrap point.

   `flex: 1 1 auto` grows them from their natural widths to fill the row, so
   three or four controls both come out as one even row rather than as buttons
   of six different widths.

   `min-width: fit-content` IS THE LOAD-BEARING HALF, and a fixed floor was
   wrong here — measured, not reasoned: at 1440 the hero card is ~510px inside
   its padding and the three real ghost labels ("Pause now", "Bedtime ›",
   "+15 min") plus the primary door fit one row. An
   8.5rem floor forced 4x136+24 = 568px and wrapped the four-button card onto
   two rows — reproducing the exact defect this rule exists to remove, while
   the three-button card beside it stayed on one. `fit-content` lets them fill
   the row when there is room and wrap in WHOLE buttons when there is not (a
   390px phone), which is the one thing a plain `flex-shrink` would not do: it
   would squeeze each label until it broke across lines inside its own button.

   Nothing here changes WHICH buttons are offered — a control this page cannot
   honour is still absent rather than present and dead (see grantRows() in
   js/overview.js), so a card can legitimately carry three where its sibling
   carries four; they are the same shape either way. */
.ovcard .ovc-actions > .ghost { flex: 1 1 auto; min-width: fit-content; }
/* THE PRIMARY DOOR NEVER GOT THE SAME TREATMENT — measured 2026-09-19. The
   comment above claims "the three real ghost labels ... plus the primary
   door fit one row," but `.primary` (app.css:983) is `width: 100%` by
   default, so "Open {name}'s controls" always claimed the whole row for
   itself and forced a second one under it, exactly like `.ovquick .primary`
   had to be corrected for its own row (§7 of the Home block further down).
   Same fix, same reasoning: join the flex row on desktop, where the three
   real controls plus the door measure well under the ~494px hero content
   width at 1440x900, and still drop to its own full-width line under
   `.ovcard .rowline`'s existing wrap when the card is narrow enough that
   they do not fit — a 390px phone keeps exactly the stacked shape it had
   before this rule existed. */
.ovcard .ovc-actions > .primary { flex: 1 1 auto; min-width: fit-content; width: auto; margin-top: 0; }
/* THE ACTION ROW SITS ON THE FLOOR OF THE CARD, so two cards side by side line
   their controls up. `.ovcard` is already a flex column and the grid stretches
   both to the taller one's height, so without this the card carrying one extra
   line — the "no device has reported" sentence, or a next-scheduled-change note
   — pushed its buttons ~60px below its sibling's. Measured at 1440x950 before
   this rule: Leo's row at y709, Maya's at y770, on cards of identical height.
   That is the same "two cards for the same kind of thing, different objects"
   the labels were fixed for, one layer down. */
.ovcard .ovc-actions { margin-top: auto; }
/* THE ABSENT HEADLINE. Same size and alignment as a real figure so the cards
   stay in register; the colour is what says it is not a measurement. It is
   NEVER a zero — see js/overview.js's own note on why "0h 0m" would be a claim
   this page cannot make. */
/* "Not reported" is prose, not a number — the hero's 30/36px numeric size
   (measured at the display's own weight) exists to make a real answer
   readable at a glance, and giving the same size to the sentence that says
   there isn't one yet is why this state was the single tallest line in the
   card. Sized like the help text beside it instead: still the first thing
   read in the card (no other line in this state is bold), just no longer
   claiming a number's own footprint for a state that has none. */
.ovcard .ovc-time-none,
/* .ovc-time-none always co-occurs with .ovc-time on the same element (see
   overview.js), so `.ovcard.hero .ovc-time` (3 classes, defined below) ties
   or beats a plain `.ovcard.hero .ovc-time-none` on specificity and, being
   later in the file, would still win. Matching both classes on the one
   element (4 classes) is what actually wins the cascade here. */
.ovcard.hero .ovc-time.ovc-time-none { color: var(--haze); font-size: 15px; font-weight: 650; letter-spacing: normal; }
.ovcard .ovpick { margin-top: 2px; }
.ovcard .ovpick > .help { margin: 0 0 8px; }
.ovcard .ovpicklist { max-height: 232px; overflow-y: auto; -webkit-overflow-scrolling: touch; }

/* Stat tiles. */
.ovtiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
.ovtile { background: var(--dusk); border: 1px solid var(--line); border-radius: 14px; padding: 15px 17px; }
.ovtile .ovt-k { font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--haze); }
.ovtile .ovt-v { font-size: 20px; font-weight: 750; margin-top: 6px; color: var(--cloud); }
.ovtile .ovt-s { font-size: 11.5px; color: var(--haze); margin-top: 2px; }

/* Weekly chart card (GoodQA round-1332). .husage-chart* itself is styled in
   household-usage.css, shared with #householdUsage — see that file's own
   comment on why. This is only the card shell and the per-child grouping,
   since a household with more than one linked child needs a label between
   charts that the shared classes alone do not carry. */
.ovweekly-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.ovweekly-child { margin-top: 18px; }
.ovweekly-child:first-of-type { margin-top: 14px; }
.ovweekly-name { font-size: 13px; font-weight: 700; color: var(--cloud); margin-bottom: 6px; }

@media (max-width: 899px) {
  .ovhead .ov-greet { font-size: 22px; }
  .ovcard .ovc-time { font-size: 26px; }
}

/* A card heading that carries a real count beside it. SHARED and unscoped on
   purpose: js/engage.js reached for `#overviewMount`-scoped markup and got an
   unstyled block (the badge fell under the title) because style-vocabulary
   .test.js scans app.js and index.html only — a class used solely inside a
   js/*.js module is invisible to it. Anything a module needs belongs here,
   scoped to nothing, so the next module cannot repeat that. */
.cardhead { display: flex; align-items: center; gap: 8px; }
.cardhead > .sect { flex: 1; }

/* Check-ins & quizzes (js/engage.js) shares Home's two-track grid at the
   sidebar breakpoint — same reason #overviewMount needs its own rule: it is a
   mount div one level below `.screen`, so `.screen`'s grid cannot place its
   children. */
@media (min-width: 900px) {
  #engageMount { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-content: start; }
  #engageMount > .card { margin-top: 0; margin-bottom: 0; }
  /* THE REFERENCE DESIGN'S 2x2, PUT BACK ON TOP OF THE PHONE'S ORDER.
     mockup-check-ins-quizzes.png is two authoring cards on the top row and
     the two histories under them. That is a fine SHAPE and a bad DOM ORDER
     for a 390x844 phone, where it buries the check-in history under a whole
     quiz builder — and MOBILE IS LOAD-BEARING here (e2e/mobile.spec.cjs
     measures a real phone, and a previous pass on another screen pushed
     content below the fold that way). So js/engage.js emits the order a
     phone wants — compose, check-in history, build, quiz history — and the
     desktop shape is restored by NAMING the areas, which costs the phone
     nothing because the whole block is inside this media query.
     `align-items: start`, not the grid default `stretch`: the check-in
     composer is three controls and the quiz builder beside it is a whole
     form, and a stretched short card renders as a tall empty box (the same
     measurement #settingsMount's own rule at the end of this file records). */
  #engageMount { grid-template-areas: "compose build" "cihist qhist"; align-items: start; }
  #engageMount > .engage-compose { grid-area: compose; }
  #engageMount > .engage-build { grid-area: build; }
  #engageMount > .engage-cihist { grid-area: cihist; }
  #engageMount > .engage-qhist { grid-area: qhist; }
}

/* THE QUIZ BUILDER AND ITS RESULTS PANEL (js/engage.js).
   UNSCOPED and class-based, for the reason the `.cardhead` block above
   records: style-vocabulary.test.js scans app.js and index.html only, so a
   class used solely inside a js/*.js module is invisible to it and a typo
   here produces no error anywhere. Every rule below is layout on top of
   vocabulary this stylesheet already owns (.card/.arow/.inp/.fld/.fineprint/
   .pill/.ghost) — no new colours, no new type scale. NOT inside a media
   query: a parent builds a quiz on a phone as often as on a laptop, so these
   are the same at 390px and at 1440px, and the only desktop-only addition on
   this screen is the grid-template-areas block above. */
.engage-field { display: block; margin: 12px 0; }
.engage-field > .fld, .engage-field > .fineprint { display: block; margin-bottom: 5px; }
.engage-field > .fineprint { margin-top: 6px; margin-bottom: 0; }
.engage-question { border: 1px solid var(--arowline); border-radius: 14px; padding: 12px; margin: 12px 0; min-width: 0; }
/* flex, not a 2-track grid: the third child (Remove) only exists once a
   question has more than the server's minimum two choices, and on a 390px
   phone it must be allowed to WRAP under the answer box rather than squeeze
   it. `flex: 1 1 140px` is the answer field's floor — below that the text
   being typed is unreadable, which is worse than a wrapped button. */
.engage-choice { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.engage-choice > .inp { flex: 1 1 140px; min-width: 0; }
.engage-choice > input[type="radio"] { flex: none; width: 20px; height: 20px; accent-color: var(--glow); }
/* THE SAME CHECKBOX FIX AS .loginpanel .consentRow ABOVE (search this file
   for "THE CHECKBOX FIX"), applied to this modal's own native control: the
   PopQuizModal "which answer is correct" radio. `accent-color` immediately
   above only paints the CHECKED fill -- MEASURED separately, under
   prefers-color-scheme:dark with this app still on its light theme, the
   UNCHECKED radio painted as a solid dark disc, because nothing here ever
   pinned `color-scheme` and index.html's `<meta name="color-scheme" content=
   "light dark">` lets a dark-preferring OS choose for it. Scoped to
   `.engage-modal` (this composer's own dialog), not fixed globally, same
   reasoning as the loginpanel rule: a repaint of every native control on the
   dashboard is a separate, verified pass. */
:root[data-theme="light"] .engage-modal,
:root:not([data-theme]) .engage-modal { color-scheme: light; }
:root[data-theme="dark"] .engage-modal { color-scheme: dark; }
.engage-qactions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
/* MEASURED ON A REAL 390x844 PAGE, not reasoned about. A history row is
   `.arow` — flex, with `.an` at `flex: 1; min-width: 0` and every sibling at
   `flex: none`. On a phone the quiz row's siblings are a score pill (~90px)
   and a Results button (~110px), which leaves the title column about 70px and
   turns "For Leo · 2 questions · 8/30/2026" into four one-word lines. Giving
   the column a 200px FLEX BASIS instead of a 0 one makes it claim the whole
   line the moment the row cannot hold all three, and the pill and button wrap
   underneath — the same `flex-wrap` `.arow.new` already uses, for the same
   reason, and with no breakpoint, so it is the row's own content that decides
   rather than a viewport width guessed in advance. Scoped to `.engage-row`:
   `.arow` itself is shared by a dozen screens and is not this pass's to move. */
.arow.engage-row { flex-wrap: wrap; }
.arow.engage-row > .an { flex: 1 1 200px; }
/* When the Results panel is open it is a full-width block under all three. */
.arow.engage-quiz > .engage-results { flex-basis: 100%; border-top: 1px solid var(--arowline); margin-top: 4px; padding-top: 10px; }
.engage-attempt { margin-top: 8px; }
.engage-attempt + .engage-attempt { border-top: 1px solid var(--arowline); padding-top: 8px; }
/* Minutes a child ACTUALLY earned are the one number in this panel that is
   good news, and mint is this product's "healthy/applied" colour
   (UI-DIRECTION-2026-08-22 §4). Every other line in the panel stays the
   ordinary muted `.asub`. */
.asub.good { color: var(--done-text); }
/* THE SAME LIGHT-THEME SUBSTITUTION `.pill.on` ALREADY MAKES, and the same
   already-measured colour rather than a fourth literal: bare --mint as TEXT on
   the light page measures under the 4.5:1 an 11px `.asub` needs, and #0A6242
   is the reading this file's own comment beside `.pill.on` records at 5.9:1.
   Both arms, because "light" reaches the page two ways — an explicit
   data-theme, and the OS preference with no attribute set at all. */
:root[data-theme="light"] .asub.good { color: #0A6242; }
:root[data-theme="light"] .asub.good { color: #0A6242; }

/* ==========================================================================
   PROCESSES — the task-manager column layout (js/processes.js)

   UNSCOPED, and class-based rather than `#processesMount`-scoped, for the
   reason the `.cardhead` block above records: style-vocabulary.test.js scans
   app.js and index.html only, so a class used solely inside a js/*.js module
   is already invisible to it, and burying the rule under an id would make it
   invisible to the next module too. Nothing new is invented for the row
   itself — `.arow`/`.an`/`.asub`/`.atime`/`.sw`/`.pill` still do all the
   painting. `.procrow` only says WHERE those existing parts sit.

   The reference design draws this screen as a table with a column-label header
   (Name / Safe? / What it is / Allow). This turns the existing `.arow` from a
   flex strip into a grid with those same tracks, so the safety pill, the
   description and the allow control each land on the same x down the whole
   list — the thing a task-manager view exists for, and the one thing the flex
   strip could not do, because each row's columns started wherever that row's
   name happened to end.

   BELOW 1000px THE SAME DOM RESTACKS INTO THREE ROWS. A four-column table on a
   390px phone is the exact failure `.arow .atime`'s own comment writes up: the
   name column ends up a ribbon and the list stops being scannable. So the
   phone gets name / (safety + allow) / description stacked, and the column
   labels — which are meaningless without columns — are not rendered at all.
   This is the DEFAULT here and the table is the override, so a width nobody
   thought about gets the layout that survives 390px.
   ========================================================================== */
/* ==========================================================================
   `:not(.pw-process-row)` IS THE WHOLE OF T-R1, AND IT IS NOT A TIDY-UP.
   The shipped Processes row has not been THIS row since js/processes.js grew
   renderWorkspaceRow(): it is `.arow.procrow.pw-row.pw-process-row` with SIX
   cells (name / rating / description / access / limit / more), and
   css/policy-workspace.css sizes it with six tracks of its own. But
   `.arow.procrow` is two classes and `.pw-process-row` is one, so THIS block
   went on winning, and the six cells were auto-placed into a five-track
   template written for entirely different children. MEASURED in Chromium at
   1440x900, en, dark, before this line: the computed row was
   `246px 100px 492px 90px 46px`, the category pill was drawn 46px outside its
   own 100px cell and 32px INTO the sentence beside it, the Allow/Temporary/
   Block control overflowed its 90px cell by 36px (50px in fr) and painted
   "Block" across "View limits", and `.pw-more-cell` — with no track left on
   row 1 — wrapped to an implicit second grid row underneath the name.
   platform/test/processes-layout.test.js measures all four.

   The legacy table below is NOT deleted, because renderRow() — the no-helper
   fallback this module keeps for small harnesses and mixed-version deploys —
   still renders `.an`/`.pill`/`.procwhat`/`.atime`/`.sw` and still needs it.
   It simply stops claiming a row it was never written for. Everything else
   `.arow.procrow` says (the `.rowline` margin, the 44px `.ghost.small` floor,
   `.asub.procid`'s monospace filename) is UNSCOPED on purpose and still
   reaches the workspace row, which is the only reason those three are not
   repeated in policy-workspace.css.
   ========================================================================== */
.prochead, .arow.procrow:not(.pw-process-row) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-areas:
    "name name name"
    "pill word sw"
    "what what what";
  gap: 7px 12px;
  align-items: center;
}
.arow.procrow:not(.pw-process-row) > .an { grid-area: name; }
/* justify-self so the pill keeps its own width — a grid item stretches by
   default, and a `.pill` stretched across its track reads as a filled bar. */
.arow.procrow > .pill { grid-area: pill; justify-self: start; }
.arow.procrow > .procwhat { grid-area: what; }
.arow.procrow > .atime { grid-area: word; }
.arow.procrow > .sw { grid-area: sw; }
.arow.procrow > .procguard {
  grid-column: 1 / -1;
  justify-self: stretch;
  padding-top: 7px;
  border-top: 1px solid var(--arowline);
}

/* Parent Guard's full truth copy remains visible on desktop. Phones get a
   compact, native disclosure whose summary always exposes the current state;
   this removes a large setup paragraph from the first viewport without
   hiding whether scanning is ready, unavailable, or waiting on the parent. */
.proc-guard-details { margin: 0; }
.proc-guard-summary {
  display: none;
  align-items: center;
  grid-template-columns: minmax(0, 1fr) auto 16px;
  gap: 10px;
  min-height: 44px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: var(--dusk2);
  color: var(--cloud);
  cursor: pointer;
  list-style: none;
}
.proc-guard-summary::-webkit-details-marker { display: none; }
.proc-guard-summary::after {
  content: "⌄";
  color: var(--haze);
  font-size: 18px;
  line-height: 1;
  transform: rotate(-90deg);
  transition: transform .16s ease;
}
.proc-guard-details[open] > .proc-guard-summary::after { transform: rotate(0); }
.proc-guard-details > .banner { margin-bottom: 16px; }

@media (max-width: 560px) {
  .proc-guard-details { margin-bottom: 12px; }
  .proc-guard-summary { display: grid; }
  .proc-guard-details[open] > .proc-guard-summary { margin-bottom: 8px; }
  .proc-guard-details > .banner { margin-bottom: 0; padding: 12px 13px; }
  .proc-guard-details .ghost.small,
  .arow.procrow .ghost.small { min-height: 44px; padding: 9px 12px; }
  #processesCard { padding: 14px; }
  #processesMount > .cardhead > .sect { margin-bottom: 8px; }
  #processesMount > .proc-list-sub { display: none; }
}

/* "What it is" — the classifier's own one-line use-case sentence. Same haze as
   `.asub`, one step larger because on a wide screen it is a column of its own
   rather than a footnote under a name. */
.procwhat { font-size: 12.5px; color: var(--haze); min-width: 0; overflow-wrap: anywhere; }

/* The column labels exist only where the columns do. */
.prochead { display: none; }

/* 1000px, NOT the 900px the sidebar and the two-track card grid use. The four
   columns need about 500px of their own before "What it is" stops wrapping to
   three lines, and between 900 and 1000 this screen's card is one track of a
   two-track grid — roughly 310px. So the table appears only where there is
   room for a table, and everywhere below that the SAME DOM stays stacked,
   which is the layout that already works at 390px. */
@media (min-width: 1000px) {
  /* A TABLE IS ONE WIDE UNIT, so this screen's single card spans both tracks
     of `.screen`'s two-track grid. Id-scoped for the same reason
     `#overviewMount`'s own `grid-column: 1 / -1` above is: placing a specific
     child in a specific parent grid is not something a shared class can say.
     MEASURED with a real 1440x950 screenshot before this line existed — in one
     track the four columns shared about 160px between them, "What it is"
     wrapped to four lines on every row and an eleven-character app name
     hyphenated mid-word, which is the same "nothing failed; the page just
     became unreadable" failure `#profList .arow`'s comment describes.
     #processesNoChild joins it so the choose-a-child card in the same slot
     does not jump width. */
  #processesCard, #processesNoChild { grid-column: 1 / -1; }
  /* Ratios measured against the reference design's own table: the name gets
     about a quarter of the free width and the description about half, because
     a use-case sentence is the long text on this screen and an app name is
     not. */
  /* `:not(.pw-process-row)` for the reason written out in full above this
     block: these five tracks are the five-cell legacy row's, and the shipped
     six-cell workspace row must get its own six from
     css/policy-workspace.css. THIS declaration is the one that did the
     damage — it is the template a real 1440px page computed as
     `246px 100px 492px 90px 46px` under six children. */
  .prochead, .arow.procrow:not(.pw-process-row) {
    grid-template-columns: minmax(0, 1.1fr) 100px minmax(0, 2.2fr) 90px 46px;
    grid-template-areas: "name pill what word sw";
    gap: 14px;
  }
  /* `.rowline`'s own 10px is sized for a control stacked under a paragraph; in
     a table cell it is the only thing under a single line of name. */
  .arow.procrow .rowline { margin-top: 6px; }
  /* 14px = `.arow`'s own 13px padding + its 1px border, so a label sits over
     the column it names instead of one pixel off it. */
  .prochead {
    display: grid; padding: 2px 14px 8px; align-items: end;
    font-size: 10.5px; font-weight: 800; letter-spacing: .09em;
    text-transform: uppercase; color: var(--haze);
  }
}

/* ==========================================================================
   DEVICES · REQUESTS · ACTIVITY · HOUSEHOLD — REDESIGN 2026-08-29
   ==========================================================================
   Appended at the end of the file, the convention every GoodQA feature block
   above already follows: purely ADDITIVE selectors, so a merge with concurrent
   work on another screen is a clean append on both sides. A few rules here
   deliberately RE-DECLARE an existing selector later in source (`.reqcard`,
   `.reqhead .an`, `.devgroupbody`, `.reqactions`'s margin) rather than editing
   the original — the original stays readable next to its own comment, and
   reverting this pass is a deletion of this block rather than an archaeology
   exercise.

   NO NEW HEX AND NO NEW COLOUR MEANINGS (UI-DIRECTION-2026-08-22 §3). Every
   colour below is one of this file's own tokens, or one of the light/dark text
   literals it already uses for the identical problem elsewhere (#0A6242 =
   `.pill.on` light, #B23127 = `.pill.off`/`.banner.err` light, #FFB4AC =
   `.pill.off` dark, #0A6E8C = `.devactionbtn.setup` light). That matters here
   specifically: devices-ui.test.js runs axe-core over #devices at 390x844 AND
   1280x900 in BOTH themes, so a hue picked by eye fails the suite.

   MOBILE IS THE MEASURED CASE. Everything that only belongs on a wide screen
   (the request row's side-by-side actions, the activity clock column, the
   full-width card bands) is inside a media query; below those breakpoints
   these screens keep the single-column shape e2e/mobile.spec.cjs and
   protection-honesty.test.js measure at 390x844.
   ========================================================================== */

/* ------------------------------------------------------------- devices card
   The reference build's device tile: an identity band, a four-signal health
   strip and an action row, stacked full width — not `.devrow`'s original two
   side-by-side columns, where a four-button action stack squeezed the name
   column (the same flex-squeeze `#profList .arow` and `#sitesList .arow` each
   already carry their own fix for further up this file).

   `.devcard` is a SECOND class on the same element and the DOM under it is
   byte-for-byte what it was: parent-lock-ui.test.js asserts this row's three
   direct children are [nav button, health capsule, lock control] as siblings,
   and that the nav column holds [name row, last-seen, platform line] in that
   order. So the card is built entirely out of CSS over the existing tree. */
.devrow.devcard {
  display: flex; flex-direction: column; gap: 11px;
  padding: 14px; margin-bottom: 0; border-radius: 16px;
}
.devcard .devbtn { padding: 0; min-height: 0; align-items: flex-start; gap: 10px; }
/* A flex column is what lets `order` lift the platform line to the top (see
   `.devplat` below) without moving it in the document. */
.devcard .devnm { display: flex; flex-direction: column; }
.devcard .devnmtext { font-size: 17px; font-weight: 750; letter-spacing: -.01em; }
.devcard .devsub { font-size: 11.5px; }
.devcard .devopen { margin-top: 2px; }
/* THE EYEBROW. `.devplat` is the platform sentence app.js has always rendered
   ("Android tablet" — it read "Supervised Android device" until tablet-first
   setup, when the word left the string because `dev.platform` cannot know
   whether anything is enforcing), promoted to the top of the card in PAINT
   order only — `order: -1`, never a move in the markup, because the fixture in
   parent-lock-ui.test.js reads that line at a fixed child index. --haze and
   not --glow: this text is 10.5px, so it needs 4.5:1, and --haze is the one
   secondary colour in this file measured at that ratio against --dusk2 in both
   themes (see its own comment on :root[data-theme="light"] near the top). */
.devcard .devplat {
  order: -1; margin: 0 0 3px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
}
/* A per-row banner inside a card does not need the 16px a page-level banner
   carries — it already sits inside 14px of card padding. */
.devcard .banner { margin: 6px 0 0; padding: 9px 11px; }

/* The health strip. `.healthcap` is still the same single button with the same
   data-device-id / data-health / aria-label devices-ui.test.js reads; it is
   now full width, with each signal's own real label beside its dot instead of
   four bare dots whose meaning lived only in a `title` attribute. --dusk
   inside a --dusk2 card reads as an inset panel in BOTH themes (darker on
   dark, lighter on light), which is why the fill is a token swap and not a
   translucent wash — a wash is also what makes axe report an indeterminate
   background instead of a measured one. */
.devcard .healthcap {
  width: 100%; padding: 9px 11px; border-radius: 12px; background: var(--dusk);
  display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 7px 12px; text-align: left; align-items: start;
}
.hccell { display: flex; align-items: flex-start; gap: 7px; min-width: 0; }
.hccell .hcdot { margin-top: 4px; }
.hctxt {
  font-size: 11px; font-weight: 700; line-height: 1.25; color: var(--haze);
  min-width: 0; overflow-wrap: anywhere;
}
/* COLOUR PER SIGNAL, NEVER ONE ROLLED-UP GREEN. The reference build paints all
   four of these cells from a single online/paused boolean; here each label
   takes its own signal's colour, so a device that is online and applied but
   has had supervision released renders three green cells beside one red one.
   The light-theme values are the already-measured ones this file uses for the
   identical problem elsewhere — never a fresh hue. */
.hcdot-green + .hctxt { color: var(--done-text); }
.hcdot-amber + .hctxt { color: var(--waiting-text); }
.hcdot-red + .hctxt { color: #FFB4AC; }
:root[data-theme="light"] .hcdot-green + .hctxt { color: #0A6242; }
:root[data-theme="light"] .hcdot-red + .hctxt { color: #B23127; }
:root[data-theme="light"] .hcdot-green + .hctxt { color: #0A6242; }
:root[data-theme="light"] .hcdot-red + .hctxt { color: #B23127; }

/* The action row. `.devlock` was a narrow right-hand COLUMN; on a card it is a
   wrapping row, with the delivery sentence ("Pause requested…", "Applied ✓")
   on its own full-width line under the buttons rather than splitting them in
   half. `.devlockbtn`'s own 48px floor is untouched — parent-lock-ui.test.js
   reads that literal out of this stylesheet. */
.devcard .devlock { flex-direction: row; flex-wrap: wrap; gap: 8px; padding: 0; align-items: center; }
.devcard .devlockbtn, .devcard .devactionbtn { width: auto; flex: 1 1 auto; min-width: 128px; }
.devcard .devlockstate { flex: 1 0 100%; min-height: 0; text-align: left; }

/* Two-up (or more) device tiles wherever the column is wide enough, one column
   on a phone — `auto-fill` plus `min(100%, 380px)` answers that from the
   container's real width, so no breakpoint has to guess it. Re-declared rather
   than edited: the original `.devgroupbody` rule keeps its own comment. */
.devgroup > .devgroupbody {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr));
  gap: 12px; margin-top: 8px; align-items: start;
}
.devgroup > .devgroupbody > .muted { grid-column: 1 / -1; }
.devgroup > .chips { margin: 6px 0 2px; }

/* The per-device pause result list. Each <li> is an `.arow` carrying the device
   name and ONE pill with the route's own per-device outcome — never a blanket
   "Paused ✓" (renderPauseAllResult in app.js owns that promise). */
.pauseAllResults .pauseAllResultRow { align-items: center; gap: 10px; }
.pauseAllResults .pauseAllResultRow .an { font-size: 13px; font-weight: 600; }

/* ------------------------------------------------------------- requests row
   Monogram, eyebrow, address, scope — with the three decisions beside them on
   a wide screen and under them on a phone. */
.reqcard { padding: 14px 16px; }
.reqhead { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
.reqtext { flex: 1; min-width: 0; }
.reqeyebrow {
  font-size: 10.5px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase;
  color: var(--haze); margin-bottom: 3px;
}
.reqcard .reqtext .an { font-size: 16px; font-weight: 700; }
/* HER APPROVE/DENY SPEC: 8px/14px at Quicksand 12.5px, the 34px pill she drew.
   This rule was re-stating 10px/15px/13px/42px and, because it is more specific
   than `.iconbtn`, it silently beat the base rule -- so fixing `.iconbtn` alone
   changed nothing on the one screen these buttons actually appear on.
   min-height follows the same touch rule as `.iconbtn`: 44px is the floor, her
   34px applies only at >=900px with a fine pointer. */
.reqactions .iconbtn { padding: 8px 14px; font-size: 12.5px; min-height: 44px; }
@media (min-width: 900px) and (pointer: fine) {
  .reqactions .iconbtn { min-height: 34px; }
}
/* ALLOW IS THE PRIMARY DIRECTION, so it takes the cyan this page already means
   "trusted + primary" by, and No keeps `.iconbtn.danger`'s coral. "Allow
   scoped" stays undecorated on purpose — it is a form of allow, not a third
   meaning (openAskRow's own comment in app.js). #0A6E8C on light is the exact
   value `.devactionbtn.setup` already uses for cyan-on-white. */
.iconbtn.affirm {
  /* HER APPROVE BUTTON: a SOLID green pill with white text, not a tinted
     outline. Measured rgb(63,166,108) on the handoff, which is --done-accent.
     This was a 12%-alpha wash of --glow with a colour-mixed border, which at
     a glance is indistinguishable from the neutral control beside it -- the
     affirmative action did not look affirmative.
     NOTE, deliberately flagged rather than done quietly: this moves the
     affirmative action from BLUE to GREEN. UI-DIRECTION §3 reserves blue for
     "trusted + primary" and green for "done/applied", so this is a semantic
     change, not only a visual one -- defensible (approving IS the done
     direction) but it is the owner's call, and it is raised in
     plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md rather than
     buried in a stylesheet. */
  color: #FFFFFF;
  border-color: transparent;
  /* Closest darker brand green with comfortable AA text contrast: 4.85:1.
     The mockup's #3FA66C with white measured only 3.05:1. */
  background: #2F8053;
}
@media (min-width: 900px) {
  .reqcard { display: flex; align-items: center; gap: 18px; }
  .reqcard > .reqhead { flex: 1; min-width: 0; }
  .reqcard > .reqactions { margin-top: 0; flex: none; }
}

/* HELP-REPLY DESKTOP WIDTH REPAIR
   A free-text help ask has three siblings (headline, textarea, Send), unlike
   the fixed-decision request row the generic desktop flex rule was built for.
   Give its headline a full row, then keep a genuinely wide reply field beside
   the fixed-width action. This breakpoint-only rule owns the wide layout;
   css/requests.css owns the phone reply row. Every `.reqdecisions` row stays
   untouched at both sizes. */
@media (min-width: 900px) {
  .reqcard.help-reply-card {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
    gap: 10px 12px;
  }
  .reqcard.help-reply-card > .reqhead { grid-column: 1 / -1; }
  .reqcard.help-reply-card > .inp { min-width: 0; }
  .reqcard.help-reply-card > .reqactions {
    grid-column: 2;
    align-self: end;
    margin-top: 0;
  }
}

/* ------------------------------------------------------------ activity list
   One divided ledger inside the card instead of a stack of separately bordered
   rows: a clock column, a category glyph, then the sentence. The
   `.arow`/`.an`/`.asub` shape underneath is untouched — activity-ui.test.js
   reads `.an`'s first child node and the row's first `.asub`. */
#activityList .arow.actrow {
  background: transparent; border: 0; border-radius: 0; margin-bottom: 0;
  border-bottom: 1px solid var(--line);
  padding: 11px 2px; gap: 10px; align-items: flex-start;
}
#activityList .arow.actrow:last-child { border-bottom: 0; }
.acttime {
  flex: none; width: 62px; padding-top: 5px; white-space: nowrap;
  font-size: 11.5px; font-weight: 700; color: var(--haze); font-variant-numeric: tabular-nums;
}
/* A neutral tile, never a coloured one: colour on this page is a health claim,
   and "a website was blocked" is an event rather than a state. */
.acticon {
  flex: none; width: 30px; height: 30px; border-radius: 10px; background: var(--dusk2);
  display: flex; align-items: center; justify-content: center; font-size: 14px; line-height: 1;
  color: var(--haze);
}
/* GOODQA #469 — the '•' fallback still uses the tile's own font-size above;
   real icons (fillActicon() in app.js) are inline SVGs on the same 24x24/
   stroke-currentColor grid as the nav icons, so they take their own size and
   this tile's `color` (unset before this pass — emoji do not obey `color`,
   so nobody needed one) rather than inheriting the row's body text colour. */
.acticon svg { width: 16px; height: 16px; }
/* The clock is the one part of the row a 390px phone has no width for, and it
   costs nothing there: the `.asub` under every sentence already carries
   "{child} · {when}". Same 560px breakpoint the add-a-device swap above uses. */
@media (max-width: 560px) {
  .acttime { display: none; }
}

/* ---------------------------------------------------------- household rows
   The profile list is the reference build's "Child profiles" card: a face, a
   name, what it is bound to, and its controls. Deliberately does NOT touch
   `#profList .arow { flex-wrap: wrap }` / `.an { min-width: 55% }` above —
   inventory-ui.test.js deletes those two lines byte-for-byte to prove its own
   390px measurement can fail, so re-stating either here would silently disarm
   that control. */
#profList .arow.profrow .an { font-size: 15px; font-weight: 700; }
#profList .arow.profrow .asub { font-weight: 400; margin-top: 2px; }
/* An archived profile cannot be bound to anything and is not a live row; it was
   previously indistinguishable from a live one apart from its sentence. */
#profList .arow.archived { opacity: .72; }

/* --------------------------------------------------------- full-width bands
   At the sidebar breakpoint `.screen` is a two-track grid (see its own rule
   near the top of this file). These three are LISTS, not cards in a column:
   the reference build gives each the full content width and lets the list
   itself decide how many tracks it wants. `:has()` rather than an id list, for
   the same reason the `.screen` rule keys on a class — the markup these select
   is outside this pass's territory and must not have to be edited to opt in.
   Inside the media query, so a phone is untouched. */
@media (min-width: 900px) {
  .screen > .card:has(#devList) { grid-column: 1 / -1; }
  .screen > .card:has(#activityList) { grid-column: 1 / -1; }
  .screen > #asksCard { grid-column: 1 / -1; }
}

/* ==========================================================================
   ACCOUNT & SETTINGS — RESTYLE 2026-08-29 (js/account.js)
   Layout only. Every class the module draws with is one this stylesheet
   already defined for another screen (.ovhead/.ov-eyebrow/.ov-greet/.ov-sub,
   .ovtiles/.ovtile/.ovt-k/.ovt-v/.ovt-s, .chips/.chip[aria-pressed]) — this
   block adds no new vocabulary, only the placement those classes need inside
   a mount div. Declared here rather than beside the module because
   style-vocabulary.test.js scans app.js and index.html only, so a class used
   solely inside a js/*.js module is invisible to it (the .cardhead comment
   above is the incident that rule comes from).
   ========================================================================== */
@media (min-width: 900px) {
  /* Same reason #overviewMount and #engageMount each need their own rule:
     #settingsMount is a wave-0 mount div one level below `.screen`, so
     `.screen`'s two-track grid cannot place its children — before this, all
     eight account cards stacked in ONE half-width track with the other track
     left empty. */
  /* `align-items: start` and not the grid default `stretch`, which the other
     two mount grids can leave alone because their cards are all roughly one
     height. These are not: #planCard is a single sentence and the billing
     panel beside it is a three-tier price list, and a stretched one-line card
     rendered as ~600px of empty box. MEASURED in a 1440x950 screenshot, the
     way #profList .arow's own comment in this file says these are found. */
  #settingsMount { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-content: start; align-items: start; }
  #settingsMount > .card { margin-top: 0; margin-bottom: 0; }
  /* The header, the count row and the group rail are about the page, not
     about one card in it. */
  #settingsMount > .ovhead, #settingsMount > .ovtiles, #settingsMount > .chips { grid-column: 1 / -1; }
  /* Exactly the reasoning `#overview > .toprow` above already carries: the
     header block states the screen, so the generic "Settings" title directly
     over it would be the same word twice. */
  #settings > .toprow { display: none; }
}

/* MOBILE IS LOAD-BEARING (e2e/mobile.spec.cjs measures a real 390x844 phone,
   and a previous pass on another screen pushed content below the fold there).
   `.ovtiles` is auto-fit/minmax(180px), so on a 390px screen the three count
   tiles would stack into ~200px of chrome above the group rail — and they are
   a convenience only: each number is also readable from the list in the card
   it summarises. So the phone gets the header and the rail, and the tiles are
   a wide-screen affordance. */
@media (max-width: 899px) {
  #settingsMount > .ovtiles { display: none; }
}

/* ------------------------------------------------------ focused task pages
   Device linking and profile administration used to sit below the live device
   list on every visit. They are still the same controls, now selected through
   one short task rail. Device acquisition remains a distinct header action;
   phones keep their only visible sign-out path and the findable account-safety
   disclosure. */
#devices #addAnotherDeviceLink { display: inline-flex !important; }
#devices #addAnotherDeviceMobileMsg { display: none !important; }
/* CONVERTED 2026-09-09 — PORTAL-LAYOUT-LAYER.md §8 step 2 and its rule 5.
   #devices is `.screen.composed` now, so it has no grid to escape and the
   three `grid-column: 1 / -1` rules that used to be here are DELETED rather
   than left inert and misleading. What survives is the one thing the grid's
   `gap` used to do: a card inside `.stack` must not add its own 16px margin
   to the stack's 14px, and `@media(min-width:900px) .screen > .card` no
   longer reaches these cards because `.stack` is between them and the
   section. */
/* KEYED ON `.devstack`, THIS FILE'S OWN NAME, NOT ON base.css's `.stack`.
   base.css's whole isolation guarantee is that no other stylesheet names one
   of its classes (PORTAL-LAYOUT-LAYER §5, pinned by
   test/base-css-isolation.test.js); writing `#devices > .stack` here would
   have broken it on the first conversion, which is exactly what that test
   caught. The wrapper wears both: `stack` for the layer's rhythm, `devstack`
   for this screen's own margin correction. */
#devices > .devstack > .card,
#devices > .devstack > .device-panel { margin-bottom: 0; }
#devices > .devstack > .device-panel-stack { display: grid; gap: 16px; }
#devices > .devstack > .device-panel-stack > .card { margin: 0; }
#startCard .start-steps { margin-top: 12px; }
#startCard .start-steps .startlist { margin-top: 12px; }

.screen-help {
  margin: 0 0 16px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--dusk2);
}
.screen-help summary {
  cursor: pointer; padding: 12px 14px; color: var(--haze);
  font-size: 13px; font-weight: 750; list-style: none;
}
.screen-help summary::-webkit-details-marker { display: none; }
.screen-help summary::before { content: "▸ "; color: var(--attention-text); }
.screen-help[open] summary::before { content: "▾ "; }
.screen-help-body { padding: 0 14px 12px; }
.screen-help-body > :last-child { margin-bottom: 0; }

.child-settings-more {
  grid-column: 1 / -1; margin: 0; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--dusk2);
}
.child-settings-more > summary {
  cursor: pointer; display: flex; justify-content: space-between; gap: 16px;
  align-items: center; padding: 16px 18px; list-style: none; color: var(--haze);
}
.child-settings-more > summary::-webkit-details-marker { display: none; }
.child-settings-more > summary::before { content: "▸"; color: var(--attention-text); }
.child-settings-more[open] > summary::before { content: "▾"; }
.child-settings-more > summary strong { flex: 1; color: var(--ink); font-size: 15px; }
.child-settings-more > summary .help { margin: 0; }
.child-settings-more-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; padding: 0 18px 18px; }
.child-settings-more-body > .card { margin: 0; }
.child-settings-more-body > .theme-studio { grid-column: 1 / -1; }
.child-history .child-settings-more-body { grid-template-columns: 1fr; }
.proc-more { margin-top: 14px; }

@media (max-width: 700px) {
  .child-settings-more > summary { align-items: flex-start; flex-wrap: wrap; }
  .child-settings-more > summary .help { flex-basis: 100%; padding-left: 18px; }
  .child-settings-more-body { grid-template-columns: 1fr; padding: 0 12px 12px; }
  .child-settings-more-body > .theme-studio { grid-column: auto; }
}

@media (min-width: 900px) {
  #devices #devTopActions > #pauseAllBtn { display: inline-flex; }
}

/* --------------------------------------------------------------------------
   MOBILE APP SHELL — compact reference navigation
   Signed-in phones keep the four highest-value destinations in a bottom bar;
   the complete rail opens as a sheet from the header's Menu control. Auth
   screens retain the centered sign-in shell. */
.mobilemenubtn { display: none; }
/* THE FOLD CUE IS A PHONE-SHELL THING AND NOTHING ELSE. Declared closed here
   so that at >=900px it stays gone whatever app.js does with the `hidden`
   attribute: there is no fixed dock at that width to hide a card boundary
   behind, so the cue would be answering a question nobody asked. The rules
   that actually paint it live inside the phone media query below. */
.morebelow { display: none; }
@media (max-width: 899px) {
  /* ON :root, NOT ON body, because <html> reads it too (see the
     scroll-padding rules below) and a custom property only inherits DOWN. */
  :root {
    --phonedock: calc(73px + env(safe-area-inset-bottom, 0px));
  }
  body.signed-in {
    padding-bottom: calc(92px + env(safe-area-inset-bottom, 0px));
  }
  /* KEEP THE DOCK OUT OF THE WAY WHEN ANYTHING SCROLLS ITSELF INTO VIEW.
     `scroll-padding-bottom` is set on the SCROLL CONTAINER, which in standards
     mode is <html> and never <body> — so it is declared here rather than beside
     the --phonedock consumers below, and `.enrolpinned` is mirrored onto
     <html> by app.js for the same reason.
     WHAT IT FIXES, and it is not a test artefact: the bar and the enrol banner
     are position:fixed, so they sit over the last ~230px of the viewport. Any
     control the browser scrolls to the BOTTOM edge lands underneath them —
     `element.scrollIntoView()`, an in-page #anchor, and the automatic scroll
     when a control takes focus all do exactly that. Measured on the categories
     screen at 390x844: #catSaveBtn scrolled into view and every click on it was
     swallowed, first by #enrolBannerTitle and then by #navRow.
     A parent meets this as "the Save button does nothing". scroll-padding is
     the property that exists for precisely this and costs nothing else: it
     changes where a scroll comes to rest, not the layout. */
  html {
    scroll-padding-bottom: calc(var(--phonedock, 0px) + 24px);
  }
  /* The banner is pinned above the bar, so it needs its own clearance: its
     84px offset + its 86px height + 8px of air. */
  html.enrolpinned {
    scroll-padding-bottom: calc(178px + env(safe-area-inset-bottom, 0px));
  }
  /* THE SKIP LINK, ON A PHONE, LANDS ABOVE THE DOCK — not on top of it.
     
     `.navskip:focus` is `position: fixed; top: 10px`, which on a desktop puts
     it in the top-left of the viewport. On a phone it does not: `#nav` is the
     fixed bottom dock and carries `backdrop-filter`, and a filtered element is
     a containing block for its `position: fixed` descendants. MEASURED at
     320x568: the revealed link rendered at y=506 — ten pixels below the dock's
     own top edge and directly over the "Home" button. Visible, but sitting on
     the control next to it.
     
     `position: absolute` makes the containing block DETERMINISTIC (the nearest
     positioned ancestor, which is #nav either way) rather than dependent on
     whether the browser implements backdrop-filter, and `bottom: calc(100% +
     10px)` then means "ten pixels above the dock" in all of them. */
  body.signed-in .navskip:focus,
  body.signed-in .navskip:focus-visible {
    position: absolute;
    top: auto;
    bottom: calc(100% + 10px);
    left: 0;
  }
  body.signed-in .mobilemenubtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--dusk2);
    color: var(--cloud);
    font: inherit;
    font-weight: 700;
  }
  body.signed-in .mobilemenubtn[aria-expanded="true"] {
    background: var(--glow);
    color: var(--ink);
    border-color: var(--glow);
  }
  /* ------------------------------------------------------------------
     "THERE IS MORE BELOW THIS" — the phone's fold cue.

     WHY IT EXISTS, MEASURED. On a signed-in phone the dock above is
     `position: fixed` with a 94%-opaque background over the last 73px of the
     viewport. That is fine while a card runs THROUGH the fold — the parent
     sees it slide under the bar and knows to keep going. It is not fine when
     a card boundary happens to land in the few pixels between the last
     visible card and the dock's top edge, because then the page looks
     finished.

     MEASURED at 375x667 (iPhone SE) on the Help screen, dark and light alike:

         first topic card    ends at   y = 589
         dock top edge       sits at   y = 594
         second topic card  starts at  y = 601   (behind the dock)
         rest of the page                        706px, wholly unseen

     Five pixels of clear air and then a bar. Nothing torn, nothing clipped,
     nothing to scroll towards. At 390x844 — the viewport two thirds of this
     suite used to run at — that second card is plainly visible, which is
     precisely why 177px of extra screen hid the defect for so long.

     IT IS AN INDICATOR, NOT A CONTROL. `pointer-events: none` is load-bearing:
     it sits directly above the dock, and a tappable thing there would compete
     with the navigation underneath it. app.js shows it only while there is
     genuinely more document below the fold and removes it at the bottom, so
     it can never say "more below" when there is not.

     PHONE SHELL ONLY. At >=900px there is no fixed dock to hide the boundary,
     the rail is beside the content rather than over it, and the browser's own
     scrollbar answers the question. */
  .morebelow {
    position: fixed;
    left: 0;
    right: 0;
    /* Immediately above the dock — never over it, so it cannot obscure a
       navigation label. --phonedock already carries the safe-area inset. */
    bottom: var(--phonedock, 0px);
    z-index: 24;
    height: 34px;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: 4px;
    pointer-events: none;
    /* The page fading INTO the bar is half the message; the chevron is the
       other half. `transparent` is a system colour in some engines, so the
       start stop is the ground colour at zero alpha rather than the keyword.
       `--bg`, NOT `--ink`: this is a fade to the PAGE'S OWN BACKGROUND, and
       `--ink` is the dark-text-on-a-bright-surface token (`.btn` on --glow,
       `.avatar`'s fill, etc.) — it only happened to read correctly here
       because dark theme's --ink (#0A1020) and --bg (#09101D) are both
       near-black. Light theme's --ink is #16233F, a dark navy meant for text
       on white, so the gradient faded to a dark smear on a white page
       instead of to white. www/assets/site.css's identical strip already
       fades to `--bg` and has never shown this — this brings app.css's copy
       in line with it rather than inventing a new light-theme override. */
    background: linear-gradient(to bottom,
      color-mix(in srgb, var(--bg) 0%, transparent),
      color-mix(in srgb, var(--bg) 62%, transparent));
  }
  .morebelow[hidden] { display: none; }
  .morebelow-chev {
    display: block;
    width: 13px;
    height: 13px;
    border-right: 2px solid var(--haze);
    border-bottom: 2px solid var(--haze);
    transform: rotate(45deg);
    /* The rotated square's own corner sticks out below its box; pull it back
       so the visible arrow is centred in the strip rather than clipped. */
    margin-bottom: 5px;
  }
  @media (prefers-reduced-motion: reduce) {
    /* Nothing here animates, and nothing here should start to. Stated so a
       later "gentle bounce" has to argue with a rule rather than an omission. */
    .morebelow { animation: none; transition: none; }
  }
  @media (forced-colors: active) {
    /* In forced colours a gradient is dropped and the chevron would vanish
       with it, so the strip keeps a real border the OS will paint. */
    .morebelow { background: none; border-top: 1px solid CanvasText; }
    .morebelow-chev { border-right-color: CanvasText; border-bottom-color: CanvasText; }
  }

  body.signed-in #nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 25;
    margin: 0;
    padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    border-right: 0;
    border-radius: 0;
    background: color-mix(in srgb, var(--dusk) 94%, transparent);
    backdrop-filter: blur(18px);
    box-shadow: 0 -12px 30px rgba(0, 0, 0, .18);
    display: block !important;
  }
  body.signed-in #navRow {
    display: flex;
    flex-wrap: nowrap;
    gap: 4px;
    overflow: hidden;
  }
  body.signed-in #navRow .navbtn {
    display: none;
    box-sizing: border-box;
    flex: 1 1 0;
    min-width: 0;
    width: auto;
    min-height: 56px;
    padding: 8px 4px;
    border: 1px solid transparent;
    border-radius: 14px;
    background: transparent;
    color: var(--haze);
    font-size: 11px;
    line-height: 1.1;
    text-align: center;
    justify-content: center;
    white-space: normal;
  }
  body.signed-in #navRow [data-screen="overview"],
  body.signed-in #navRow [data-screen="kids"],
  body.signed-in #navRow [data-screen="devices"],
  body.signed-in #navRow [data-screen="activity"] {
    display: flex;
  }
  body.signed-in #navRow .navlabel,
  body.signed-in #navRow #navChildren,
  body.signed-in #navRow .navadd {
    display: none;
  }
  body.signed-in #navRow .navbtn[aria-current="page"] {
    background: var(--glow);
    color: var(--ink);
    border-color: var(--glow);
  }
  /* THE WAITING BADGE, IN A 90px TAB. See requestsBadge() in app.js for the
   * measurement: as one nowrap pill reading "1 waiting" it was 71px wide and
   * overhung its own button by 16px, sitting on top of the Devices tab beside
   * it (24px at 320). The count is the part that carries information and it
   * stays; the word is what goes. `.pill-word` is still in the DOM for the
   * accessible name to have company, and the badge's own `aria-label` carries
   * the whole "N waiting" sentence, so nothing a screen reader hears changes.
   *
   * A CORNER BADGE RATHER THAN AN INLINE ONE, because these tabs stack their
   * label centred and a badge in the text flow pushes the label off centre —
   * one tab of four would sit lower than its neighbours. Absolute keeps the
   * four labels on one baseline, and `position: relative` is set on the button
   * here rather than assumed — nothing else in this stylesheet gives `.navbtn`
   * a containing block. */
  body.signed-in #navRow .navbtn { position: relative; }
  body.signed-in #navRow .navbtn .pill.wait {
    position: absolute;
    top: 4px;
    right: 6px;
    min-width: 18px;
    padding: 1px 5px;
    font-size: 10px;
    line-height: 16px;
    text-align: center;
  }
  body.signed-in #navRow .navbtn .pill.wait .pill-word { display: none; }
  body.signed-in #nav.mobile-open {
    top: auto;
    bottom: calc(82px + env(safe-area-inset-bottom, 0px));
    left: 12px;
    right: 12px;
    max-height: min(72vh, 620px);
    padding: 14px;
    border: 1px solid var(--line);
    border-radius: 22px;
    overflow-y: auto;
    box-shadow: 0 24px 70px rgba(0, 0, 0, .4);
    z-index: 31;
  }
  body.signed-in #nav.mobile-open #navRow {
    flex-wrap: wrap;
    overflow: visible;
    gap: 6px;
  }
  body.signed-in #nav.mobile-open #navRow .navbtn,
  body.signed-in #nav.mobile-open #navRow .navadd {
    display: flex;
    flex: 1 1 calc(50% - 6px);
    min-width: 130px;
    width: auto;
    min-height: 48px;
    padding: 10px;
    text-align: left;
    justify-content: flex-start;
  }
  body.signed-in #nav.mobile-open #navRow .navlabel {
    display: block;
    flex: 0 0 100%;
    padding: 10px 8px 2px;
  }
  body.signed-in #nav.mobile-open #navRow #navChildren {
    display: contents;
  }
  body.signed-in #nav.mobile-open #navRow .navbtn .pill.wait .pill-word {
    display: inline;
  }
  body.signed-in #nav.mobile-open .navbrand {
    display: flex;
    padding-bottom: 10px;
  }
  body.signed-in #nav.mobile-open .navfoot {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
  }
  body.signed-in #nav.mobile-open .navfootbtn,
  body.signed-in #nav.mobile-open .navfoot .navuser,
  body.signed-in #nav.mobile-open .navfootlook,
  body.signed-in #nav.mobile-open .navfoot .navtheme,
  body.signed-in #nav.mobile-open .navfoot .navpause,
  body.signed-in #nav.mobile-open .navfoot .navsignout {
    display: flex;
  }
  body.signed-in #nav.mobile-open .navfoot .langsel {
    display: block;
    width: 100%;
  }
  body.signed-in #main {
    padding-bottom: 18px;
  }
}

/* ==========================================================================
   PER-CHILD SCREENS — RESTYLE 2026-08-29
   js/child-settings.js, js/schedule.js, js/time.js, js/sites.js.

   UNSCOPED ON PURPOSE, and appended as one block. style-vocabulary.test.js
   scans app.js and index.html only, so a class used solely inside a js/*.js
   module is invisible to it — the `.cardhead` comment just above already
   records that this exact gap once shipped an unstyled block. Everything
   those modules need therefore lives here, keyed on nothing but the class.

   NO NEW COLOURS AND NO NEW SHAPES. Every rule below either places an
   EXISTING vocabulary class (.ovhead/.ovtiles/.cardhead/.avatar/.badge/.pill)
   onto a screen that did not have it, or draws a hairline out of --line. The
   four screens' own honesty vocabulary — Applied ✓ / Pending… — is untouched.
   ========================================================================== */

/* 1. #childMount IS A MOUNT DIV ONE LEVEL BELOW `.screen`, exactly like
   #overviewMount and #engageMount, and it had none of their grid rules — so
   at >=900px it was placed into ONE of `.screen`'s two tracks and the whole
   per-child settings screen rendered down the left half of a laptop with the
   right half empty. Same three lines those two mounts already carry. Theme
   Studio spans both tracks: its own `auto-fit, minmax(210px, 1fr)` theme grid
   has nothing to lay out inside half a column. */
@media (min-width: 900px) {
  #childMount {
    grid-column: 1 / -1; display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px; align-content: start;
  }
  #childMount > .card { margin-top: 0; margin-bottom: 0; }
  #childMount > .ovhead, #childMount > .faces, #childMount > .ovtiles,
  #childMount > .banner, #childMount > .muted, #childMount > .help,
  #childMount > .theme-studio { grid-column: 1 / -1; }
  /* The `.ovhead` block states the screen AND names the child; the generic
     "Child" title above it would be the same idea twice, and less specific.
     Exactly the rule `#overview > .toprow` already carries, for the same
     reason. Only where a rail exists — below 900px the toprow is the only
     screen title there is until the mount renders. */
  #child > .toprow { display: none; }
}

/* 2. The child screen's page header: Home's own `.ovhead` eyebrow/title/sub
   block, plus one status pill — the reference design's "3/3 DEVICES ONLINE",
   in the same place it puts it, sharing the eyebrow's line.

   THE PHONE RULES BELOW ARE MEASURED, NOT TASTE. At 390x844 the previous
   version of this header (pill on its own row, 22px title, 14px sub, plus an
   eyebrow duplicating the `.toprow` title that only exists below 900px) put
   the delivery banner's bottom edge at 871px — 27px under the fold, the exact
   failure protection-honesty.test.js was written after. With these four rules
   it lands above it again. */
.child-head { display: block; }
.child-headtop { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* IT NO LONGER FLOATS IN THE UPPER-RIGHT WHITESPACE — owner brief 2026-09-20:
   "It never floats by itself in the page's upper-right whitespace" / "The
   status is attached to the section it describes." `margin-left: auto` pushed
   this pill to the far end of an otherwise near-empty crumb row, which is the
   entire reason it read as an unrelated control dropped into the corner. Sat
   inline after the crumb it reads as part of the child identity band it is
   about. The narrow override below (`margin-left: 0` under 899px) is now what
   every width does — it is kept rather than deleted so the two blocks still
   agree if either is edited alone. */
.child-headtop > .pill { white-space: normal; }
/* `.pill.neutral`'s fill is `--dusk2`, which is one shade from `--bg` on the
   light page. Everywhere else that pill sits inside a `.card`, where the two
   do differ; this header sits on the page background, where it read as plain
   text. One hairline out of --line gives it an edge without giving it a colour
   it has not earned — amber here would claim "a request is waiting", which a
   sleeping tablet is not. */
.child-headtop > .pill.neutral { border: 1px solid var(--line); }
@media (max-width: 899px) {
  /* `#child > .toprow` still reads "Child" at this width — it is only hidden
     where the rail exists — so the eyebrow here would be the same idea a
     third time, above a title that already names the child. */
  .child-head .ov-eyebrow { display: none; }
  .child-headtop > .pill { margin-left: 0; }
  .child-head .ov-greet { font-size: 20px; margin: 4px 0 3px; }
  .child-head .ov-sub { font-size: 13px; }
}

/* 3. The "at a glance" strip — Home's own `.ovtiles`, so no new tile shape.
   HIDDEN BELOW 900px, deliberately, and MEASURED rather than assumed: at
   390x844 `.ovtiles`' auto-fit grid collapses to one column, the four tiles
   stack 374px tall, and the delivery banner's bottom edge moves from 804px to
   1178px — "Do these settings reach the tablets?", the one question this
   screen exists to answer, ends up a whole viewport below the fold. Nothing is
   lost by hiding it there: every number in it is also stated in words further
   down this same screen (the tablet count is the tablets card's badge, the
   window count is the schedules card's badge, the cap is the number in the cap
   box, the theme is the "Current theme:" line). */
.child-glance { margin: 2px 0 0; }
@media (max-width: 899px) { .child-glance { display: none; } }

/* 4. js/time.js's exact/device-specific controls are a native disclosure.
   The daily-limit slider and week remain the immediate Time workspace; this
   summary keeps the device's real Applied/Pending status visible without
   making every visit include five long settings sections. The summary is the
   shared `.cardhead`, so its whole 44px row is the keyboard/touch target. */
.time-advanced > .time-advanced-summary {
  min-height: 44px;
  margin: -8px -4px;
  padding: 8px 4px;
  cursor: pointer;
  list-style: none;
}
.time-advanced > .time-advanced-summary::-webkit-details-marker { display: none; }
.time-advanced > .time-advanced-summary::before {
  content: "▸";
  flex: none;
  color: var(--glow);
  font-size: 14px;
}
.time-advanced[open] > .time-advanced-summary {
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.time-advanced[open] > .time-advanced-summary::before { content: "▾"; }
.time-advanced > .time-advanced-summary > .sect { min-width: 0; margin-bottom: 0; }
.time-advanced > .time-advanced-summary:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 3px;
  border-radius: 10px;
}

/* Once open, the five existing sections keep their light hierarchy: one
   hairline and a block of space, with no nested cards or extra background. */
.tsec { border-top: 1px solid var(--line); margin-top: 18px; padding-top: 16px; }
.tsec > .fld { margin-bottom: 6px; }

/* 5. js/schedule.js's per-child router. #schedulesMount already sits inside
   `.card#schedulesCard` (index.html), so the router's content is a plain
   block: a second `.card` there drew a bordered, shadowed box inside a
   bordered, shadowed box — two frames around one list. */
.schedRouter { display: block; min-width: 0; }

/* 6. `Pending…` FAILS AA IN THE LIGHT THEME ON AN `.arow`, and this is a real
   axe-core failure rather than a computed worry. `.pill.wait` is
   `var(--amber)` on `rgba(255,194,75,.14)`; over `.card`'s white that
   composites to rgb(255,247,230) and #9B6500 measures 4.63:1, which passes —
   but over `.arow`'s `--dusk2` (#F6F9FC) it composites to rgb(247,241,227)
   and the same colour measures 4.38:1, which does not. That is the identical
   0.04-0.16-short-of-4.5 shortfall this file's own --haze and .ghost.danger
   comments already record, on the identical kind of small label.

   PRE-EXISTING, and MEASURED as such: an axe run against this branch's base
   commit reports the same `color-contrast` violation on the tablets card's own
   Pending pill. This restyle moves the Theme Studio's delivery rows onto the
   same `.arow` shape, which would have made it two instances, so it is fixed
   at the source instead. #7A4F00 is not a new colour — `.banner.warn`'s own
   light override two hundred lines up already uses it for exactly this
   "amber text on a light page" problem — and it measures 6.3:1 there.

   `.pill.on` and `.pill.off` already carry light-theme literals for the same
   reason; this is the third of the three, and it covers BOTH light paths (the
   explicit toggle and the untoggled OS preference), which the other two
   currently do not.

   GONE 2026-09-06, AND MEASURED GONE. Light `--amber` IS #7A4F00 now (see its
   token comment: it was #9B6500 above a comment quoting a different colour's
   measurement, and seven surfaces besides this one were failing AA on it). So
   `:root[data-theme="light"] .pill.wait { color: #7A4F00 }` had become a
   restatement of `.pill.wait { color: var(--waiting-text) }`, twice over — it was
   duplicated line-for-line. Re-measured after removing both copies, worst pixel
   under the glyphs: 6.65:1 on a card, 6.00:1 on an `.arow`, 5.39:1 on the page.
   Identical to the numbers with the overrides in place, because the colour is
   identical. The finding this comment records is kept; only the now-redundant
   rules are gone. */

/* ==========================================================================
   SCREEN HEADERS — REDESIGN 2026-08-29, WAVE 2
   Devices / Requests / Activity / Processes, brought to the same shape as the
   Hands-On reference build (mockups/ParentProof-Hands-On-Ordered-Demo-
   2026-08-24.zip, `parentproof-complete-clickable-demo.html`).

   REBASED ONTO #471-#488, AND DELIBERATELY MUCH SMALLER THAN IT WAS. The
   Processes table (`.prochead`/`.procrow`/`.procwhat`), the activity row's
   leading glyph (`.acticon`/`.acttime`), the device cards, the Requests rows
   and the whole `#settingsMount` group rail all landed on main first, in
   #472/#473/#479 — the block immediately above this one. Everything this pass
   had written for those five surfaces was dropped in the rebase rather than
   shipped a second time under a second set of class names. What is left is
   the one thing main still does not have: the eyebrow / big title / one
   explaining line ABOVE each screen, and the two page-level layout bugs that
   pass found by rendering the page.

   THE RULE THIS SECTION FOLLOWS. Nothing below invents a second design
   system. Every colour is an existing token, every radius/space matches
   `.card`/`.arow`, and the reference build's `.top` (eyebrow + big title +
   sub) is this file's existing `.ovhead`, extended rather than duplicated.

   REMINDER FOR THE NEXT MODULE AUTHOR. style-vocabulary.test.js scans
   index.html and app.js ONLY — a class used solely inside a js/*.js module is
   invisible to it, which is why `.cardhead` above carries the same warning.
   Everything here is unscoped and shared for that reason. Grep before you
   invent.
   ========================================================================== */

/* ------------------------------------------------------- the screen header */
/* `.ovhead` shipped in wave 1 as three stacked blocks (eyebrow / greeting /
   sub) and js/overview.js — and now js/account.js's own `#settingsHead` —
   still use it exactly that way, so the flex row is opt-in via `:has(.ov-who)`
   rather than applied to every `.ovhead`. Home and Account are not touched by
   this rule at all. A screen that wants the reference build's
   header-with-a-status-pill wraps its three lines in `.ov-who` and drops a
   `.pill` beside it; a screen that does not, keeps the stack.
   `:has()` is already used by this file (`.wrap:not(:has(.nav.hide))`). */
.ovhead:has(.ov-who) { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; }
.ovhead .ov-who { flex: 1 1 auto; min-width: 0; }
.ovhead > .pill, .ovhead > .badge { flex: none; }
/* The header's own trailing control (Log out, on Devices) sits centred against
   the title block rather than stretching to its full height. */
.ovhead > .ghost { flex: none; align-self: center; }
/* The eyebrow/sub already have sizes from the wave-1 block above; only the
   headline needs toning down from Home's 27px greeting, which is deliberately
   the largest type on the product. */
.ovhead .ov-title { font-size: 24px; font-weight: 800; letter-spacing: -.02em; margin: 5px 0 3px; color: var(--cloud); }
@media (max-width: 899px) { .ovhead .ov-title { font-size: 21px; } }
/* The parent's own email under the Devices title. Same role `.toprow .id` had
   before this header replaced it, same muted 11px. */
.ovhead .ov-id { font-size: 11px; color: var(--haze); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Devices keeps its two household actions with the title. They are still the
   same controls and IDs, but the header now makes the next useful action
   visible before the device list starts. */
.ovhead-actions {
  display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap;
  gap: 8px; margin-left: auto; min-width: 0;
}
.ovhead-actions .btnlink,
.ovhead-actions > button { width: auto; min-height: 44px; margin: 0; }
.ovhead-actions #pauseAllMsg { flex-basis: 100%; min-width: 220px; }
.ovhead-actions #pauseAllMsg:empty { display: none; }
#addAnotherDeviceMobileMsg { display: none; }
@media (max-width: 560px) {
  .ovhead-actions { width: 100%; margin-left: 0; justify-content: stretch; }
  .ovhead-actions .btnlink, .ovhead-actions > button { flex: 1 1 180px; }
  #addAnotherDeviceMobileMsg { display: block; flex-basis: 100%; margin: 0; }
  .ovhead-actions .btnlink { display: none; }
  /* The hide above exists because ADDING a device needs a desktop computer.
     BUYING one does not, so the shop doorway stays on a phone. `.is-hidden`
     is !important and still wins while the catalogue is empty. */
  .ovhead-actions #shopLink { display: flex; }
}

/* --------------------------------------------- spanning the two-track grid */
/* THE TRAP THE `#overviewMount` COMMENT ABOVE ALREADY DESCRIBES, one more
   time, for the elements this wave adds: a direct child of `.screen` that is
   not in this list occupies ONE of the two tracks and renders at half width —
   and if it is itself a grid, at a quarter. A screen header and a page-level
   banner are about the PAGE, not about one card in it, so both span. */
@media (min-width: 900px) {
  .screen > .ovhead, .screen > .banner { grid-column: 1 / -1; }
}

/* --------------------------------- THE QUARTER-WIDTH MOUNT BUG, ONCE MORE */
/* `#overviewMount`'s own comment above describes this exactly, and
   `#settingsMount` (in the block above, from the Account pass) was the second
   case. `#requestsMount` is the third and last: a wave-0 mount div is a DIRECT
   CHILD of `.screen`, `.screen` is a two-track grid at this width, and the
   mount is not a `.card` — so it silently occupies ONE track, and anything it
   holds comes out half-width with a wall of empty page beside it.

   The mount is empty today (the duplicate composers it used to hold are gone —
   see index.html's own note on it), so nothing is currently mis-sized by this.
   The rule stays anyway, exactly as its two siblings do, because the trap is a
   property of being a wave-0 mount div, not of what happens to be inside one
   this week: the next card added here would hit it on its first paint. */
@media (min-width: 900px) {
  #requestsMount { grid-column: 1 / -1; }
}

/* -------------------------------------------------- one long list, one page */
/* MEASURED, NOT GUESSED (real Chromium, 1440x900): Activity's single card
   rendered 544px wide inside a 1106px screen, with 560px of empty page beside
   it — a day-grouped, filterable ledger squeezed into half the width while the
   other half held nothing at all. It is the exact complaint the sidebar
   redesign above exists to answer, one screen further on.

   A screen whose whole content is ONE list spans both tracks. Scoped by id
   rather than by `.card` because this is a statement about these screens'
   content — one list — not about cards in general; Devices and Settings really
   do want two columns of independent cards and are deliberately not here.
   `#activitySignpost` is a `.card` too and spans harmlessly while hidden. */
@media (min-width: 900px) {
  #activity > .card, #processes > .card { grid-column: 1 / -1; }
}

/* ------------------------------- THE INVISIBLE CURRENT-SCREEN NAV ENTRY */
/* A REAL BUG, FOUND BY RENDERING THE PAGE, not by reading it — and it was on
   every screen at once.
 *
 * `.navbtn[aria-current="page"]` (above) paints the current screen: cyan fill,
 * ink text, and a light-theme override that switches the text to white for
 * contrast on the darker light-theme --glow. In the >=900px SIDEBAR, though,
 * the rail rule `#navRow .navbtn { background: transparent; border-color:
 * transparent }` is specificity (1,1,0) against that rule's (0,2,0) — an id
 * beats two classes — so the fill was thrown away while the white text
 * override, which sets only `color`, survived.
 *
 * MEASURED IN REAL CHROMIUM at 1440x900, light theme: the current entry
 * rendered #FFFFFF text on the rail's own #FFFFFF panel — contrast 1.0:1, the
 * label simply gone. The dark theme had the mirror-image defect (--ink text on
 * the --dusk panel). A parent could not see which screen they were on.
 *
 * The fix is specificity, not a new colour: the same three rules the row
 * version already has, re-stated at (1,2,0)+ so they outrank the rail's own
 * reset. Nothing about the palette or the aria contract changes. */
@media (min-width: 900px) {
  #navRow .navbtn[aria-current="page"] {
    background: var(--glow); border-color: var(--glow); color: var(--ink);
  }
  /* Hover must not eat the current-screen fill either — `#navRow .navbtn:hover`
     is the same (1,1,1) trap one rule further on, and it is what actually made
     the defect visible in the screenshot that found this. */
  #navRow .navbtn[aria-current="page"]:hover { background: var(--glow); }
  /* The binding light-theme rail keeps selection inside the navy surface:
     white type on a restrained translucent-white fill, rather than importing
     the page's solid-blue primary action treatment into the navigation. */
  :root[data-theme="light"] #navRow .navbtn[aria-current="page"],
  :root[data-theme="light"] #navRow .navbtn[aria-current="page"]:hover {
    background: rgba(255,255,255,.1); border-color: transparent; color: #FFFFFF;
  }
}
/* The `@media (prefers-color-scheme: light)` half that used to live here is
   gone, same reason and same date as the amber patch documented later in this
   file: light is reached ONLY through `data-theme="light"` (DARK IS THE
   DEFAULT, not a browser preference — see app.js's init), so in the untoggled
   state the dark tokens are in force and this selector's dark colour is
   correct. An OS-preference query painting white onto it regardless was the
   defect a parent whose system prefers light actually saw. */

/* ===========================================================================
 * PER-CHILD REQUEST HANDLING (#requestHandlingPanel, on the `child` screen)
 * platform/public/js/requests.js, paintCard
 * ===========================================================================
 *
 * MOVED HERE FROM #requests 2026-09-17 (GoodQA 8c11ca92) — one card, one
 * child, no multi-child comparison grid: the screen this card lives on is
 * already scoped to one child (ctx.childProfile()), the same way apps/sites/
 * schedules are, so there is no second box to lay out beside it any more. The
 * reference build's "Request handling" card (Hands-On demo,
 * `requestPolicyCard`) drew one bordered box per child inside a single
 * household-wide card; this card keeps that box's own inner shape (a live
 * usage line + status pill at the top, then the controls) but not the
 * multi-child wrapper, since there is only ever one child here.
 *
 * `.screen` IS A TWO-TRACK GRID at >= 900px (see its own rule near the top of
 * this file). `#requestHandlingPanel` is a plain `.card`, and `.card` already
 * takes `grid-column: 1 / -1` generically — unlike the old
 * `#requestHandlingMount`, this one needs no rule of its own here.
 *
 * EVERY CLASS BELOW IS USED ONLY FROM js/requests.js, WHICH
 * style-vocabulary.test.js DOES NOT SCAN (it reads app.js and index.html
 * only). So the pairing is checked by hand and stated here: `.reqpolicy-box`,
 * `.reqpolicy-head`, `.reqpolicy-switch`, `.reqpolicy-notice`,
 * `.reqpolicy-preview`, `.reqpolicy-previewsheet`, `.reqpolicy-previeweyebrow`,
 * `.reqpolicy-previewtitle` and `.reqpolicy-msg` are the complete set the
 * module emits, and all nine are defined in this block. Everything else the
 * card uses (`.card`, `.cardhead`, `.sect`, `.help`, `.fineprint`, `.inp`,
 * `.fld`, `.muted`, `.tiny`, `.small`, `.packet5-field`,
 * `.pill.on/.off/.neutral`) already existed. Two things the pre-2026-09-17
 * version of this card used are gone along with the multi-child grid they
 * existed for: `.reqpolicy-grid` (the two-column comparison layout, not
 * redefined below) and `.reqpolicy-who`/`.pill.trusted` (the "Per child"
 * name + badge this card no longer draws — the screen it is on already names
 * the child).
 */

.reqpolicy-box {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--dusk2); padding: 14px; min-width: 0;
}

/* Usage on the left, the one status pill hard right — the same `.cardhead`
   shape, kept local because this box is not a card and must not inherit a
   card heading's margins. */
.reqpolicy-head { display: flex; align-items: flex-start; gap: 10px; justify-content: space-between; }

/* The status above stays scannable; native <details> owns the longer editor.
   `.adv` supplies the shared disclosure marker and divider, while this larger
   summary remains an obvious 44px keyboard/touch target. */
.reqpolicy-editor > summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--glow);
  font-size: 13px;
  font-weight: 750;
}
.reqpolicy-controls { padding-top: 2px; }

/* THE INFORMATIONAL PILL. Cyan/blue is UI-DIRECTION-2026-08-22 §3's "trusted
   information" — it makes no claim about health, attention or damage, so it
   must not borrow mint, amber or coral. `--glow` is this file's own name for
   that hue (see the token map at the top). Same geometry as every other
   `.pill`; only the two colours are new.
   NOT USED BY THIS BLOCK ANY MORE (js/requests.js dropped the "Per child"
   badge it used to draw here, 2026-09-17 — the screen it moved to already
   names the child) but still declared, because js/desktop-preview.js's own
   pairing comment a few hundred lines down assumes it already exists. */
.pill.trusted { background: rgba(105, 216, 255, .16); color: var(--glow); }
/* GOODQA #465/#468 — light --glow moved teal -> brand blue. Bare var(--glow)
   as text on THIS pill's own translucent fill (not solid white) is marginal:
   computed 4.60:1 over a white card, 4.37:1 over --dusk2 — below 4.5 on the
   card colour this pill actually sits on. #1B4A73 is not a new colour: it is
   the handoff's own "deep/hover" stop for this same blue, and clears both
   (9.24:1 / 8.7:1+) with room to spare, same "same hue, darkened, measured"
   discipline --haze/--amber/--sky use elsewhere in this file. */
:root[data-theme="light"] .pill.trusted { background: rgba(8, 125, 169, .12); color: #1B4A73; }

/* A checkbox and its sentence on one line, aligned at the first text line
   rather than centred — a two-line label next to a centred box reads as a
   mistake. */
/* THE LABEL IS THE TARGET, AND IT WAS 21px TALL. A sweep that measured the
   <input> reported 13x13 and pointed at the wrong thing: this label wraps the
   box AND its sentence, so the region that accepts the click is the whole row,
   and that row is what has to clear the minimum. MEASURED with every
   disclosure on the Requests screen open: 21px at 440 and 1440, and 42px at
   320 only because the sentence wrapped to two lines. 21px fails WCAG 2.2 AA
   2.5.8 (24x24), not merely the 44px target this portal is held to.

   `padding-block` RATHER THAN A BARE `min-height`, because `align-items:
   flex-start` above is deliberate — a two-line label beside a centred box
   reads as a mistake — and a 44px floor on its own would pin a one-line
   sentence to the top of the box with 23px of dead space under it. Padding
   grows the row symmetrically and keeps the checkbox on the first text line,
   which is exactly what that decision asks for. The min-height stays as a
   floor for any future shorter content. */
.reqpolicy-switch {
  display: flex; align-items: flex-start; gap: 9px;
  min-height: 44px; padding-block: 11px;
  margin-top: 12px; line-height: 1.4; cursor: pointer;
}
.reqpolicy-switch > input { margin-top: 2px; flex: none; }

/* The reference build's `.notice` around the redirect switch: the one control
   in the box that carries an explanation and a preview, boxed so it reads as
   one thing. */
.reqpolicy-notice {
  margin-top: 12px; padding: 11px 12px; border-radius: 13px;
  border: 1px solid var(--line); background: var(--dusk);
}
.reqpolicy-notice > .reqpolicy-switch { margin-top: 0; }
.reqpolicy-notice > .small { margin: 6px 0 0; }

.reqpolicy-preview { margin-top: 8px; }
.reqpolicy-preview > summary { cursor: pointer; font-size: 12px; color: var(--glow); }

/* The child's own page, quoted. Deliberately NOT restyled into a child-surface
   pastel: this is a parent-surface quotation of what a child will read, and
   painting a fake device frame around it would invite it to be mistaken for a
   live view of the tablet. */
.reqpolicy-previewsheet {
  margin-top: 8px; padding: 12px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--dusk2);
}
.reqpolicy-previeweyebrow {
  font-size: 10px; font-weight: 800; letter-spacing: 1px; color: var(--done-text);
}
.reqpolicy-previewtitle { font-size: 15px; font-weight: 700; margin-top: 4px; }
.reqpolicy-previewsheet > .small { margin: 6px 0 0; }

/* The per-box save line. `.muted` already carries the colour; this only stops
   it colliding with the control above it. */
.reqpolicy-msg { margin-top: 10px; }

/* ==========================================================================
   MAC CHILD-EXPERIENCE PREVIEW — js/capability.js, on the device policy screen
   (mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   parentproof-secured-desktop-macos.html: "Preview Leo's real desktop
   experience").

   DIRECTLY ABOVE THIS BLOCK, ON PURPOSE. `.reqpolicy-previewsheet` is the only
   other preview of a child's screen this product ships, and its own comment
   eight lines up is the rule this block obeys: a parent-surface QUOTATION of
   what a child will read, never a fake device frame, because a frame invites
   the quotation to be mistaken for a live view of the device. The mockup this
   parity pass works from is a pixel-realistic simulated macOS desktop —
   wallpaper, Dock, a running clock, a "Protected · Wi-Fi" tray. None of that
   chrome is reproduced here for exactly that reason, and the vocabulary below
   is deliberately a bigger sibling of `.reqpolicy-preview*` rather than a new
   idiom: two quoted screens instead of one, and a two-column boundary list.

   NO NEW COLOURS. --mint/--coral/--haze/--line/--dusk2 are the tokens the rest
   of this stylesheet already uses for healthy/blocked/secondary/border/inset,
   and UI-DIRECTION-2026-08-22 §3 reserves those hues for those meanings. The
   one thing this block does NOT do is paint any part of the quotation mint:
   nothing in a preview has been confirmed by a device.
   ========================================================================== */

#macPreviewCard .fld { margin-top: 16px; }
#macPreviewCard > .help { margin: 6px 0 0; }

/* THE HEAD. `.ovhead` supplies the eyebrow/title sizes (they are unstyled
   outside it); this only puts the PREVIEW badge on the eyebrow's line and steps
   the title down from Home's 27px page-header size, because this is a card
   inside a screen and not the screen's own header. */
.macprev-headtop { display: flex; align-items: center; gap: 8px; }
.macprev-headtop > .ov-eyebrow { flex: 1; }
.macprev-head .ov-greet { font-size: 20px; margin: 4px 0 8px; }

.macprev-preview { margin-top: 12px; }
.macprev-preview > summary { cursor: pointer; font-size: 12px; color: var(--glow); }

/* WHEN this screen appears, above the screen itself — the label a parent needs
   before they can read the quotation as anything but a mystery. */
.macprev-when {
  font-size: 10px; font-weight: 800; letter-spacing: 1px; color: var(--haze);
  margin-top: 12px;
}
.macprev-quote:first-of-type .macprev-when { margin-top: 10px; }

/* The quotation box. An inset panel on the PARENT's dark surface, not a Mac. */
.macprev-sheet {
  margin-top: 6px; padding: 12px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--dusk2);
}
.macprev-qtitle { font-size: 15px; font-weight: 700; }
/* A CAPTION INSIDE THE QUOTATION, and it must not read as part of it. The pause
   wall's middle lines are built at runtime (a schedule label, an "Until HH:MM"),
   so they are described rather than quoted; italic and a dimmer weight is what
   separates "what the Mac says" from "what we are telling you about it". */
.macprev-qcap {
  font-size: 11px; font-style: italic; color: var(--haze);
  line-height: 1.5; margin-top: 8px;
}
.macprev-qlist { margin-top: 6px; }
.macprev-qitem { font-size: 12px; color: var(--haze); line-height: 1.5; }
.macprev-qnote { font-size: 12px; color: var(--haze); line-height: 1.5; margin-top: 6px; }
/* A heading INSIDE the quotation — smaller than the screen's own title, heavier
   than its body, because that is the weight the Mac gives it. */
.macprev-qsub { font-size: 12px; font-weight: 700; color: var(--cloud); margin-top: 12px; }
/* The child's two buttons, drawn as what they are — labels in a quotation, not
   controls. No hover, no cursor change, nothing that invites a parent to press
   a button that belongs to a screen they are not looking at. */
.macprev-qacts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.macprev-qact {
  font-size: 12px; font-weight: 700; padding: 6px 10px; border-radius: 10px;
  border: 1px solid var(--line); color: var(--cloud);
}

/* A gap this Mac is reporting: its pill, then the sentence. */
.macprev-gap { display: flex; align-items: flex-start; gap: 8px; margin-top: 8px; }
.macprev-gaptext { font-size: 12px; color: var(--haze); line-height: 1.5; }

/* The mockup's two boundary columns. `minmax(0,1fr)` rather than `1fr` for the
   reason every other grid in this file uses it: a long sentence in one column
   must not be able to push the other off the card. */
.macprev-bound { display: grid; gap: 12px; margin-top: 8px; }
@media (min-width: 700px) {
  .macprev-bound { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.macprev-boundcol {
  padding: 12px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--dusk2);
}
.macprev-boundhead {
  font-size: 10px; font-weight: 800; letter-spacing: 1px; margin-bottom: 6px;
}
.macprev-boundhead.yes { color: var(--done-text); }
.macprev-boundhead.no { color: var(--coral); }
.macprev-bounditem { font-size: 12px; color: var(--haze); line-height: 1.5; margin-top: 6px; }

/* THE SAME LIGHT-CORAL FIX .ghost.danger / .pill.off / .banner.err ALREADY
   CARRY, applied to this block's two coral TEXT spots. The light palette's
   canonical coral (#D4483D) measures 4.38:1 on white — just under the 4.5 an
   11-13px label needs — and #B23127 is not a new colour but the one those
   three rules already substitute for this identical problem. Reused rather
   than re-derived, and scoped to the explicit light theme only, because dark
   is this product's default and its coral is already well clear on the dark
   ground. */
:root[data-theme="light"] .macprev-boundhead.no { color: #B23127; }

/* ==========================================================================
   OVERVIEW / HOME — parity with the Hands-On reference build
   (mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   parentproof-complete-clickable-demo.html: `<section id="home">`, its
   `childHero()` card and the `.cards four` tile row).

   APPENDED, NOT MERGED, and deliberately so: several screens are being
   restyled in parallel and every rule below is scoped to #overview,
   #overviewMount or an `ov*` class this screen already owns, so nothing here
   can reach another screen's block. The `ov*` vocabulary itself is not new —
   it was introduced by the design-v2 pass a few hundred lines up and is reused
   by Settings and the per-child screen; this block only grows the parts the
   reference build shows larger, plus the three things it shows that had no
   rule at all (the quick-action row, and a tile value that
   is allowed to carry a colour). Find-a-control lives in css/jump.css.
   ========================================================================== */

/* 1. ONE HEADER, NOT TWO. `#overview > .toprow` is the static "Overview"
      title in index.html; js/overview.js renders the real header (date
      eyebrow, greeting, explaining line) into the mount below it. app.css
      already hid the static one at >=900px for exactly that reason — below
      900px BOTH were visible, so a phone opened Home reading "Overview" and
      then "Good evening." one line later.

      sr-only RATHER THAN display:none, and that is the whole point of doing
      it here instead of deleting the markup: app.js's focusScreen() moves
      focus to `.toprow .nm` on arrival, and `.ov-greet` is deliberately NOT in
      that selector (it lives in a mount that is still empty when focus runs —
      see that function's own comment). Hidden visually, still the heading a
      screen reader lands on. */
#overview > .toprow {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* 2. THE QUICK-ACTION ROW, between the attention strip and the child cards —
      `.rowline` (the shared flex row) plus wrapping, because three labelled
      buttons do not fit one 390px line and a row that overflows is worse than
      a row that wraps. */
.ovquick { flex-wrap: wrap; margin-top: 0; }
/* W4.1: .ovquick leads the LEFT (decision) track, not a full-width row —
   see #overviewMount's three-band comment. */
#overviewMount > .ovquick { grid-column: 1; }

/* 4. THE CHILD CARD AS A HERO. The reference build's `.card.hero` is a bigger,
      warmer card than the rest of the page: more padding, a gradient, a larger
      face, name and number. --statustop/--statusbot ARE that gradient's own
      stops — this file's header records that they were lifted verbatim from
      the prototype's `.hero` — so this reuses the tokens rather than pasting a
      second pair of hexes, and it follows the light theme for free. */
.ovcard.hero {
  padding: 24px;
  background: linear-gradient(135deg, var(--statustop), var(--statusbot));
}
.ovcard.hero .avatar.lg { width: 54px; height: 54px; font-size: 20px; border-radius: 17px; }
.ovcard.hero .ovc-name { font-size: 22px; font-weight: 800; letter-spacing: -.01em; }
.ovcard.hero .ovc-state { font-size: 13px; }
.ovcard.hero .ovc-time { font-size: 36px; letter-spacing: -.02em; }
.ovcard.hero .ovbar { height: 9px; }
/* The badge sits on the SAME line as the name, at the top of the card, which
   is where the reference build puts it and where an eye scanning three cards
   expects one column of states. `.ovc-head` already pushes it right. */
.ovcard.hero .ovc-head > .pill { align-self: flex-start; }
@media (max-width: 899px) {
  .ovcard.hero { padding: 19px; }
  .ovcard.hero .ovc-time { font-size: 30px; }
  .ovcard.hero .ovc-name { font-size: 19px; }
}

/* 5. THE TILE ROW — four across, like the reference build's `.cards.four`,
      instead of auto-fit's "as many as fit", which put six 180px tiles on one
      1440px line and made each of them unreadably narrow. Two across on a
      tablet, one on a phone. */
@media (min-width: 1100px) {
  .ovtiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 620px) and (max-width: 1099px) {
  .ovtiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 619px) {
  .ovtiles { grid-template-columns: 1fr; }
}
/* A TILE VALUE MAY CARRY A COLOUR ONLY WHERE THE NUMBER SAYS THE SAME THING.
   js/overview.js emits `.ok` only when the confirmed count EQUALS the total
   and `.attn` only when the waiting count is above zero — so mint here can
   never claim more than the cards beneath it, which is the one thing the
   reference build's unconditional green "5 healthy" does claim. Same two
   meanings §3 reserves these hues for: mint healthy/applied, amber a thing
   waiting for the parent. */
.ovtile .ovt-v.ok { color: var(--done-text); }
.ovtile .ovt-v.attn { color: var(--waiting-text); }

/* 6. THE ATTENTION STRIP AND THE HEADER SPAN THE TRACKS. `#overviewMount` is
      itself a two-track grid (see its own rule above); a new full-width child
      added here would otherwise take one track and sit at half width. The
      existing rule names .ovhead/.ovstrip/.ovtiles/.help/.muted — this adds
      the one child this pass introduces. */
@media (min-width: 900px) {
  /* W4.1: .ovquick wears `rowline ovquick`, so this rule was ALSO holding it
     full-width — three separate rules had to change to move one element, which
     is why the first attempt measured no effect on the quick-action row.
     Any OTHER .rowline still spans; only the quick-action row is placed. */
  #overviewMount > .rowline { grid-column: 1 / -1; }
  #overviewMount > .rowline.ovquick { grid-column: 1; }
}

/* 7. THE QUICK-ACTION ROW'S OWN BUTTON SIZING. `.primary` is `width: 100%`
      by default — correct for a card's single submit, wrong for one of three
      buttons on a line, where it took the whole row and pushed the other two
      onto a second one (measured, not guessed: it is what the first
      screenshot of this row showed). `#stepUpRow` makes exactly this
      correction for exactly this reason a few hundred lines up. The margin
      reset goes with it: `.rowline` already owns the space above the row. */
.ovquick .primary, .ovquick .ghost { flex: none; width: auto; margin-top: 0; }
.ovquick .primary { padding: 12px 16px; font-size: 14px; min-height: 44px; }
/* GoodQA ticket 488 — real inline-stroke icon beside the label (ctx.icon), replacing
   the ♡/★ glyph that used to be baked into the translated string itself. */
.ovquick .primary, .ovquick .ghost { display: inline-flex; align-items: center; gap: 8px; }
.ovquickicon { width: 16px; height: 16px; flex: none; }

/* 8. THE "WAITING FOR YOU" CARD SITS IN THE LEFT (DECISION) TRACK.
      It used to span both, and the reasoning was sound at the time: it is a
      LIST of full sentences, and a list in one HALF-width column reads badly
      beside an empty other half.
      W4.1 changes the premise rather than the conclusion. The other half is no
      longer empty — .ovchildren ("Today at a glance") now occupies it — and
      the left track is 1.3fr (~667px at 1512), not half. The handoff's own
      Pending-requests card measures ~630px in the same viewport, so this is
      her width, not a squeeze.
      It also fixes the defect that motivated the band change: spanning both
      tracks forced this card to the BOTTOM of the page, top edge y=1309 on a
      1772px page, while the banner at y=239 announced the requests it holds. */
@media (min-width: 900px) {
  #overviewMount > .ovalerts { grid-column: 1; }
}

/* 9. THE LIVE DOT. The reference build's `.pulse` is a 9px dot with a soft
      ring around it (`box-shadow: 0 0 0 5px`), which is what makes it read as
      "this is live" rather than as a bullet point. Same shape here, sized to
      this file's own 6px dot and tinted from --mint rather than a fourth copy
      of that literal. */
.ovcard.hero .ovc-state .ovc-dot {
  width: 8px; height: 8px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--mint) 14%, transparent);
}

/* ==========================================================================
   DEVICES — HANDS-ON ZIP PARITY, 2026-08-30
   ==========================================================================
   Appended as its own block, the convention every feature above follows: every
   selector here is either new or a deliberate RE-DECLARATION of one earlier in
   this file, so reverting this pass is a deletion rather than an archaeology
   exercise, and a concurrent edit to another screen's block merges cleanly.

   WHAT IT FINISHES. #473 built the device tile (identity band / health strip /
   action row) but left three pieces of the reference build
   (mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   parentproof-complete-clickable-demo.html: deviceCard() and
   renderDeviceCategory()) unbuilt: the card's rolled-up badge, the pulsing
   status line under the name, and the group header's "N protected devices"
   sentence, which shipped as a bare row count in a chip on the far right.

   NO NEW HEX AND NO NEW COLOUR MEANING (UI-DIRECTION-2026-08-22 §3). mint =
   healthy/applied, amber = waiting, coral = paused/destructive, cyan = trusted
   + primary. The light-theme literals are the ones this file already measured
   for the identical problem — #0A6242 (`.pill.on` light), #B23127
   (`.pill.off`/`.banner.err` light), #FFB4AC (`.pill.off` dark) — never a hue
   picked by eye, because devices-ui.test.js runs axe-core over #devices at
   390x844 and 1280x900 in BOTH themes.
   ========================================================================== */

/* ------------------------------------------------ the group header
   The reference build's header is a glyph, a name, and a muted count sentence
   under the name — not a name with a numeric chip pushed to the far right.
   `.devgrouptitle` and `.devgroupcount` keep their class names and their
   nesting is one level deeper, which devices-ui.test.js's own
   `querySelector('.devgroupcount')` reads unchanged. */
.devgroupicon {
  width: 30px; height: 30px; border-radius: 10px; flex: none;
  background: var(--dusk2); color: var(--glow);
  display: grid; place-items: center; font-size: 13px; line-height: 1;
}
.devgrouptext { display: flex; flex-direction: column; gap: 1px; flex: 1 1 auto; min-width: 0; }
.devgroup > .devgrouphead .devgrouptitle { flex: none; font-size: 15px; font-weight: 750; letter-spacing: -.01em; }
/* Re-declared, not edited: the chip shape was the whole point of the old
   right-aligned count, and this line is a sentence rather than a number. */
.devgroup > .devgrouphead .devgroupcount {
  background: none; border-radius: 0; padding: 0; flex: none;
  font-size: 11.5px; font-weight: 600; color: var(--haze);
}

/* ------------------------------------------------ the four-signal strip
   FOUR ABREAST, which is the shape the design is named for. `auto-fit` with a
   118px floor put three cells on the first row and one alone on the second in
   a 400px card — measured in verification/zip-parity/devices/before-*.png. The
   cells fit four-up because each one is now the reference build's own
   `<b>name</b><span>value</span>` pair (a short value under a tiny heading)
   instead of one long sentence; the `title`/`aria-label` keep the full
   sentences, so nothing a screen reader hears got shorter.
   Two-up under 620px, matching the reference build's own 600px switch. */
.devcard .healthcap { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 9px 10px; }
@media (max-width: 620px) {
  .devcard .healthcap { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Both halves inherit `.hctxt`'s per-signal colour, so the cell is coloured
   exactly as it was before this pass — this only splits one text run in two. */
.hcname {
  display: block; font-size: 10px; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase; line-height: 1.15;
}
.hcval { display: block; margin-top: 2px; font-size: 11.5px; font-weight: 650; line-height: 1.2; }

/* ------------------------------------------------ the identity band
   PAINT ORDER ONLY. The nav column's document order is fixed —
   parent-lock-ui.test.js reads [name row, last seen, platform line] at those
   indices — so the eyebrow, the name and the status line are lifted into the
   reference build's order with `order`, exactly as `.devplat` has been since
   #473. `.devstatus` is appended last in the DOM and lands second here. */
.devcard .devplat { order: -3; }
.devcard .devnmrow { order: -2; }
.devcard .devstatus { order: -1; }
.devstatus {
  display: flex; align-items: center; gap: 7px;
  margin: 3px 0 1px; font-size: 12px; font-weight: 700; color: var(--haze);
}
.devstatustext { min-width: 0; overflow-wrap: anywhere; }
/* The reference build's `.pulse`: a dot with a soft ring, in the line's own
   colour. `currentColor` is what keeps the dot and the words from ever
   disagreeing, and what keeps this to one rule per theme instead of four. */
.devstatus .pulse {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: currentColor;
  box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 16%, transparent);
}
/* TONE COMES FROM deviceRollup(), the same function the pill above it uses, so
   a card can never show a mint dot beside a coral badge. */
.devstatus.tone-on { color: var(--done-text); }
.devstatus.tone-wait { color: var(--waiting-text); }
.devstatus.tone-off, .devstatus.tone-paused { color: #FFB4AC; }
:root[data-theme="light"] .devstatus.tone-on { color: #0A6242; }
:root[data-theme="light"] .devstatus.tone-off,
:root[data-theme="light"] .devstatus.tone-paused { color: #B23127; }
:root[data-theme="light"] .devstatus.tone-on { color: #0A6242; }
:root[data-theme="light"] .devstatus.tone-off,
:root[data-theme="light"] .devstatus.tone-paused { color: #B23127; }
/* A LONG BADGE MAY NEVER EAT THE DEVICE NAME. `.devnmtext` truncates and
   `.devnmrow .pill` does not, so on a 390px card a long label (or a longer
   translation of a short one) spent the NAME's ellipsis instead of its own —
   measured, not guessed: "Maya's spare ta…" in
   verification/zip-parity/devices/after-mobile-390x844.png before this rule.
   The English labels are one word each; this is the safety valve for the
   locales where they are not. */
@media (max-width: 620px) {
  .devcard .devnmrow { flex-wrap: wrap; row-gap: 4px; }
}
/* THE CHILD FILTER CHIPS SIT ON THE HEADER ROW, at its right end, which is
   where the reference build's renderDeviceCategory() puts them
   (`.devicecategoryhead` is one flex row: title block on the left,
   `.devicefilters` on the right) — this shipped with them stacked underneath.
   They stay a SIBLING of <summary> in the markup, because a <button> inside a
   <summary> would toggle the group every time a parent picked a child; only
   their painted position moves.

   `position: absolute` and NOT `display: grid`/`flex` on the <details> itself:
   overriding a <details>'s own display breaks the UA's closed-state hiding of
   its content, MEASURED in this browser (a collapsed group kept rendering its
   cards). Absolute positioning leaves that mechanism untouched.

   Wide screens only. The reference build stacks this same header vertically
   under 920px, and under 900px there is not room for a title and a chip strip
   on one line. */
@media (min-width: 900px) {
  .devgroup { position: relative; }
  .devgroup > .devgrouphead { padding-right: 260px; }
  .devgroup > .chips {
    position: absolute; top: 8px; right: 0; margin: 0;
    max-width: 250px; justify-content: flex-end; flex-wrap: wrap; row-gap: 5px;
  }
  .devgroup > .devgroupbody { margin-top: 10px; }
}
/* The reference build's device glyph at the left of the identity band
   (`.icon.deviceicon`). Appended last in the nav button and lifted to the
   front by `order` alone, because parent-lock-ui.test.js reads the nav column
   at that button's child index 0. Cyan on the card's inset panel — the same
   `--glow`-on-`--dusk2` pair `.devgroupicon` above uses, so the group's glyph
   and its cards' glyphs are one vocabulary and not two. */
.devcard .devicon {
  order: -1; flex: none;
  width: 40px; height: 40px; border-radius: 12px;
  background: var(--dusk2); color: var(--glow);
  display: grid; place-items: center; font-size: 16px; line-height: 1;
}
/* ON A PHONE THE NAME WRAPS INSTEAD OF TRUNCATING. `.devnmtext` carries
   nowrap + ellipsis so a long name cannot push the badge off the card; at
   390px, with the device glyph and a two-word badge on the same line, that
   budget runs out and the name loses its own tail instead ("Maya's spare…",
   measured in verification/zip-parity/devices/after-mobile-390x844.png).
   A device's name is the one string on this card a parent chose themselves
   and the only way they tell two tablets apart, so on a narrow screen it
   takes a second line and the badge keeps its place at the top right. The
   wide-screen ellipsis is untouched — there the budget is real. */
@media (max-width: 620px) {
  .devcard .devnmtext {
    white-space: normal; overflow: visible; text-overflow: clip;
    overflow-wrap: anywhere;
  }
  .devcard .devicon { width: 34px; height: 34px; border-radius: 11px; font-size: 14px; }
}

/* ==========================================================================
   ACTIVITY — THE HOUSEHOLD LEDGER, ZIP PARITY (2026-08-30)
   Appended, never edited into another block: several passes are in this file
   at once and an append is a clean merge on every side.

   SOURCE: mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   `parentproof-complete-clickable-demo.html` — `section#activity` plus
   renderActivity()/.activityline/.filterrow/.filterchip, read line by line
   rather than from a description of them.

   WHAT THE REFERENCE DRAWS, and what was already here before this pass:
   `.activityline` (a clock column, a glyph tile, the sentence) landed in #473
   as `.arow.actrow`/`.acttime`/`.acticon`, and the screen header landed in
   #489 as `.ovhead`. Both are above in this file and neither is restated here.
   What was missing is what this block adds:

     1. `.filterrow`/`.filterchip` — the reference's chip row, which this
        screen did not have at all: its filters were two stacked `.chips`
        rows INSIDE the card, above a heading that repeated the page title.
        The reference puts one wrapping row of pills between the header and
        the card, and gives the card no heading of its own.
     2. The row's third column — the `.pill` naming the child, hard right.

   ONE DELIBERATE DEVIATION, STATED. The reference tints one pill per child
   (blue / purple / green) and paints a selected chip as a dark-blue fill.
   Neither is copied:
     - the row pill is `.pill.neutral`, because mint IS "healthy/applied" on
       this product (UI-DIRECTION-2026-08-22 §3) and a mint pill beside "a site
       was blocked" would read as a health claim about something that is an
       event, not a state. Same column, same weight, honest colour.
     - the selected chip keeps this file's existing `.chip[aria-pressed]`
       cyan fill, which is the same --blue token the reference's own palette
       block at the top of this file was mapped from, at a contrast this file
       has already verified in both themes.
   ========================================================================== */

/* The reference's `.filterrow`: one wrapping row of pills, above the card.
   The two `role="group"` children (by child, by type) stay real elements —
   a screen reader needs to know that "Leo" and "Websites" answer different
   questions — and simply sit next to each other in it, so what a parent sees
   is one continuous row of chips with a slightly wider seam in the middle. */
.filterrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 14px; }
.filterrow > .chips { gap: 8px; margin: 0; }

/* THE `.screen` TWO-TRACK GRID TRAP, for the third time in this file (see
   `#overviewMount` and `#requestsMount`'s own comments above). A direct child
   of `.screen` that does not span occupies ONE of the two tracks — this row
   would have wrapped its chips into a half-width column beside an empty page.
   Below 900px `.screen` is ordinary block flow, where the margin above is what
   separates the row from the card; at 900px+ the grid's own 18px gap does it,
   so the margin comes back off rather than being counted twice. */
@media (min-width: 900px) {
  #activity > .filterrow { grid-column: 1 / -1; margin-bottom: 0; }
}

/* The reference's chip is a full pill at 12px rather than this file's 20px
   radius at 13px. Shape only — the colours, the pressed state and the 44px
   touch target `.chip` already carries are untouched, and the horizontal
   padding stays wide enough to keep the 48px minimum width real. */
.filterchip { border-radius: 999px; font-size: 12px; padding: 9px 14px; }

/* COLUMN THREE OF `.activityline`: who this event belonged to, hard right.
   `.an` is `flex: 1` so this lands at the right edge on its own; `flex: none`
   is `.pill`'s already. Top-aligned to match the reference's `align-items:
   start`, and capped so a long child name cannot squeeze the sentence off a
   390px phone — `.pill` is `white-space: nowrap`, so it ellipsises cleanly. */
#activityList .arow.actrow > .pill {
  align-self: flex-start; margin-top: 2px;
  max-width: 34%; overflow: hidden; text-overflow: ellipsis;
}

/* ==========================================================================
   ZIP PARITY — THE PER-CHILD CLUSTER
   Apps (#inv), Websites (#sites), Categories (#categories),
   Time (#schedules) and Child settings (#child).

   Appended as one block, at the end, so it cannot collide with another
   track's edits above it. Everything here is layout and grouping: no new
   colour, no new token, and nothing that makes a claim. The palette words
   are the ones already in this file — --mint applied/healthy, --amber
   attention, --coral destructive, --glow trusted/primary.

   THE `.screen` GRID TRAP, WHICH THIS BLOCK EXISTS TO GET OUT OF.
   `.screen` is a two-track grid at >=900px, so a `#*Mount` div — a wave-0
   mount point, not a `.card` — silently occupies ONE track. A module that
   then lays its own cards out inside that div renders them at a QUARTER of
   the page width, and a module that renders one tall card leaves the other
   track empty for the length of the screen. #overviewMount's own comment
   further up this file records the same discovery being made the hard way,
   with a screenshot. Every mount this cluster owns is named below.
   ========================================================================== */

@media (min-width: 900px) {
  /* ---- APPS. Scope/delivery is before the list in the DOM as well as on
     screen, so visual, keyboard and screen-reader order now agree. Both span
     the full width; no CSS order inversion is needed. */
  #inv > #appsCard { grid-column: 1 / -1; }
  #inv > #appsMount { grid-column: 1 / -1; }
  /* #invAddCard is #inv's THIRD direct child and was missing from this list —
     the trap this block exists to get out of, sprung one card further down.
     MEASURED 2026-09-21 with a real 114-app household: `.screen` only keeps
     two tracks at >=1400px (the `(min-width: 900px) and (max-width:
     1399.98px)` block further down collapses it to one), and there the card
     rendered 561px wide in track one at 1440 — track two empty for its whole
     height, under a 1140px list. Below 1400px it happened to look right only
     because there was no second track to leave empty. */
  #inv > #invAddCard { grid-column: 1 / -1; }

  /* ---- WEBSITES. Same reversal, same reason: js/sites.js's "add a website"
     builder is the thing a parent came here to use, and #sitesCard below it is
     the list of what they have already decided. The mockup's Web pane has
     exactly this shape — a full-width add band, then the rules. */
  #sites > #sitesCard { grid-column: 1 / -1; order: 1; }
  #sites > #sitesMount { grid-column: 1 / -1; }

  /* ---- TIME and CATEGORIES. One card each; a half-width card floating in a
     1150px content column was the "narrow ribbon of text" the mockup is not. */
  #schedules > #schedulesCard { grid-column: 1 / -1; }
  #categories > #categoriesCard { grid-column: 1 / -1; }

  /* ---- CHILD SETTINGS is NOT here. #childMount's own grid rules already
     exist, ~250 lines up in the "PER-CHILD SCREENS — RESTYLE 2026-08-29"
     block, and they are the same three lines #overviewMount carries. A
     second copy in this block would be a rule that has to be changed twice
     and will be changed once. Verified by screenshot at 1440x950: the child
     settings screen already lays out across both tracks. */
}

/* A mount is not a card and gets no frame of its own; the card inside it has
   one. Without this the module's card keeps its own top margin and sits a
   grid gap plus a margin away from everything else. */
#appsMount > .card, #sitesMount > .card { margin-top: 0; margin-bottom: 0; }

/* BELOW 900px THERE IS NO GRID. Apps already has scope/delivery before its
   list in source order, so flex only gives the two full-width siblings a
   simple vertical flow. Websites retains its existing card inversion.
   `:not(.hide)` IS LOAD-BEARING AND NOT DECORATION. `.hide { display: none }`
   is a single class (0,1,0); `#inv` is an id (1,0,0) and would beat it, so a
   bare `#inv { display: flex }` here would make these two screens IMPOSSIBLE
   TO HIDE and every screen would render stacked on top of the others. The
   guard means the rule simply does not apply to a hidden section. */
@media (max-width: 899.98px) {
  #inv:not(.hide), #sites:not(.hide) { display: flex; flex-direction: column; }
  #sites:not(.hide) > #sitesCard { order: 1; }
}

/* ------------------------------------------------- Apps: the delivery card
   js/apps.js. One row per linked device, with the Applied / Pending pill
   that is this product's answer to "the dashboard accepted it and nothing
   enforced it". `.pill.on` (mint) and `.pill.wait` (amber) are this file's
   existing words for confirmed and not-yet; the module never reaches for a
   colour of its own. */
.appsdel .arow.appsdelrow { align-items: center; }
.appsdel .arow.appsdelrow .an { font-weight: 650; }
.appsdel .arow.appsdelrow .asub { margin-top: 3px; }

/* ------------------------------------------ Websites: allow / block columns
   app.js's renderRuleColumns(). Two boxes side by side, each headed with the
   direction and the count of the rules actually inside it. Below 780px they
   stack, because two 180px columns of hostnames is neither. */
.rulecols { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 780px) {
  .rulecols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.rulebox {
  border: 1px solid var(--line); border-radius: 16px; padding: 12px 12px 4px;
  background: var(--dusk2); min-width: 0;
}
/* A 3px edge in the direction's own colour, not a wash: mint is this page's
   "allowed / healthy" and coral its "this direction costs the child
   something" — the same pair `.pill.on` / `.pill.off` already put on every
   rule row inside these boxes, so the box and its rows never disagree. */
.rulebox.ruleallow { border-left: 3px solid var(--mint); }
.rulebox.ruleblock { border-left: 3px solid var(--coral); }
.rulehead {
  display: flex; align-items: baseline; gap: 8px; justify-content: space-between;
  margin-bottom: 10px; font-size: 13px;
}
.rulebox.ruleallow .rulehead b { color: var(--done-text); }
.rulebox.ruleblock .rulehead b { color: var(--coral); }
/* THE SAME LIGHT-THEME LITERALS THIS FILE ALREADY USES FOR THESE TWO MEANINGS,
   and for the reason it already records three times over (--haze,
   .ghost.danger, .pill.wait): the light palette's `--coral` is #D4483D, which
   measures about 4.0:1 on the card's white — short of AA for text this size,
   in exactly the 0.04-to-0.5 way those three comments describe. #0A6242 and
   #B23127 are NOT new colours: they are `.pill.on`'s and `.pill.off`'s own
   light-theme values, on the two pills that sit inside these very boxes, so
   the heading and the rows beneath it stay one colour rather than two.
   BOTH light paths are covered — the explicit toggle and the untoggled OS
   preference — which is the gap `.pill.wait`'s own comment notes the older two
   still have. */
:root[data-theme="light"] .rulebox.ruleallow .rulehead b { color: #0A6242; }
:root[data-theme="light"] .rulebox.ruleblock .rulehead b { color: #B23127; }
:root[data-theme="light"] .rulebox.ruleallow .rulehead b { color: #0A6242; }
:root[data-theme="light"] .rulebox.ruleblock .rulehead b { color: #B23127; }
.rulecount { font-size: 11px; font-weight: 700; color: var(--haze); white-space: nowrap; }
/* The rows inside a column are narrower than the full-width rows this class
   was written for, so the remove button must be allowed onto its own line
   rather than crushing the hostname to one letter per line — the exact
   collapse `#profList .arow`'s own measured comment describes. `align-items:
   flex-start` because these rows stack a host over two or three sentences, so
   the favicon belongs beside the NAME and not floating in the middle of the
   block; and `margin-left: auto` keeps Remove on the right of whichever line
   it lands on rather than orphaning it under the favicon. */
.rulebox .arow { flex-wrap: wrap; align-items: flex-start; }
.rulebox .arow .an { min-width: 45%; }
.rulebox .arow .iconbtn { margin-left: auto; }
.rulebox > .muted { padding: 4px 0 12px; }

/* ------------------------------------------------ Apps: one detail line, not five
   THE ROW'S FACTS ARE UNCHANGED AND SO IS ITS MARKUP. Every `.asub` the row
   has always carried is still a separate element with exactly the same text —
   test/inventory-ui.test.js and test/apps-picker-ui.test.js read them as an
   array and assert on their contents, including the per-platform block
   sentence, and none of that moves. What changes is that they FLOW as one
   wrapped line instead of stacking four deep, which is the difference between
   the mockup's compact app item and a card where ten apps are 1,100px tall.
   Scoped to `#invList .arow .an` so the picker, the site rows, the schedule
   router and every other `.asub` on the page are untouched. */
#invList .arow .an > .asub { display: inline; margin-top: 0; line-height: 1.55; }
/* A HARD BREAK AFTER THE NAME, and it has to be forced. `.an`'s own name is a
   bare TEXT NODE (there is no element around it, and there must not be — six
   tests read `.an`'s firstChild for the app's name), so the only thing that can
   push the first fact onto its own line is the first `.asub` itself. Without
   this, 390x844 rendered "Minecrafton Leo’s tablet" — measured, not guessed. */
#invList .arow .an > .asub:first-of-type::before { content: "\A"; white-space: pre; }
/* The separator inherits `.asub`'s own --haze rather than being painted
   --line: at --line it was measured invisible against the row, which turned
   four facts into one run-on sentence instead of a list. */
#invList .arow .an > .asub + .asub::before { content: " · "; }
/* The chip strip and the New-row action buttons are blocks and must stay
   blocks — they are controls, not prose, and inlining them would put a button
   in the middle of a sentence. */
#invList .arow .an > .invchips { display: flex; }

/* --------------------------------------------- Time: the per-child router
   js/schedule.js's `.schedRouter`. The rows now carry a pill as well as a
   name, a snapshot line and a button, which is one item more than the row was
   laid out for at a phone width. */
.schedRouter .arow { flex-wrap: wrap; }
.schedRouter .arow .an { min-width: 55%; }

/* ==========================================================================
   ZIP PARITY 2026-08-30 — PROCESSES and ACCOUNT & SETTINGS
   (js/processes.js, js/account.js)

   APPENDED, NEVER INTERLEAVED, and every selector below is either a new class
   or an EXISTING one re-stated at the end of the file so a later declaration
   wins. Two blocks above already own these screens (`PROCESSES — the
   task-manager column layout` and `ACCOUNT & SETTINGS — RESTYLE 2026-08-29`);
   neither is edited, so a concurrent pass on either is a clean append on both
   sides. Where this block deliberately OVERRIDES one of them, it says so and
   says why.

   No new colour is invented. Everything below paints out of the existing
   tokens (--haze, --line, --dusk, --dusk2, --glow, --sky) and the existing
   shapes (.ovtile, .avatar, .asub).
   ========================================================================== */

/* ---- 1. PROCESSES: §9's executable line ----------------------------------
   PARITY-DECISIONS-2026-08-29 §9's "muted secondary detail beneath the
   friendly name", on the Processes screen ONLY. `.asub` already carries the
   whole treatment that matters — 11px, --haze, and the font-weight: 400
   override that keeps it out of `.an`'s 600 — so `.procid` adds exactly two
   things and nothing else:

     - a monospace face, because this string is a FILENAME and the point of
       showing it is that a parent can match it character for character
       against what they see in Windows itself. It is the same stack this file
       already uses at line ~563 rather than a second one;
     - `break-all`, because an executable name has no spaces to break at, and
       an unbreakable 30-character token in a 1.1fr grid track pushes the
       whole table wide (the horizontal-scroll failure the screenshot pass
       asserts against).

   NOT a new colour, NOT smaller than `.asub`: it must read as an
   identification aid, not as fine print a parent is expected to squint past.
   `.procid` is used only inside js/processes.js, which style-vocabulary
   .test.js does not scan — that is precisely why it is declared here in
   app.css with the rest of the vocabulary, per the `.cardhead` note above. */
.arow.procrow .an .asub.procid {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  word-break: break-all;
  margin-top: 3px;
}

/* ---- 2. ACCOUNT: the summary row's identity cell -------------------------
   The reference Account screen's `.accountsummary` is a WIDE identity cell
   followed by three narrow counts —
   `minmax(260px,1.55fr) repeat(3, minmax(120px,.55fr))` in the mockup's own
   stylesheet. `.ovtiles` (Home's tile row, reused here) is
   `repeat(auto-fit, minmax(180px, 1fr))`, which gives an email address the
   same width as a one-digit number.

   `.acctsum` is therefore added BY js/account.js only when the identity tile
   actually rendered, so a screen that could not draw it (no session address,
   or the household read failed) keeps the equal-track grid that suits three
   counts on their own rather than leaving a 1.55fr hole where a person should
   be. */
.ovtiles.acctsum {
  grid-template-columns: minmax(240px, 1.55fr) repeat(3, minmax(110px, .55fr));
}

/* The identity tile itself. `.ovtile` still paints the card; this only turns
   it into an avatar-beside-text row and gives it the reference's own tinted
   fill, drawn from --dusk2 and the trusted/primary --glow at low alpha rather
   than from a new literal. */
.ovtile.acctowner {
  display: flex; align-items: center; gap: 13px; min-width: 0;
  background: linear-gradient(135deg, color-mix(in srgb, var(--glow) 12%, var(--dusk)), var(--dusk));
  border-color: color-mix(in srgb, var(--glow) 28%, var(--line));
}
.ovtile.acctowner .avatar { flex: 0 0 auto; }
.acct-who { min-width: 0; }
/* The address, sized between `.ovt-k`'s label and `.ovt-v`'s 20px numeral: an
   email is identity, not a statistic, and at 20px a real address wraps to
   three lines inside a 240px cell. `anywhere` because an address has no space
   to break at either. */
.acct-mail {
  font-size: 14.5px; font-weight: 700; color: var(--cloud);
  margin-top: 5px; overflow-wrap: anywhere;
}
.acct-name {
  margin-top: 5px;
  color: var(--cloud);
  font-size: 16px;
  font-weight: 800;
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.acct-name + .acct-mail {
  margin-top: 2px;
  color: var(--haze);
  font-size: 12px;
  font-weight: 600;
}
.ovtile.acctowner .ovt-s { margin-top: 3px; }

/* ---- 3. ACCOUNT ON A PHONE — the summary row comes back ------------------
   OVERRIDES `#settingsMount > .ovtiles { display: none }` in the
   `ACCOUNT & SETTINGS — RESTYLE 2026-08-29` block above. That rule is left
   exactly as it is and beaten by source order at equal specificity, which is
   the append convention this file is built on.

   WHY IT IS REVERSED. Its own comment gives the reason it existed: `.ovtiles`
   is auto-fit/minmax(180px), so on a 390px screen three count tiles stack
   into ~200px of chrome above the group rail. That is a fact about the GRID,
   not about the content — and the reference design answers it directly, with
   a 2-column restack at 760px (`.accountsummary{grid-template-columns:
   repeat(2,1fr)}` and `.accountowner{grid-column:1/-1}`). Two rows of paired
   tiles is roughly half the height that made hiding them look reasonable, and
   it is what the mockup a parent was shown actually does.

   e2e/mobile.spec.cjs (the real 390x844 measurement this file's mobile
   comments defer to) makes no assertion about this screen; the 390x844
   photographs in verification/zip-parity/account/ are the evidence for this
   one. */
@media (max-width: 899px) {
  #settingsMount > .ovtiles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  /* The identity cell spans both tracks — the reference's own restack, and the
     only shape in which an email address is readable at this width. */
  #settingsMount > .ovtiles > .ovtile.acctowner { grid-column: 1 / -1; }
  /* Two-up counts are a summary, not a headline: the 20px numeral face and
     the 15px card padding are both sized for a 1440 screen. */
  #settingsMount > .ovtiles .ovtile { padding: 12px 13px; }
  #settingsMount > .ovtiles .ovt-v { font-size: 18px; margin-top: 4px; }
}

/* ==========================================================================
   CHECK-INS & QUIZZES — ZIP PARITY, 2026-08-30 (js/engage.js)
   ==========================================================================
   Appended, not merged into the `#engageMount` block a few hundred lines up,
   because that block is another pass's and this file's convention is that a
   screen's later work lands at the end under its own header. Where the two
   disagree the rules below win by source order at equal specificity, which is
   deliberate and is noted at each override.

   WHAT CHANGED, AND WHY. The reference build
   (mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   parentproof-complete-clickable-demo.html, `#engage`) opens this screen with
   TWO TOOL CARDS — icon tile, what it does, one primary button — above the two
   history lists, and puts each short-lived authoring task in a modal. The
   resting screen keeps that shape; the accessible modal implementation is in
   the later CONTAINED AUTHORING block.

   Every class here is used ONLY from js/engage.js, which style-vocabulary
   .test.js does not scan — see the `.cardhead` block's own note. Each was
   grepped against this file before use. */

/* The named-area layout above names the old inline composer cards. The four
   resting cards now auto-place as tools over histories, while the modal is
   fixed outside the grid flow. Same media query and equal specificity: the
   stale named areas are neutralised without deleting the earlier pass. */
@media (min-width: 900px) {
  #engageMount { grid-template-areas: none; }
  #engageMount .engage-compose,
  #engageMount .engage-build { grid-area: auto; grid-column: 1 / -1; }
  #engageMount .engage-cihist,
  #engageMount .engage-qhist { grid-area: auto; }
}

/* A TOOL CARD. The button sits at the bottom on both cards even when one
   description wraps to a second line and the other does not — a pair of
   primary buttons at different heights is the first thing that reads as
   unfinished at 1440px. */
.engage-tool { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.engage-tool .help { flex: 1 1 auto; margin-bottom: 4px; }
/* `.primary` is `width: 100%` by default — right for a form's one submit,
   wrong for a card whose button is an invitation rather than the end of a
   task. The reference build's two tool buttons are compact and left-aligned
   under the description, which is also what stops two side-by-side cards
   reading as two competing full-width submits. */
.engage-tool .primary { flex: none; width: auto; padding: 13px 22px; }
/* The round icon tile. Neutral by default for the same reason `.acticon` is:
   colour on this page is a health claim. The quiz card's tint is the one
   exception the reference build makes — it is an IDENTITY, telling two tools
   apart at a glance, and it is --glow's own trusted/primary hue rather than
   mint (healthy), amber (attention) or coral (destructive), none of which
   this card is claiming. */
.engage-ico {
  width: 38px; height: 38px; border-radius: 13px; background: var(--dusk2);
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; line-height: 1; color: var(--haze); margin-bottom: 6px;
}
.engage-tool-quiz .engage-ico { background: rgba(74,163,255,.14); color: var(--glow); }

/* The two history rows get a leading tile like the Activity list's. `.acticon`
   is `flex: none` already, so the sentence column keeps `flex: 1 1 200px` and
   the pill keeps its place at the end of the row. */
.arow.engage-row .acticon { margin-top: 1px; }

/* The composer's two selects sit next to each other where there is room and
   stack where there is not — the reference build's own paired `.grid.equal`.
   The message is the sentence a parent is writing, so it never shares a line;
   `.engage-field:first-of-type` is that textarea's wrapper. */
.engage-compose .engage-field .inp { width: 100%; }
@media (min-width: 720px) {
  .engage-compose { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 14px; }
  .engage-compose .sect,
  .engage-compose .help,
  .engage-compose .muted,
  .engage-compose .primary,
  .engage-compose .fineprint,
  .engage-compose .rowline { flex: 1 1 100%; }
  .engage-compose .engage-field { flex: 1 1 220px; min-width: 0; }
  .engage-compose .engage-field:first-of-type { flex: 1 1 100%; }
}

/* ------------------------------------------------ ASSISTANT (the §65 screen) */
/* THE QUARTER-WIDTH MOUNT BUG, ONCE MORE — and it is the fourth time, so the
   rule is stated rather than re-derived. `#overviewMount`, `#settingsMount` and
   `#requestsMount` above each needed this line for the same reason:
   `#assistantMount` is a DIRECT CHILD of `.screen`, `.screen` is a two-track
   grid at this width, and a mount div is not a `.card` — so without this it
   silently occupies ONE track and everything the module draws comes out at half
   width, with a wall of empty page beside it. (Anything inside it that is itself
   a grid would then be split again, to a quarter.)

   ONE COLUMN INSIDE IT, DELIBERATELY, unlike #overviewMount and #engageMount.
   Those hold independent cards that read fine side by side. This screen holds a
   composer and the answer to what was typed into it: they are one continuous
   act, in that order, and putting the answer in a second column beside the box
   would mean the empty right-hand track IS the resting state of the screen —
   the exact "half the width holding nothing at all" complaint the Activity rule
   further up was written to fix. */
@media (min-width: 900px) {
  #assistantMount { grid-column: 1 / -1; }
}
#assistantAskBox { display: flex; flex-direction: column; gap: 18px; }
/* Grid/flex `gap` owns the spacing now, the same swap `.screen > .card` makes. */
#assistantAskBox > .card { margin-top: 0; margin-bottom: 0; }

/* A READABLE MEASURE FOR PROSE. This is the one screen in the dashboard whose
   content is paragraphs rather than rows, and a full-width card at 1440 is ~1100
   px of running text — roughly 160 characters a line, about twice the measure
   text is comfortably read at. Capped on the paragraphs only: the composer's
   textarea, the starter chips and the proof strip all want the whole card. */
#assistantAskBox .card > p { max-width: 68ch; }

/* AND A WRAP OPPORTUNITY IN EVERY WORD, BECAUSE ONE OF THESE PARAGRAPHS WAS
   WRITTEN SOMEWHERE ELSE.

   `max-width: 68ch` above decides where the breaks a browser IS ALLOWED to make
   should fall. It does not create one. Every other paragraph on this screen is
   our own copy and we can count its longest word; the ANSWER is prose a language
   model wrote, rendered by assistant.js's answerCard(), and the questions a
   parent most obviously types here — "which websites did I block?", "what is
   this app?" — are exactly the ones whose answer repeats a URL back at them.

   MEASURED IN CHROMIUM AT 375x667, in a 343px column, `overflow-wrap: normal`:
     media-cdn-eu-west-1.streaming-…-network.com  (76 chars, hyphenated)  fits
     mediacdnstreaming.videoforchildren….com      (74 chars, dots only)   +144px
     https://www.example….com/watch/verylongid    (72 chars, a URL)       +124px
   Hyphens are break opportunities and dots and slashes are not, so the case that
   breaks the page is the ordinary one. 124px of horizontal scroll on the
   documentElement is not a wide card — it drags the nav, the header and every
   other screen sideways with it, on the one state this feature exists to reach.

   `anywhere` rather than `break-word` to match `.ask-setting` and the twenty-odd
   other places in this file where remote or parent-supplied text lands
   (`.arow .an`, `.procwhat`, `.sites-preview p`, `.devprov-value`): `anywhere`
   also shrinks the paragraph's min-content width, which is what stops the flex
   column above from being sized by the longest word in the model's reply.

   Proven by platform/test/assistant-se-layout.test.js — including a CONTROL that
   switches this rule off in the live page and asserts the overflow comes back,
   so the test cannot pass for some other reason. */
#assistantAskBox .card > p { overflow-wrap: anywhere; }

/* The question box. `.inp` already carries the field's border, radius, colours
   and focus ring — this only gives the textarea a sensible height and lets a
   parent drag it taller, neither of which `.inp` has an opinion about because
   every other field wearing it is a one-line <input>. */
#assistantAskInput { min-height: 84px; resize: vertical; }

/* ==========================================================================
   ZIP PARITY — THE TIME SCREEN'S THREE MISSING CONTROLS
   The per-child Time screen (#schedules, js/schedule.js's router): "time left
   today", per-device Pause, and the daily-limit chips on the child settings
   screen. The Applied ✓ / Pending… pill and the layout this screen needed to
   carry it are the per-child cluster block above — this block adds only what
   that one did not: the time figure.

   Appended after it, at the very end, so it cannot collide with another
   track's edits. Everything here is layout: NO new colour and NO new token.
   Every class named on the new nodes is one this stylesheet already owns —
   .pill.on / .pill.wait / .pill.off, .ghost.danger, .arow / .an / .asub, and
   .chips / .chip[aria-pressed] for the daily-limit strip, which is why that
   strip needs no rule here at all.

   NOTHING FROM THE CLUSTER BLOCK IS REPEATED. `#schedules > #schedulesCard {
   grid-column: 1 / -1 }` and `.schedRouter .arow { flex-wrap: wrap }` are both
   already up there, put there for the same reason this pass would have needed
   them (AUDIT D4, and a row that now carries one item more than it was laid
   out for). A second copy is a rule that has to be changed twice and will be
   changed once.

   HAND-CHECKED AGAINST THIS FILE, ONE CLASS AT A TIME, and that is not
   ceremony: test/style-vocabulary.test.js reads class="…", classList and
   className, and these modules build every node with `elem(tag, cls)` — which
   that scanner cannot see. A class invented here fails silently and forever,
   which is exactly how `rulebox allow` shipped against a stylesheet that only
   defines `.rulebox.ruleallow`. test/zip-parity-time-gaps.test.js carries the
   scanner that scanner cannot be.
   ========================================================================== */

/* "TIME LEFT TODAY" — the mockup's `2h 30m left`, at the size a number that is
   the answer to the question deserves. Deliberately NOT `.ovc-time`: that class
   is scoped to `.ovcard` on Home and is 30px, which on a list row would be
   taller than the row. Same tokens as it, though — --cloud for the figure and
   --haze for its label, tabular figures so three tablets' numbers line up down
   the list rather than jittering.

   THE LABEL AND THE NUMBER ARE ONE UNIT and js/schedule.js appends neither
   without the other: a label over a blank is an invitation to read the answer
   as zero, which is the opposite of what the omission means. */
.schedRouter .arow .an > .schedleftlab {
  display: block; margin-top: 6px; font-size: 11px; font-weight: 600; color: var(--haze);
}
.schedRouter .arow .an > .schedleft {
  display: block; font-size: 20px; font-weight: 800; letter-spacing: -.01em;
  color: var(--cloud); font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   KID DESKTOP PREVIEW — the child's locked-down computer, drawn for a parent
   (mockups/ParentProof-Hands-On-Ordered-Demo-2026-08-24.zip,
   parentproof-kid-desktop-lockdown.html: "Preview Leo's real desktop
   experience"). Rendered only by platform/public/js/desktop-preview.js.

   EVERY CLASS THAT MODULE EMITS IS DEFINED BELOW, and the pairing is stated
   here by hand for the reason the `.reqpolicy-*` block a few hundred lines up
   states it: test/style-vocabulary.test.js reads app.js and index.html only,
   so a class invented in a js/ module fails silently and forever. The complete
   set is `kd-eyebrow`, `kd-title`, `kd-notlive`, `kd-signin`, `kd-modes`,
   `kd-modecard`, `kd-modehead`, `kd-modebody`, `kd-macnote`,
   `kd-stage`, `kd-stagewall`, `kd-shield`, `kd-shell`, `kd-shelltitle`,
   `kd-shellbody`, `kd-stagenote`, `kd-stateunknown`, `kd-indicator`,
   `kd-indicatorline`, `kd-quote`, `kd-quotehead`, `kd-quotebody`,
   `kd-quotebtns`, `kd-quotebtn`, `kd-states`, `kd-stateslabel`,
   `kd-statechip`, `kd-quotednote`, `kd-appsnote`, `kd-themenote`,
   `kd-truthgrid`, `kd-truthcol`, `kd-truthhead`, `kd-truth`, `kd-jump` and
   `is-solo`. Everything
   else it uses (`.card`, `.sect`, `.help`, `.fineprint`, `.tiny`, `.chip`,
   `.pill.trusted`, `.pill.wait`, `.is-hidden`) already existed.

   THE ONE RULE IN THIS BLOCK THAT IS NOT DECORATION. `.kd-stagewall` and
   `.kd-shield` carry the CHILD'S colours, not this page's: the captive
   background is a real Win32 RGB triple the PC is painted with
   (agent/windows/PpCaptiveMode.psm1) and the pause/block screens are the
   agents' own warm off-white ground with their own ink
   (ppsecure-pause.cs `Color.FromArgb(247,245,239)` / `(33,42,74)` /
   `(72,82,110)`, PpSecureShield.swift's identical calibrated triple). They are
   therefore LITERALS and are NOT re-themed for light mode — a quotation of a
   child's screen that changes colour with the parent's theme is a picture of a
   screen that does not exist. Everything around the frame uses this file's own
   tokens and follows the theme normally.
   ========================================================================== */

@media (min-width: 900px) { #kidDesktopPreview { grid-column: 1 / -1; } }

.kd-eyebrow {
  font-size: 10px; font-weight: 800; letter-spacing: 1.2px; color: var(--glow);
}
/* `.sect` is this file's 12px muted section label. This one is a CARD TITLE and
   the eyebrow above it needs room, so it takes the primary ink and a step up in
   size while keeping `.sect`'s own hairline and layout. */
.kd-title { font-size: 15px; color: var(--cloud); margin: 7px 2px 12px; }
.kd-notlive { margin: 6px 0 0; }
.kd-signin { margin: 8px 0 0; }

/* The mockup's `.modegrid`. `minmax(0,1fr)` for the reason every grid in this
   file uses it: a long mode name must shrink rather than force a scrollbar. */
.kd-modes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px; margin-top: 14px;
}
@media (max-width: 820px) { .kd-modes { grid-template-columns: 1fr; } }
/* macOS gets one mode, not two — see the module's own note. */
.kd-modes.is-solo { grid-template-columns: 1fr; }

.kd-modecard {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--dusk2); padding: 12px; min-width: 0;
}
/* The one that is actually set, marked with the trusted hue rather than a
   heavier weight — a bolder border reads as "selectable", and neither of these
   is a control. */
.kd-modecard.is-current { border-color: var(--glow); }
.kd-modehead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.kd-modehead > b { min-width: 0; }
.kd-modebody { margin: 6px 0 0; font-size: 12px; color: var(--haze); line-height: 1.45; }
.kd-macnote { margin: 8px 0 0; }

/* THE FRAME. `overflow: hidden` so the wall's own colour cannot bleed past the
   rounded corner, and a fixed aspect so the drawing keeps a screen's shape at
   every width instead of collapsing to a strip on a phone. */
.kd-stage {
  position: relative; margin-top: 14px;
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.kd-stagewall {
  position: relative; min-height: 260px; padding: 16px;
  display: flex; flex-direction: column; gap: 12px;
  /* The default when no captive colour applies: a plain plate that is
     visibly NOT a wallpaper, so nothing here can be mistaken for the
     computer's own picture. */
  background: var(--ink);
}
@media (max-width: 560px) { .kd-stagewall { min-height: 220px; padding: 12px; } }

/* The agents' shield ground, quoted. See the block header. */
.kd-shield { background: #F7F5EF; color: #212A4A; }

.kd-shell {
  align-self: flex-start; max-width: 420px;
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 14px;
  background: rgba(9, 16, 29, .55); padding: 12px 14px; color: #EDF5FF;
}
.kd-shelltitle { font-weight: 800; font-size: 14px; }
.kd-shellbody { margin: 5px 0 0; font-size: 12px; line-height: 1.45; color: #C6D5E8; }
/* PP-19 — the computer's own reported simplified-desktop app list. */
.kd-shellapps { margin: 8px 0 0; padding: 0 0 0 18px; font-size: 12px; line-height: 1.5; color: #EDF5FF; }
.kd-shellapps li { margin: 2px 0; }
.kd-stagenote { margin: 0; font-size: 11px; line-height: 1.45; color: #48526E; }

/* The tray indicator, bottom-right on both platforms. */
.kd-indicator {
  margin-top: auto; align-self: flex-end; max-width: 320px; text-align: right;
  border-radius: 12px; padding: 8px 10px;
  background: rgba(9, 16, 29, .72); border: 1px solid rgba(143, 211, 255, .35);
  color: #8FD3FF; font-size: 11px;
}
.kd-indicator > b { display: block; font-size: 12px; }
.kd-indicatorline { display: block; margin-top: 3px; color: #C6D5E8; line-height: 1.4; }
/* On a quoted shield the indicator sits on a light ground, so it takes the
   shield's own ink rather than staying a dark chip nobody could read. */
.kd-shield > .kd-indicator {
  background: rgba(33, 42, 74, .06); border-color: rgba(33, 42, 74, .18); color: #48526E;
}
.kd-shield > .kd-indicator > b { color: #212A4A; }
.kd-shield > .kd-indicator > .kd-indicatorline { color: #48526E; }

/* A quoted child screen. Centred, because these three really are centred
   dialogs and a left-aligned quotation misrepresents the screen's shape. */
.kd-quote { text-align: center; padding: 18px 14px; border-radius: 14px; }
.kd-quotehead { font-size: 20px; font-weight: 800; line-height: 1.25; }
.kd-quotebody { margin: 8px auto 0; max-width: 460px; font-size: 13px; line-height: 1.5; color: #48526E; }
.kd-quotebtns { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 14px; }
/* NOT `<button>`, and the module builds them as spans on purpose: these are
   pictures of the child's buttons. Anything clickable here would be a parent
   pressing a control that belongs to another computer. */
.kd-quotebtn {
  border: 1px solid rgba(33, 42, 74, .25); border-radius: 10px;
  background: rgba(33, 42, 74, .05); color: #212A4A;
  padding: 7px 11px; font-size: 12px; font-weight: 700;
}

/* THE HONESTY OVERLAY — a strip under the frame, not a badge on top of it: a
   parent has to read it to reach the rest of the card. */
.kd-stateunknown {
  display: flex; flex-direction: column; gap: 3px;
  padding: 10px 12px; border-top: 1px solid var(--line);
  background: rgba(255, 194, 75, .10); color: var(--waiting-text); font-size: 12px; line-height: 1.45;
}
.kd-stateunknown > b { font-size: 12px; }
.kd-stateunknown > span { color: var(--haze); }

/* THE ROUTE TO THE CARD, under the Desktop-mode control it previews — see the
   module's own jumpLink note. `.ghost.wide` already carries the shape; this
   only stops it colliding with the status line above it. */
.kd-jump { margin-top: 8px; }

.kd-states { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 12px; }
.kd-stateslabel { margin-right: 2px; color: var(--haze); }
.kd-statechip { flex: none; }
.kd-quotednote { margin: 8px 0 0; }
.kd-appsnote, .kd-themenote { margin: 8px 0 0; }

.kd-truthgrid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px; margin-top: 16px;
}
@media (max-width: 820px) { .kd-truthgrid { grid-template-columns: 1fr; } }
.kd-truthcol { min-width: 0; }
.kd-truthhead {
  font-size: 10px; font-weight: 800; letter-spacing: 1px; color: var(--haze);
  text-transform: uppercase; margin-bottom: 6px;
}
.kd-truth { display: flex; gap: 8px; align-items: flex-start; margin-top: 6px; font-size: 12px; line-height: 1.45; }
.kd-truth > b { flex: none; color: var(--done-text); }
.kd-truth > span { color: var(--haze); }
/* Coral is this product's destructive/withheld hue and these are the things
   the product does NOT do — the same colour vocabulary UI-DIRECTION §4 sets. */
.kd-truth.no > b { color: var(--coral); }

/* ===================================================================   PER-CHILD "FAMILY CONTROLS" — PARITY-DECISIONS-2026-08-29 §69
   js/family-controls.js. APPENDED AS ONE BLOCK, unscoped, for the reason the
   "PER-CHILD SCREENS — RESTYLE" block above already records:
   style-vocabulary.test.js scans index.html and app.js only, so a class used
   solely inside a js/*.js module is invisible to it. Every class this module
   names is therefore here, and every one was grepped against this file by hand
   before it was used.

   NO NEW COLOURS AND NO NEW SHAPES. `.fc-head` wears the `.ovhead`/`.child-head`
   the child screen already had (its phone rules are MEASURED — see that block),
   `.fc-chips` wears `.chips`/`.chip`, the hero wears `.ovcard`/`.ovc-*` and the
   policy tiles wear `.ovtiles`/`.ovtile`. What is below is placement.
   ========================================================================== */

/* THE .screen GRID TRAP, and it is the reason this rule exists at all. At
   >=900px `.screen` is a two-track grid (see its own rule ~line 209). A direct
   child of it that is ITSELF a grid container takes ONE track and then splits
   that half again — quarter-width cards, nothing thrown, discovered twice
   before by screenshot (#overviewMount's own comment records both). These
   nodes are all direct children of `.screen`, inserted by js/family-controls.js
   around the tab strip, so all of them span. */
@media (min-width: 900px) {
  .screen > .fc-head, .screen > .fc-chips { grid-column: 1 / -1; }
  .screen > .fc-ov {
    grid-column: 1 / -1; display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px; align-content: start;
  }
  .screen > .fc-ov > .card { margin-top: 0; margin-bottom: 0; }
  /* The five screens that are not Overview keep their own `.toprow` title
     ("Apps / Leo’s list" and its All-tablets button) — it names the TAB, and
     it sits under the tabs where the reference build puts a pane heading. Only
     #child's is hidden, and that rule is already ~200 lines up in this file
     with its own reasoning: this header states the screen AND names the child,
     so the generic word above it would be the same idea twice. */
}
/* Below the rail breakpoint `.screen` is a plain block, so nothing to span. */
.fc-head { margin-bottom: 10px; }
/* THE EYEBROW IS THE CHILD'S NAME NOW, AND ON A PHONE IT IS THE ONLY PLACE
   ABOVE THE FOLD THAT NAMES THEM. `.child-head .ov-eyebrow { display: none }`
   in the block above is correct for the header it was written for — there the
   eyebrow read "Per-child settings" over a title that already said "Leo’s
   settings", a third copy of the same idea. This header's title is "Family
   controls" and says no name at all, so hiding the eyebrow leaves a parent on a
   390px screen looking at controls with no statement of whose they are.
   MEASURED: the first phone capture of this screen showed the badge, the title
   and the tabs with the word "Leo" nowhere above the scope line. Same
   specificity as the rule it overrides, and it wins on source order — this
   block is appended after it. */
@media (max-width: 899px) { .fc-head .ov-eyebrow { display: block; } }
.fc-chips { margin: 0 0 12px; }

/* ONE CHIP PER DEVICE. `.chip` is the existing filter/tab chip and has no
   layout of its own; a dot plus a name needs one. `aria-pressed` styling comes
   free from `.chip[aria-pressed="true"]` and is honest here — this really is a
   one-of-N selection, and it drives the hero below it. */
.fc-chips > .chip { display: inline-flex; align-items: center; gap: 8px; }
.fc-chipname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 22ch; }
/* A POSITIVE CLAIM, DRAWN ONLY FOR ONE. js/family-controls.js renders this
   node only for a device really heard from inside HEARD_FROM_RECENTLY_MS, so
   mint is earned; no dot is the honest absence. A hollow/grey variant is
   deliberately NOT defined, because it would assert "offline", which nothing
   has been told. */
.fc-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--mint); flex: none; }
/* The add control is a way OUT of this row rather than one of its members, so
   it is drawn as an outline rather than a filled chip. */
.fc-add { border-style: dashed; }

/* Redesign Ledger dec-multidevice-pill — "merged pill + one-tap breakdown"
   (js/family-controls.js onlinePill()). A <button> wearing the same `.pill`
   state classes as the plain read-only pill, so 2+ devices looks identical to
   1 device except for the caret and the tap target; only its background comes
   from `.pill.on/.wait/.neutral` already defined above. */
.fc-pillbtn { display: inline-flex; align-items: center; gap: 4px; background-color: inherit; font: inherit; letter-spacing: inherit; }
.fc-pillcaret { font-size: 9px; line-height: 1; }
/* The breakdown itself: one row per device, read-only (no `.chip`, no
   selection state — that stays paintChips's alone on the Overview tab). */
.fc-pillbreak { margin: 8px 0 0; padding: 8px 10px; border-radius: var(--radius); background: var(--dusk2); display: flex; flex-direction: column; gap: 6px; }
.fc-pillrow { display: flex; align-items: center; gap: 8px; font-size: 12.5px; flex-wrap: wrap; }
.fc-pillrowname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 650; }
.fc-pillrowstatus { flex: none; color: var(--haze); }
/* dec-device-config-home's second door to the real per-device settings
   screen (js/family-controls.js onlinePill()'s breakdown row). */
.fc-pillrowaction { flex: none; margin-left: auto; }

.fc-ov { display: block; }
/* Each card is its own height. Without this the grid stretches both to the
   taller one and the shorter card gets a long empty tail — measured at 1440,
   where the policy card ran ~150px past its last row. */
@media (min-width: 900px) { .screen > .fc-ov { align-items: start; } }
.fc-ov > .card + .card { margin-top: 14px; }
@media (min-width: 900px) { .fc-ov > .card + .card { margin-top: 0; } }
.fc-hero { min-width: 0; }
.fc-policy { min-width: 0; }
/* The reference build's Profile policy table: label left, figure right, a
   hairline between. --line is the same hairline `.tsec` and the card borders
   already draw, so this adds no colour. */
.fc-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  border-top: 1px solid var(--line); margin-top: 14px; padding-top: 14px;
}
.fc-rowk { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.fc-rowsub { font-size: 11.5px; color: var(--haze); }
.fc-rowv { flex: none; font-size: 17px; font-weight: 750; text-align: right; }

/* pc-mac-architecture idx0 (GoodQA 46e88855) — the merged protection pill,
   shown only for a child with both a tablet and a PC. Same `.card`/`.fc-row`
   vocabulary as the policy card above; `<details>`/`<summary>` for the
   one-tap breakdown, the same native-disclosure pattern js/processes.js's
   system-noise fold already uses. */
.fc-protection { min-width: 0; }
.fc-protection-fold { min-width: 0; }
.fc-protection-summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer; list-style: none;
}
.fc-protection-summary::-webkit-details-marker { display: none; }
.fc-protection-toggle { font-size: 12.5px; color: var(--haze); text-decoration: underline; }
.fc-protection-list { margin-top: 4px; }

/* The hero's action row — the reference build's four side-by-side controls.
   `.primary` is `width: 100%` on this page (it is the page's form-submit
   shape), so a primary button dropped into a flex row takes the whole row and
   pushes its two neighbours onto lines of their own. MEASURED: the first
   screenshot of this card had Pause, +15 min and Child home & themes on three
   separate rows. Neither the colour nor the padding is touched — only the
   width claim, and only inside this row. */
.fc-actions { display: flex; flex-wrap: wrap; gap: 9px; align-items: stretch; }
.fc-actions > button { width: auto; flex: 0 1 auto; margin-top: 0; }
.fc-actions > .primary { font-size: 14px; padding: 12px 16px; min-height: 44px; }

/* "Not reported" SITS IN THE BIG NUMBER'S SLOT AND MUST NOT LOOK LIKE A
   NUMBER. `.ovc-time` is 30px/800 tabular figures; a sentence at that weight
   reads as a measurement. Same slot, muted, at text weight — the absence is
   stated, not shouted, and the help line under it says what would make a
   figure appear. */
/* SCOPED TO `.ovcard.hero` DELIBERATELY, not because it needs those ancestors
   but because it needs the WEIGHT. There are FOUR rules on this element's size
   in this file — `.ovcard .ovc-time` (0,2,0), its 899px override, and
   `.ovcard.hero .ovc-time` (0,3,0) plus that one's own override — and the hero
   card carries `.hero`, so anything below (0,3,0) loses. MEASURED TWICE: a bare
   `.fc-time-none` lost to the first, and `.ovcard .fc-time-none` then lost to
   the third, both times printing "Not reported" at the full 800-weight tabular
   figures of a real measurement, in the slot where a real measurement goes.
   Equal specificity to the rule it has to beat and later in the file, so it
   wins on source order, and the browser test behind it reads the COMPUTED size
   rather than trusting this comment. */
.ovcard.hero .fc-time-none { font-size: 17px; font-weight: 650; color: var(--haze); letter-spacing: 0; }

/* ==========================================================================
   THE NAVIGATION RAIL'S REFERENCE PARITY — PARITY-DECISIONS-2026-08-29 §75
   ("Match the reference's nav, and add anything it is missing")
   ==========================================================================

   Three things arrive in the rail with this block: an Add-device entry inside
   the entry list, and a footer that carries Appearance, Pause all devices, the
   signed-in parent and Sign out — the reference's own `.sidebottom`, in the
   reference's own order (mockups/…Hands-On…zip,
   parentproof-complete-clickable-demo.html, its `.side > .sidebottom`).

   APPENDED, NOT WOVEN IN. Several agents are editing this file at the same
   time; a block at the end under its own header merges cleanly and a reviewer
   can read the whole change in one place.

   RAIL-ONLY, LIKE THE CHROME IT EXTENDS. `.navfoot` is `display: none` below
   900px (see the @media block beside `.navbrand`), so everything scoped under
   it here is too. That is not an oversight and it is not a regression: the
   reference's own mobile drawer carries no pause control either, and Pause all
   devices keeps its full card on the Devices screen — #pauseAllBtn — which is
   how a phone reaches it. Sign out likewise keeps #signoutBtn on Devices. */

/* -------------------------------------- Set up a new device (an <a>, in the
   entry list, between Activity and Settings).

   `.chip` gives the shape and `.navbtn`'s rail rule gives the full-width row;
   an anchor inherits neither the button's centring nor its box, so these are
   the same three properties `.btnlink` exists to supply one screen over. Not
   `.btnlink` itself, because that one CENTRES its content and every other entry
   in this rail is left-aligned — a centred setup link in a column of
   left-aligned rows reads as a different kind of control. */
.navadd {
  display: flex; align-items: center; justify-content: flex-start;
  text-decoration: none; box-sizing: border-box;
}
/* Below the rail breakpoint the entries are wrapped pills again, and a pill
   whose content is flush-left looks broken beside its neighbours. */
@media (max-width: 899px) { .navadd { justify-content: center; } }

@media (min-width: 900px) {
  /* THE FOOTER'S OWN CONTROLS. They are NOT `.chip`s: a nav entry is a
     destination and these are actions, and the reference draws the same
     distinction (its rail entries are plain buttons; its `.sidebottom` holds
     `.btn ghost` / `.btn danger`). Shape is borrowed from `.ghost` — a bordered
     transparent row — rather than redefined, and stated here rather than by
     adding `.ghost` to the markup because `.ghost.wide` carries a top margin
     the footer's own `gap` already provides. */
  .navfootbtn {
    display: flex; align-items: center; gap: 8px;
    /* 44px for the same reason #navRow .navbtn is: Appearance, Pause all
       devices and Log out are the rail's three real actions and were 42px. */
    width: 100%; box-sizing: border-box; min-height: 44px;
    padding: 9px 12px; border-radius: 10px;
    border: 1px solid var(--line); background: transparent;
    color: var(--cloud); font-family: var(--ui); font-size: 13px; font-weight: 600;
    text-align: left; cursor: pointer;
  }
  .navfootbtn:hover { background: var(--dusk2); }
  .navfootbtn:disabled { opacity: .5; cursor: default; }
  /* Appearance: an icon, the word, and the theme that is actually on. The value
     is painted by paintThemeControls() off the live `data-theme` attribute, so
     `.nf-val` can never name a theme the page is not wearing. */
  .navtheme .nf-ic { flex: none; color: var(--haze); }
  .navtheme .nf-label { flex: 1 1 auto; min-width: 0; }
  .navtheme .nf-val { flex: none; color: var(--haze); font-weight: 700; }

  /* PAUSE ALL DEVICES — coral, because UI-DIRECTION §2 rule 4 gives coral to
     destructive/paused and this is the one control in the rail that takes
     protection away from every device at once. The same colour treatment
     `.ghost.danger` already uses, including its light-theme substitute: bare
     --coral as text measures 4.38:1 on white, which is why that rule exists and
     why this one reuses its exact #B23127 rather than inventing a second value.

     IT IS A REQUEST, NOT A STATE. Nothing here paints a "paused" condition and
     there is deliberately no success colour for it to move to: the honest
     per-device answer is rendered by renderPauseAllResult() on the Devices
     screen, one amber `.pill.wait` per device, and a green fill in the rail
     would be exactly the blanket "Paused ✓" §75 forbids. */
  .navpause { color: var(--coral); border-color: rgba(255,107,94,.45); }
  .navpause:hover { background: rgba(255,107,94,.10); }
  :root[data-theme="light"] .navpause { color: #FFB4AC; border-color: rgba(255,180,172,.55); }

  /* Sign out sits below the identity row it belongs to and is the quietest
     control in the footer — it is not an emergency and must not compete with
     the one above it that is. */
  .navsignout { justify-content: center; color: var(--haze); font-weight: 600; }
  .navsignout:hover { color: var(--cloud); }
  :root[data-theme="light"] .navsignout,
  :root[data-theme="light"] .navsignout:hover { color: var(--railtext); }

  /* The identity row keeps its existing shape (see `.navfoot .navuser` in the
     rail block above); this only gives it breathing room now that there are
     controls on both sides of it. Trimmed 4px -> 2px alongside the rest of
     this block's "checkins" rail-room pass. */
  .navfoot .navuser { margin-top: 2px; }
}

/* ---------------------------------------------------- THE RAIL'S REAL HEIGHT
   (same §75 block; separated because it FIXES something rather than adding it)

   `.nav` above is `position: sticky; top: 0; height: 100vh`, and it is a grid
   item in the SECOND row of `.wrap` — the page header holds the first. So the
   rail's box starts partway down the page, and 100vh of height hangs its last
   header-height pixels off the bottom of the screen. MEASURED at 1440x950 on a
   seeded deployment, before this rule: the rail ran 106 -> 1056 against a 950px
   viewport, `scrollHeight === clientHeight` (so it could not scroll to the
   missing part either), and #navSignoutBtn sat at 996 — off screen, unreachable
   until the parent scrolled the PAGE, and unreachable at all on a screen short
   enough not to scroll.

   It did not show before because the footer held only an identity row that
   renderNavChrome() could never draw (EMAIL is null when it runs — see
   paintNavIdentity). §75 puts the emergency Pause-all control down there, so
   "reachable from any screen" now depends on this being right.

   --railtop is app.js's measureRailTop(), the rail's own `offsetTop` — its
   normal-flow position, which no sticky offset and no height set here can move,
   so this cannot feed back into itself. The 0px fallback is the old behaviour,
   for any path where the measurement has not run. */
@media (min-width: 900px) {
  /* PADDING, NOT A SHORTER BOX, and the difference is visible. Shrinking the
     rail to `calc(100vh - railtop)` also fixes the unstuck case, but once the
     page scrolls and `top: 0` pins the rail, its panel then ENDS railtop pixels
     above the bottom of the screen and the page background shows through as a
     seam under the sidebar. Padding keeps the panel full height in both states
     and moves only the content: `margin-top: auto` on `.navfoot` pushes it to
     the bottom of the CONTENT box, which this rule puts exactly on the fold.
     Verified by measurement at 1440x950 — #navSignoutBtn 996 -> 932 against a
     950px viewport.

     18px -> 6px (redesign area "checkins", docs/OWNER-DECISIONS.md
     2026-09-17): the `engage` row shipping to the nav is a thirteenth real
     destination, and the same 950px measurement put Sign out at 962 with
     the base constant still at 18 (after the other trims in this rule's
     sibling block above). Matches `.nav`'s own top padding, now 6px too. */
  .nav { padding-bottom: calc(24px + var(--railtop, 0px)); }
  /* The appearance glyph is the reference's own ◐; at the surrounding 13px it
     reads as a speck, so it is sized as an icon rather than as text. */
  .navtheme .nf-ic { font-size: 15px; line-height: 1; }
}

/* --------------------------------------- THE PHONE'S WRAPPING ROW HAS A BUDGET
   (same §75 block)

   Below 900px there is no rail: the same entries are a wrapping row of chips
   across the top of every screen, and its height is spent by everything under
   it. §75 adds two destinations to that row (Processes, Add device) and on the
   test machine's font metrics they tipped it from three wrapped rows to four —
   +50px — which put "No web filtering is on" at bottom 836 in an 844px viewport
   with 8px of clear space where protection-honesty.test.js requires 40. That is
   this product's most important honesty banner falling off a phone to make room
   for a menu.

   SO THE ROW IS MADE TO FIT INSTEAD, and only horizontally: 13px of side padding
   becomes 10px on the primary nav's own chips. `.chip`'s 44px `min-height` and
   48px `min-width` are untouched, so every touch target stays at the floor this
   file's own nav comment sets; what shrinks is the whitespace either side of a
   word. Scoped to `#navRow .navbtn` — the schedule chips, device-group tabs and
   filter chips elsewhere on the page keep their own spacing.

   MEASURED, both machines, at 390x844: three wrapped rows and 144px, the same
   height the row had before §75 with two fewer entries in it. */
@media (max-width: 899px) {
  #navRow .navbtn { padding-left: 10px; padding-right: 10px; }
}

/* Auth links can be opened from a browser that still has a parent session.
   These account-only surfaces never share the reset/verification window,
   including while a late security response is settling. */
body:not(.signed-in) #enrolBanner { display: none !important; }

/* ===========================================================================
 * THE SIGN-IN WINDOW AND THE SECOND STEP (#loginShell, #twoStep, #enrolBanner)
 * platform/public/index.html + the §70 block in platform/public/app.js
 * PARITY-DECISIONS-2026-08-29.md §70, which AMENDS §10
 * ===========================================================================
 *
 * The three-step identity flow shares one stable frame, and at desktop width
 * that frame has TWO SIDES — the owner, on this screen: "needs to look like the
 * reference image, the full proper 2 sided ui, centered not just aligned left".
 * A phone gets the form alone; the story column is the part that folds away.
 *
 * CENTRED MEANS CENTRED IN THE VIEWPORT. `.wrap` is a 250px rail plus content
 * at >=900px, and the auth screens hold `.nav.hide` — so the rail column was
 * still reserved, empty, and pushed this window right of centre by half of it.
 * The window is horizontally and vertically centred in the space under the
 * banners instead.
 *
 * IT IS IN THE FLOW, NOT A FIXED OVERLAY, so the full form remains scrollable
 * at short heights and does not cover account or recovery messages.
 *
 * EVERY CLASS BELOW IS CHECKED AGAINST THIS FILE BY HAND. style-vocabulary.
 * test.js reads `class="…"` out of index.html and app.js, so the static markup
 * IS covered here — but app.js also builds `.tscode` rows with
 * document.createElement + `className`, which that scan reads through a
 * different regex, and js/*.js modules it does not read at all. The rule that
 * matters is the one that rule exists for: a class named in markup and missing
 * from this file renders as unstyled text with no error anywhere.
 *
 * NO `.screen` GRID TRAP HERE, and it is worth saying why rather than leaving
 * the next reader to wonder: #loginShell is a child of <main>, not of a
 * `.screen` section, so the two-track grid that turns an unspanned mount into a
 * quarter-width column has no reach into it. `.loginwindow` is its own grid and
 * that grid is the design. */

/* `safe center`, not plain centring: on a short viewport the window is taller
   than the box it is centred in, and unsafe centring would clip its top off
   the screen with no way to scroll back to it. */
/* REBUILT 2026-09-21 to actually match the binding 2026-09-16 handoff's real
   markup (Login.dc.html/Signup.dc.html), not an approximation of it —
   Eric, live on production, direct: "this is easy shit.. you literally HAVE
   THE HTML OF THE PAGES all you have to do is convert them". Card is her
   exact max-width 1040px / radius 32px / shadow 0 24px 60px rgba(22,35,63,.08)
   / 1fr:1.2fr split, not this file's own earlier guesses (980px/22px/var(--shadow)). */
.loginshell { display: grid; justify-items: center; align-content: safe center; margin: 0 auto 16px; }
.loginwindow {
  width: min(1040px, 100%);
  display: grid; grid-template-columns: minmax(0, 1fr);
  border: 1px solid var(--border-subtle); border-radius: 32px; overflow: hidden;
  background: var(--dusk);
  box-shadow: 0 24px 60px rgba(22,35,63,.08);
}
/* One column on a phone: the story is the half that folds away, never the
   form. Same rule the reference draws at its own breakpoint. HER PANEL IS
   WHITE, not a navy/pale-blue gradient — `--dusk` IS #FFFFFF in light theme
   (app.css ~209), so this now reads as one seamless white card with a
   hairline divider, exactly like her export, in both themes. */
.loginstory { display: none; padding: 34px; flex-direction: column; background: var(--dusk); border-right: 1px solid var(--border-subtle); }
/* The form sits in the middle of the taller panel, under the step rail. */
.loginpanel { padding: 28px 22px; min-width: 0; display: flex; flex-direction: column; }

@media (min-width: 900px) {
  /* No rail on an auth screen (`.nav.hide`), so the rail's column must go too —
     otherwise the window is centred in the leftover 1190px, not in the page. */
  .wrap:has(.nav.hide) { grid-template-columns: minmax(0, 1fr); grid-template-areas: "head" "main"; }
  /* No rail, no dock, nothing below the window: the 60px reserved for a
     signed-in page's bottom chrome is what made this screen scroll. */
  .wrap:has(.nav.hide) #main { max-width: none; padding-bottom: 0; }
  .loginshell { min-height: min(calc(100vh - 270px), 760px); }
  .loginwindow { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); min-height: 600px; }
  .loginstory { display: flex; padding: 44px clamp(28px, 4vw, 48px); }
  .loginpanel { padding: 44px clamp(28px, 4vw, 52px); }
}

.loginbrand {
  display: flex; align-items: center;
  font-size: 16px; font-weight: 700; letter-spacing: -.01em; color: var(--ink);
  font-family: 'Quicksand', system-ui, sans-serif;
}
.loginbrand b { font-weight: 700; }
.loginbrand img { width: 30px; height: 30px; display: block; flex: none; margin-right: 10px; border-radius: 10px; }
.loginstorymain { margin: auto 0; display: flex; flex-direction: column; }
/* The older reassurance checklist is retired; the current designer story
   uses `.loginlede` and `.loginsub` for one honest ParentProof-team statement. */
.logineyebrow, .loginchecks { display: none; }

/* THREE DOTS, AND THE COLOURS CARRY THEIR USUAL MEANING (UI-DIRECTION §4):
   blue is where you are, mint is what is done, the track colour is what is
   still ahead. */
/* QA SWEEP (2026-09-19): 26px under a 6px-tall rail was the single largest
   "gap" in the create-account card's vertical stack, and it is pure spacing —
   nothing reads it, nothing aligns to it — so it is the first thing trimmed
   to bring the 18+/Terms checkbox back above the fold at 1280x800. */
.loginsteps { display: flex; gap: 7px; margin-bottom: 16px; }
.loginstep { height: 6px; flex: 1; border-radius: 99px; background: var(--track); }
.loginstep.is-active { background: var(--glow); }
.loginstep.is-done { background: var(--mint); }

/* ONE WINDOW, NOT A CARD INSIDE A WINDOW. The auth sections keep `class="card"`
   (every test that finds them by it still does), so their own border, radius,
   shadow and margin are cancelled here rather than removed from the markup. */
/* `margin: auto 0` is the vertical centring: the step rail stays at the top of
   the panel and the form sits in the middle of what is left, as the reference
   draws it. */
/* route() focuses these sections on arrival and Chromium treats that scripted
   focus as `:focus-visible`, so the whole window wore a cyan rectangle. Same
   fix `.screen[tabindex="-1"]:focus` already carries; these are cards, not
   screens, so it never reached them. */
.loginpanel > .card[tabindex="-1"]:focus { outline: none; }
/* The light-theme polish block later in this file repaints `.card` white with
   its own shadow at equal specificity, and being later it wins — which drew a
   white slab around the form inside the window. Restated with the theme
   attribute so the panel keeps one surface in both themes. */
:root[data-theme="light"] .loginpanel > .card {
  background: none; border: 0; box-shadow: none;
}
.loginpanel > .card {
  background: none; border: 0; border-radius: 0; box-shadow: none;
  padding: 0; margin: auto 0;
}

.auth-eyebrow {
  color: var(--glow); font-size: 11px; font-weight: 800;
  letter-spacing: .12em; text-transform: uppercase; margin-bottom: 7px;
}
/* The story column already says "Parent access only"; the card must not say it
   again beside it. Screens whose eyebrow is their own word keep theirs. */
#signin > .auth-eyebrow, #forgotPassword > .auth-eyebrow { display: none; }
.auth-title { margin: 0 0 8px; font-size: 30px; line-height: 1.08; letter-spacing: -.02em; }
.auth-subtitle { margin: 0 0 18px; color: var(--haze); font-size: 14px; line-height: 1.5; }
.auth-mode-link {
  appearance: none; background: transparent; border: 0; color: var(--glow);
  display: inline-flex; align-items: center; justify-content: flex-start;
  min-height: 44px; margin: 8px 0 0; padding: 8px 0;
  font-size: 14px; line-height: 1.35; text-align: left;
}
/* `.fineprint` carries a top margin and no bottom one, so in the create state
   each note sat flush against the next field's label. */
/* QA SWEEP (2026-09-19): the create-account card is the one place five
   `.fld`/`.fineprint` pairs stack in a row, and at 1280x800 the sum of their
   ordinary (single-field-form) margins pushed the required 18+/Terms
   checkbox 35px below the fold (measured: 943px of real content over an
   800px viewport). These four rules are the only spacing this fix touches —
   every number here is a MARGIN between elements, not a field's own
   min-height or padding, and each selector names only #signin's own nodes so
   #forgotPassword, #resetPassword and #twoStep (each a single, short form
   with room to spare) keep the spacing they had. Together they claw back
   ~50px; paired with the rail trim above, the checkbox and the Terms link
   both land inside an ordinary 800px-tall viewport again. */
#displayNameNote, #usernameNote { margin-top: 6px; margin-bottom: 8px; }
#signin .fld { margin-bottom: 5px; }
#signinSubtitle { margin-bottom: 10px; }
#signupTermsRow { margin: 8px 0 2px; }
#signupTermsLink { margin-top: 6px; }
#signin #signinBtn { margin-top: 8px; }

/* ============================================================================
   2026-09-21 AUTH CARD REDESIGN — bolder labels/heading, the testimonial
   block inside .loginstory, the two-position "Forgot your password?" link,
   the darker-navy primary gradient, and the light-theme checkbox fix.
   Scoped to #signin/.loginpanel throughout on purpose: `.primary`, `.fld`
   and `.consentRow` are shared classes used across the whole app (forgot/
   reset/2FA screens, schedule forms, device menus…), and this redesign is
   for the sign-in/create-account card only. Widening any of these globally
   was explicitly out of scope. Source of every exact number below: the
   binding 2026-09-16 handoff, Login.dc.html / Signup.dc.html
   (~/Downloads/ParentProof_Platform_Prototype_1_ (1)/standalone_export/). */

/* Labels read as muted secondary text at 12px/600 everywhere else in the
   app; the mockup's are dark, 13.5px, 700 — matched here, scoped to the
   auth card only. */
#signin .fld, #signin .pw-label { font-weight: 700; font-size: 13.5px; color: var(--cloud); }
:root[data-theme="light"] #signin .fld, :root[data-theme="light"] #signin .pw-label { color: var(--ink); }
.auth-title { font-weight: 800; }

/* THE TESTIMONIAL — sits below .loginchecks inside the existing navy/pale
   .loginstory panel (plans/DECISIONS-2026-09-18-prototype-conflicts.md #9 /
   OWNER-DECISIONS §3.16 addendum 2: a testimonial may sit ALONGSIDE the
   branded reassurance panel, not replace it — see the WAL and the markup
   comment above .logintestimonial in index.html for the full citation). */
/* SIZED FOR THE PANEL'S ONLY CONTENT NOW, not a small addendum under a
   checklist — her export: font-size 25px/1.4, quote mark 34x26. */
.logintestimonial { margin: 0; padding: 56px 0; border: 0; }
.logintestimonial-mark { display: block; font-size: 34px; line-height: 1; color: rgba(255,255,255,.28); font-family: Georgia, serif; margin-bottom: 18px; }
:root[data-theme="light"] .logintestimonial-mark { color: #CFE3F5; }
.logintestimonial-body { margin: 0 0 20px; font-size: 25px; line-height: 1.4; letter-spacing: -.01em; font-weight: 600; color: var(--cloud); font-family: 'Quicksand', system-ui, sans-serif; }
.logintestimonial-attr { font-size: 14.5px; font-weight: 700; color: var(--text-tertiary); font-style: normal; }

/* THE PASSWORD LABEL ROW — sign-in mode only; positionForgotPasswordLink()
   (app.js) moves #forgotPasswordLink into this row. Mirrors the mockup's
   `display:flex;justify-content:space-between` beside the Password label. */
.pw-label-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 5px; }
.pw-label-row .pw-label { margin: 0; }
/* Inside #pwLabelRow the link is inline text, not the bottom-row pill. */
#pwLabelRow #forgotPasswordLink {
  width: auto; flex: none; background: transparent; border: 0; padding: 0;
  min-height: 0; margin: 0; color: var(--glow); font-size: 13px; font-weight: 700;
}
#pwLabelRow #forgotPasswordLink:hover { text-decoration: underline; }

/* THE BOTTOM ROW — holds #signinNote always, and #forgotPasswordLink only in
   create mode, restyled as the mockup's outlined pill beside "Back to sign
   in". positionForgotPasswordLink() leaves the DOM order to CSS `order` so
   the pill always reads before the text link, matching the mockup. */
.signin-bottom-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; }
.signin-bottom-row #signinNote { order: 2; margin: 0; }
.signin-bottom-row .forgot-pill {
  order: 1; width: auto; flex: none; min-height: 0;
  background: transparent; border: 1px solid var(--line); color: var(--cloud);
  padding: 10px 16px; font-size: 14px; font-weight: 700; margin: 0;
}
:root[data-theme="light"] .signin-bottom-row .forgot-pill { color: var(--ink); }
.signin-bottom-row .forgot-pill:hover { border-color: var(--glow); color: var(--glow); }
/* In sign-in mode the pill is moved up into #pwLabelRow, so nothing here
   needs to hide it — there is exactly one node, and it is either here or
   there (positionForgotPasswordLink, app.js). */

/* THE DARKER GRADIENT — scoped to the sign-in/create-account button only
   (`#signin .primary`, i.e. #signinBtn: every other screen's `.primary`
   keeps the existing lighter --glow->#8FE7FF gradient). Mockup:
   `linear-gradient(135deg,#1B4A73 0%,#4FA8E0 100%)`. #1B4A73 is darker than
   both this file's --dusk/--ink tokens, so it is a literal colour here
   rather than a token substitution — a new token for one button was not
   worth it. CHECKED, NOT ASSUMED (WCAG contrast formula, white text): the
   mockup's own light stop #4FA8E0 is only 2.62:1 against white, and the
   button's 16px/700 label does not qualify as WCAG "large text" (needs
   18.66px bold+), so it needs 4.5:1, not 3:1. Darkened the light stop 30%
   to #37759C (5.02:1) — same hue, same "dark navy to blue" read, but AA-safe
   across the whole gradient rather than only its dark half. */
#signin .primary {
  background: linear-gradient(135deg, #1B4A73 0%, #37759C 100%);
  box-shadow: 0 10px 24px rgba(27,74,115,.28);
}
#signin .primary, :root[data-theme="light"] #signin .primary { color: #FFFFFF; }

/* THE CHECKBOX FIX — same root cause and same proven pattern as commit
   0c1e812c's .device-menu-panel fix (index.html:31's
   `<meta name="color-scheme" content="light dark">` with nothing in this
   file ever declaring `color-scheme`, so a dark-preferring OS paints a
   near-black checkbox over the app's light theme regardless of data-theme).
   Scoped to .loginpanel's own two checkboxes (#rememberMeRow,
   #signupTermsRow — both `.consentRow`) rather than a global fix, same
   reasoning as that commit: `.consentRow` is shared (e.g. the schedule
   app-list), and repainting every native control across the app needs its
   own verified pass, not a drive-by here. */
:root[data-theme="light"] .loginpanel .consentRow,
:root:not([data-theme]) .loginpanel .consentRow { color-scheme: light; }
:root[data-theme="dark"] .loginpanel .consentRow { color-scheme: dark; }
.loginpanel .consentRow input[type="checkbox"] { accent-color: var(--glow); }

/* ============================================================================ */

/* ---- the second step ---------------------------------------------------- */

.tspanel { display: block; }
/* `.fld` carries no top margin (it is normally the first thing in a card),
   so inside these panels every label sat flush against the paragraph above
   it — seen in the 390x844 capture, where "Verification code" ran straight
   on from the sentence explaining it. */
.tspanel .fld { margin-top: 14px; }
/* A six-digit code is read back digit by digit, so it is set like one: wide
   tracking, centred, and large enough that a parent checking it against a phone
   in their other hand does not have to squint. */
.tsbigcode {
  font-size: 24px; letter-spacing: 8px; text-align: center; font-variant-numeric: tabular-nums;
}
/* The TOTP setup key. `word-break` because twenty-six characters plus spaces do
   not fit a 350px phone column, and a key that overflows its box is a key a
   parent cannot copy by hand. */
.tskey {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px; letter-spacing: 1px; line-height: 1.6; word-break: break-all;
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 14px; margin: 10px 0; color: var(--cloud);
}
.tskeyrow { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.tskeyrow > * { flex: 1 1 auto; text-align: center; }
/* "Open in an authenticator app" is an <a href="otpauth://…"> and not a button,
   because it has to be a real link for the phone's app chooser to take it. It
   still sits beside a `.ghost` button and must not look like a stray sentence:
   `.ghost` sets no display, radius or line-height (those come from the bare
   `button` rule), so an anchor wearing the same class needs all three, plus the
   underline off. Measured in a screenshot before this rule existed. */
.tskeyrow > a.ghost {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 13px; text-decoration: none; font-weight: 700; box-sizing: border-box;
}

.tscodes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px;
  margin: 12px 0;
}
@media (max-width: 420px) { .tscodes { grid-template-columns: 1fr; } }
.tscode {
  display: flex; gap: 10px; align-items: baseline;
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px; min-width: 0;
}
.tscode > b { color: var(--haze); font-size: 11px; font-weight: 700; flex: none; width: 16px; }
.tscode > span {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px; color: var(--cloud); overflow-wrap: anywhere; min-width: 0;
}
.tsconfirm { margin-top: 14px; }

/* The confirmed interstitial. Mint, because it is the one screen on this page
   that reports something that actually happened — the same "healthy/applied"
   role the rest of this file gives it. */
.tsdoneicon {
  width: 62px; height: 62px; border-radius: 20px; display: grid; place-items: center;
  background: linear-gradient(135deg, var(--glow), var(--mint));
  color: #07131F; font-size: 28px; font-weight: 900; margin: 0 0 14px;
}
/* SAME FIX AS `.primary` A FEW SCREENS UP: dark ink reads fine on dark theme's
   bright cyan-to-mint gradient (--glow #69D8FF, --mint #4EE0A5) but light
   theme's --glow/--mint are the much darker text-safe hues (#2B6CB0, #2F7B50)
   -- painted as a gradient BACKGROUND instead of text, #07131F ink on top of
   them was a near-black glyph on a dark blue-to-green tile sitting in an
   otherwise all-white card (confirmed by screenshot, GoodQA light-theme audit
   2026-09-19: this is the very first thing a parent with 2FA on sees after
   every sign-in, `#twoStepDone`). White clears AA on both gradient endpoints,
   same as `.primary`'s own light override: 5.42:1 on #2B6CB0, 5.16:1 on
   #2F7B50 (computed, WCAG 2.x relative luminance). */
:root[data-theme="light"] .tsdoneicon { color: #FFFFFF; }
.tsproof {
  display: grid; gap: 8px; margin: 16px 0;
  border: 1px solid var(--line); border-radius: 14px; padding: 12px 14px; background: var(--dusk2);
}
.tsproofrow { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
.tsproofrow > b { color: var(--haze); font-weight: 700; }
.tsproofrow > span { color: var(--cloud); text-align: right; overflow-wrap: anywhere; }

/* §63/§71'S FOLD, AND THIS BANNER NEARLY TOOK IT AGAIN.
 *
 * §71 records the cost of the last time: the pre-release banner "pushed the
 * 'No web filtering is on' notice below the fold on a phone (§63) — a real
 * honesty defect that cost a day to find". #enrolBanner is a SECOND
 * page-global banner above the same screen, and with its explanatory line it
 * put that notice at 858px on a 390x844 phone. Caught by
 * protection-honesty.test.js, which measures it rather than trusting anyone's
 * eye — and which is why that test exists.
 *
 * So on a phone the banner is a title and a button: the two things that make it
 * ACTIONABLE. The explanation is not lost — it is on the Security card the
 * button leads to, where the parent is actually doing the thing. Nothing about
 * what this deployment claims is hidden; a sentence about a task moves to the
 * screen where the task is done. */
@media (max-width: 899px) {
  /* ON A PHONE THIS BANNER LEAVES THE DOCUMENT FLOW ENTIRELY, and the numbers
     are why rather than a preference.
     MEASURED on the CI box at 390x844: the compact banner is 80px tall plus a
     10px gap, and protection-honesty.test.js's budget leaves the filtering
     notice EIGHTEEN PIXELS of headroom before it. There is no in-flow banner
     that fits in 18px — not a tightened one, not a one-line one — so the choice
     is between costing the fold and not being in the flow at all.
     Pinned to the bottom of the viewport it costs the fold exactly zero, on any
     renderer, which is what makes this robust rather than another five-pixel
     margin waiting for the next font change. It is also, on a phone, the place
     a persistent call to action belongs: always visible without scrolling,
     where a top banner scrolls away.
     `bottom: 84px` clears the 73px phone navigation bar with 11px of air. */
  #enrolBannerBody { display: none; }
  #enrolBanner {
    position: fixed; left: 12px; right: 12px; z-index: 29;
    bottom: calc(84px + env(safe-area-inset-bottom, 0px));
    margin: 0; padding: 10px 12px;
    /* `.banner.warn`'s fill is a 12%-alpha wash meant to sit on a card. Over
       scrolling content it has to be opaque, so the same wash is composited on
       the page ground rather than a new colour being invented. */
    background: linear-gradient(rgba(255,194,75,.12), rgba(255,194,75,.12)), var(--bg);
    box-shadow: var(--shadow);
  }
  /* The last card must stay reachable above a pinned strip. Driven by a class
     app.js sets when the banner is actually shown, so a parent who has enrolled
     gets no dead space — and without needing `:has()`, which this file uses
     nowhere else.
     The strip's top edge is 84 + 86 = 170px above the bottom of the viewport,
     and `body.signed-in` already reserves the bar's own 92px on top of this.

     `body.signed-in.enrolpinned`, NOT `.enrolpinned` — AND THE SELECTOR IS THE
     WHOLE FIX, because as `.enrolpinned #main` this rule never once applied.
     `body.signed-in #main { padding-bottom: 18px }` (app.css:2841, the same
     `@media (max-width: 899px)`) is 1 id + 1 class + 1 TYPE = (1,1,1);
     `.enrolpinned #main` is 1 id + 1 class + no type = (1,1,0). The 18px wins on
     the type selector no matter which comes last in the file, so the 150px was
     dead from the day it was written.

     MEASURED IN CHROMIUM at 390x844, signed in, banner up:
     `html.enrolpinned` true, `scroll-padding-bottom` 178px — both correct — and
     `getComputedStyle(#main).paddingBottom` **18px**. The page therefore could
     not scroll far enough to lift its last control clear of a fixed strip whose
     top edge is 170px up, and Playwright's own report on the Websites card is
     the symptom in one line: "#enrolBanner intercepts pointer events", 60 retries,
     after "done scrolling". A parent meets that as a button that does nothing —
     which is exactly what the block above this one says scroll-padding exists to
     prevent, and it needed the space to scroll INTO as well.

     `body` carries the class too (app.js's setEnrolBanner toggles it on both
     body and documentElement — see its own comment), so this matches whenever
     the old selector would have. */
  body.signed-in.enrolpinned #main { padding-bottom: 150px; }
  /* THE OFFER VARIANT, in the selector shape the comment above fixed.
     gpt-ux added `.enrolpinned.enroloffer #main { padding-bottom: 18px }` beside
     its own `.enrolpinned #main` — and it inherits exactly the specificity bug
     documented above ((1,2,0) still loses to `body.signed-in #main`'s type
     selector), so it would have been dead on arrival too. Carried across in the
     corrected `body.signed-in...` form instead of verbatim.
     WHY IT IS NEEDED AT ALL: app.js:10635 toggles `body.enroloffer` whenever the
     banner's tone is 'offer', and 10594 removes it otherwise. An OFFER is one
     line, not the 86px warning strip, so reserving the warning's 150px under it
     leaves a dead gap under the last card. Without this rule the class is set on
     <body> and styles nothing — which is what style-vocabulary.test.js caught. */
  body.signed-in.enrolpinned.enroloffer #main { padding-bottom: 18px; }
}

/* ==========================================================================
   DEVICES · REQUESTS · ACTIVITY · HOUSEHOLD — REDESIGN 2026-08-29
   ==========================================================================
   Appended at the end of the file, the convention every GoodQA feature block
   above already follows: purely ADDITIVE selectors, so a merge with concurrent
   work on another screen is a clean append on both sides. A few rules here
   deliberately RE-DECLARE an existing selector later in source (`.reqcard`,
   `.reqhead .an`, `.devgroupbody`, `.reqactions`'s margin) rather than editing
   the original — the original stays readable next to its own comment, and
   reverting this pass is a deletion of this block rather than an archaeology
   exercise.

   NO NEW HEX AND NO NEW COLOUR MEANINGS (UI-DIRECTION-2026-08-22 §3). Every
   colour below is one of this file's own tokens, or one of the light/dark text
   literals it already uses for the identical problem elsewhere (#0A6242 =
   `.pill.on` light, #B23127 = `.pill.off`/`.banner.err` light, #FFB4AC =
   `.pill.off` dark, #0A6E8C = `.devactionbtn.setup` light). That matters here
   specifically: devices-ui.test.js runs axe-core over #devices at 390x844 AND
   1280x900 in BOTH themes, so a hue picked by eye fails the suite.

   MOBILE IS THE MEASURED CASE. Everything that only belongs on a wide screen
   (the request row's side-by-side actions, the activity clock column, the
   full-width card bands) is inside a media query; below those breakpoints
   these screens keep the single-column shape e2e/mobile.spec.cjs and
   protection-honesty.test.js measure at 390x844.
   ========================================================================== */

/* ------------------------------------------------------------- devices card
   The reference build's device tile: an identity band, a four-signal health
   strip and an action row, stacked full width — not `.devrow`'s original two
   side-by-side columns, where a four-button action stack squeezed the name
   column (the same flex-squeeze `#profList .arow` and `#sitesList .arow` each
   already carry their own fix for further up this file).

   `.devcard` is a SECOND class on the same element and the DOM under it is
   byte-for-byte what it was: parent-lock-ui.test.js asserts this row's three
   direct children are [nav button, health capsule, lock control] as siblings,
   and that the nav column holds [name row, last-seen, platform line] in that
   order. So the card is built entirely out of CSS over the existing tree. */
.devrow.devcard {
  display: flex; flex-direction: column; gap: 11px;
  padding: 14px; margin-bottom: 0; border-radius: 16px;
}
.devcard .devbtn { padding: 0; min-height: 0; align-items: flex-start; gap: 10px; }
/* A flex column is what lets `order` lift the platform line to the top (see
   `.devplat` below) without moving it in the document. */
.devcard .devnm { display: flex; flex-direction: column; }
.devcard .devnmtext { font-size: 17px; font-weight: 750; letter-spacing: -.01em; }
.devcard .devsub { font-size: 11.5px; }
.devcard .devopen { margin-top: 2px; }
/* THE EYEBROW. `.devplat` is the platform sentence app.js has always rendered
   ("Android tablet" — it read "Supervised Android device" until tablet-first
   setup, when the word left the string because `dev.platform` cannot know
   whether anything is enforcing), promoted to the top of the card in PAINT
   order only — `order: -1`, never a move in the markup, because the fixture in
   parent-lock-ui.test.js reads that line at a fixed child index. --haze and
   not --glow: this text is 10.5px, so it needs 4.5:1, and --haze is the one
   secondary colour in this file measured at that ratio against --dusk2 in both
   themes (see its own comment on :root[data-theme="light"] near the top). */
.devcard .devplat {
  order: -1; margin: 0 0 3px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
}
/* A per-row banner inside a card does not need the 16px a page-level banner
   carries — it already sits inside 14px of card padding. */
.devcard .banner { margin: 6px 0 0; padding: 9px 11px; }

/* The health strip. `.healthcap` is still the same single button with the same
   data-device-id / data-health / aria-label devices-ui.test.js reads; it is
   now full width, with each signal's own real label beside its dot instead of
   four bare dots whose meaning lived only in a `title` attribute. --dusk
   inside a --dusk2 card reads as an inset panel in BOTH themes (darker on
   dark, lighter on light), which is why the fill is a token swap and not a
   translucent wash — a wash is also what makes axe report an indeterminate
   background instead of a measured one. */
.devcard .healthcap {
  width: 100%; padding: 9px 11px; border-radius: 12px; background: var(--dusk);
  display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 7px 12px; text-align: left; align-items: start;
}
.hccell { display: flex; align-items: flex-start; gap: 7px; min-width: 0; }
.hccell .hcdot { margin-top: 4px; }
.hctxt {
  font-size: 11px; font-weight: 700; line-height: 1.25; color: var(--haze);
  min-width: 0; overflow-wrap: anywhere;
}
/* COLOUR PER SIGNAL, NEVER ONE ROLLED-UP GREEN. The reference build paints all
   four of these cells from a single online/paused boolean; here each label
   takes its own signal's colour, so a device that is online and applied but
   has had supervision released renders three green cells beside one red one.
   The light-theme values are the already-measured ones this file uses for the
   identical problem elsewhere — never a fresh hue. */
.hcdot-green + .hctxt { color: var(--done-text); }
.hcdot-amber + .hctxt { color: var(--waiting-text); }
.hcdot-red + .hctxt { color: #FFB4AC; }
:root[data-theme="light"] .hcdot-green + .hctxt { color: #0A6242; }
:root[data-theme="light"] .hcdot-red + .hctxt { color: #B23127; }
/* @media (prefers-color-scheme: light) half removed — same reason as the
   navbtn one earlier in this file: light is reached only through
   data-theme="light", so an OS-preference query repainting this in the
   untoggled (dark) state was the defect, not a fallback. */

/* The action row. `.devlock` was a narrow right-hand COLUMN; on a card it is a
   wrapping row, with the delivery sentence ("Pause requested…", "Applied ✓")
   on its own full-width line under the buttons rather than splitting them in
   half. `.devlockbtn`'s own 48px floor is untouched — parent-lock-ui.test.js
   reads that literal out of this stylesheet. */
.devcard .devlock { flex-direction: row; flex-wrap: wrap; gap: 8px; padding: 0; align-items: center; }
.devcard .devlockbtn, .devcard .devactionbtn { width: auto; flex: 1 1 auto; min-width: 128px; }
.devcard .devlockstate { flex: 1 0 100%; min-height: 0; text-align: left; }

/* Two-up (or more) device tiles wherever the column is wide enough, one column
   on a phone — `auto-fill` plus `min(100%, 380px)` answers that from the
   container's real width, so no breakpoint has to guess it. Re-declared rather
   than edited: the original `.devgroupbody` rule keeps its own comment. */
.devgroup > .devgroupbody {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr));
  gap: 12px; margin-top: 8px; align-items: start;
}
.devgroup > .devgroupbody > .muted { grid-column: 1 / -1; }
.devgroup > .chips { margin: 6px 0 2px; }

/* The per-device pause result list. Each <li> is an `.arow` carrying the device
   name and ONE pill with the route's own per-device outcome — never a blanket
   "Paused ✓" (renderPauseAllResult in app.js owns that promise). */
.pauseAllResults .pauseAllResultRow { align-items: center; gap: 10px; }
.pauseAllResults .pauseAllResultRow .an { font-size: 13px; font-weight: 600; }

/* ------------------------------------------------------------- requests row
   Monogram, eyebrow, address, scope — with the three decisions beside them on
   a wide screen and under them on a phone. */
.reqcard { padding: 14px 16px; }
.reqhead { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
.reqtext { flex: 1; min-width: 0; }
.reqeyebrow {
  font-size: 10.5px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase;
  color: var(--haze); margin-bottom: 3px;
}
.reqcard .reqtext .an { font-size: 16px; font-weight: 700; }
/* HER APPROVE/DENY SPEC: 8px/14px at Quicksand 12.5px, the 34px pill she drew.
   This rule was re-stating 10px/15px/13px/42px and, because it is more specific
   than `.iconbtn`, it silently beat the base rule -- so fixing `.iconbtn` alone
   changed nothing on the one screen these buttons actually appear on.
   min-height follows the same touch rule as `.iconbtn`: 44px is the floor, her
   34px applies only at >=900px with a fine pointer. */
.reqactions .iconbtn { padding: 8px 14px; font-size: 12.5px; min-height: 44px; }
@media (min-width: 900px) and (pointer: fine) {
  .reqactions .iconbtn { min-height: 34px; }
}
/* ALLOW IS THE PRIMARY DIRECTION, so it takes the cyan this page already means
   "trusted + primary" by, and No keeps `.iconbtn.danger`'s coral. "Allow
   scoped" stays undecorated on purpose — it is a form of allow, not a third
   meaning (openAskRow's own comment in app.js). #0A6E8C on light is the exact
   value `.devactionbtn.setup` already uses for cyan-on-white. */
.iconbtn.affirm {
  /* See the identical rule earlier in this file for the full reasoning: her
     Approve is a SOLID green pill with white text, and this moves the
     affirmative action from blue to green (a semantic change, flagged in
     plans/DECISIONS-2026-09-20-design-fidelity-over-contrast.md).
     THIS copy is the one that actually took effect: lines 3558-3602 are
     duplicated verbatim at 6479-6523, so the later copy won and editing only
     the first changed nothing on screen. Both are kept identical deliberately
     -- removing the duplicate would reorder the cascade against ~2900 lines of
     rules defined between them. Deduplication is filed as a follow-up. */
  color: #FFFFFF;
  border-color: transparent;
  /* Keep the duplicated cascade copy identical (see comment above). */
  background: #2F8053;
}
/* @media (prefers-color-scheme: light) half removed — same reason as above:
   light is reached only through data-theme="light". */
@media (min-width: 900px) {
  .reqcard { display: flex; align-items: center; gap: 18px; }
  .reqcard > .reqhead { flex: 1; min-width: 0; }
  .reqcard > .reqactions { margin-top: 0; flex: none; }
}

/* ------------------------------------------------------------ activity list
   One divided ledger inside the card instead of a stack of separately bordered
   rows: a clock column, a category glyph, then the sentence. The
   `.arow`/`.an`/`.asub` shape underneath is untouched — activity-ui.test.js
   reads `.an`'s first child node and the row's first `.asub`. */
#activityList .arow.actrow {
  background: transparent; border: 0; border-radius: 0; margin-bottom: 0;
  border-bottom: 1px solid var(--line);
  padding: 11px 2px; gap: 10px; align-items: flex-start;
}
#activityList .arow.actrow:last-child { border-bottom: 0; }
.acttime {
  flex: none; width: 62px; padding-top: 5px; white-space: nowrap;
  font-size: 11.5px; font-weight: 700; color: var(--haze); font-variant-numeric: tabular-nums;
}
/* A neutral tile, never a coloured one: colour on this page is a health claim,
   and "a website was blocked" is an event rather than a state. */
.acticon {
  flex: none; width: 30px; height: 30px; border-radius: 10px; background: var(--dusk2);
  display: flex; align-items: center; justify-content: center; font-size: 14px; line-height: 1;
  color: var(--haze);
}
/* GOODQA #469 — the '•' fallback still uses the tile's own font-size above;
   real icons (fillActicon() in app.js) are inline SVGs on the same 24x24/
   stroke-currentColor grid as the nav icons, so they take their own size and
   this tile's `color` (unset before this pass — emoji do not obey `color`,
   so nobody needed one) rather than inheriting the row's body text colour. */
.acticon svg { width: 16px; height: 16px; }
/* The clock is the one part of the row a 390px phone has no width for, and it
   costs nothing there: the `.asub` under every sentence already carries
   "{child} · {when}". Same 560px breakpoint the add-a-device swap above uses. */
@media (max-width: 560px) {
  .acttime { display: none; }
}

/* ---------------------------------------------------------- household rows
   The profile list is the reference build's "Child profiles" card: a face, a
   name, what it is bound to, and its controls. Deliberately does NOT touch
   `#profList .arow { flex-wrap: wrap }` / `.an { min-width: 55% }` above —
   inventory-ui.test.js deletes those two lines byte-for-byte to prove its own
   390px measurement can fail, so re-stating either here would silently disarm
   that control. */
#profList .arow.profrow .an { font-size: 15px; font-weight: 700; }
#profList .arow.profrow .asub { font-weight: 400; margin-top: 2px; }
/* An archived profile cannot be bound to anything and is not a live row; it was
   previously indistinguishable from a live one apart from its sentence. */
#profList .arow.archived { opacity: .72; }

/* --------------------------------------------------------- full-width bands
   At the sidebar breakpoint `.screen` is a two-track grid (see its own rule
   near the top of this file). These three are LISTS, not cards in a column:
   the reference build gives each the full content width and lets the list
   itself decide how many tracks it wants. `:has()` rather than an id list, for
   the same reason the `.screen` rule keys on a class — the markup these select
   is outside this pass's territory and must not have to be edited to opt in.
   Inside the media query, so a phone is untouched. */
@media (min-width: 900px) {
  .screen > .card:has(#devList) { grid-column: 1 / -1; }
  .screen > .card:has(#activityList) { grid-column: 1 / -1; }
  .screen > #asksCard { grid-column: 1 / -1; }
}

/* ==========================================================================
   ACCOUNT & SETTINGS — RESTYLE 2026-08-29 (js/account.js)
   Layout only. Every class the module draws with is one this stylesheet
   already defined for another screen (.ovhead/.ov-eyebrow/.ov-greet/.ov-sub,
   .ovtiles/.ovtile/.ovt-k/.ovt-v/.ovt-s, .chips/.chip[aria-pressed]) — this
   block adds no new vocabulary, only the placement those classes need inside
   a mount div. Declared here rather than beside the module because
   style-vocabulary.test.js scans app.js and index.html only, so a class used
   solely inside a js/*.js module is invisible to it (the .cardhead comment
   above is the incident that rule comes from).
   ========================================================================== */
@media (min-width: 900px) {
  /* Same reason #overviewMount and #engageMount each need their own rule:
     #settingsMount is a wave-0 mount div one level below `.screen`, so
     `.screen`'s two-track grid cannot place its children — before this, all
     eight account cards stacked in ONE half-width track with the other track
     left empty. */
  /* `align-items: start` and not the grid default `stretch`, which the other
     two mount grids can leave alone because their cards are all roughly one
     height. These are not: #planCard is a single sentence and the billing
     panel beside it is a three-tier price list, and a stretched one-line card
     rendered as ~600px of empty box. MEASURED in a 1440x950 screenshot, the
     way #profList .arow's own comment in this file says these are found. */
  #settingsMount { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; align-content: start; align-items: start; }
  #settingsMount > .card { margin-top: 0; margin-bottom: 0; }
  /* The header, the count row and the group rail are about the page, not
     about one card in it. */
  #settingsMount > .ovhead, #settingsMount > .ovtiles, #settingsMount > .chips { grid-column: 1 / -1; }
  /* Exactly the reasoning `#overview > .toprow` above already carries: the
     header block states the screen, so the generic "Settings" title directly
     over it would be the same word twice. */
  #settings > .toprow { display: none; }
}

/* MOBILE IS LOAD-BEARING (e2e/mobile.spec.cjs measures a real 390x844 phone,
   and a previous pass on another screen pushed content below the fold there).
   `.ovtiles` is auto-fit/minmax(180px), so on a 390px screen the three count
   tiles would stack into ~200px of chrome above the group rail — and they are
   a convenience only: each number is also readable from the list in the card
   it summarises. So the phone gets the header and the rail, and the tiles are
   a wide-screen affordance. */
@media (max-width: 899px) {
  #settingsMount > .ovtiles { display: none; }
}

/* ==========================================================================
   THE TWO-STEP OFFER BECOMES ONE ROW — owner review 2026-09-03

   "A two-step-verification nag banner occupies the top of EVERY page, ~90px,
   with a 3-line paragraph and two buttons. It is the first thing on every
   screen forever."

   MEASURED at 1440x900 before this pass: 94px tall, four lines of text, above
   the greeting, on every route. The 40-word body is cut to its outcome (see
   `enrol.offerBody` in app.js for what went where and why), and these rules
   stop the remaining sentence being stacked under its own heading and stop the
   two buttons wrapping to "Turn / it on" and "Not / now" — which was what
   actually set the 94px, not the text.

   SCOPED TO `.is-oneline`, WHICH ONLY THE OFFER CARRIES. The two MANDATORY
   variants — §70's "required" and §107's "codes" — keep the stacked shape,
   deliberately: those state a consequence that changes what happens at the
   parent's next sign-in, they are read once and carefully, and they have no
   second button to be distinguished from. It is the bar a parent meets on
   every screen until they answer it that had to get shorter.

   `display: inline` ON THE `b`, NOT A MARKUP CHANGE, because #enrolBannerTitle
   and #enrolBannerBody are two nodes app.js re-labels per variant and both
   must keep their ids and their order.

   THE ACCEPT BUTTON IS THE PRIMARY ONE (UI-DIRECTION §3: cyan is the primary
   action). Both shipped as `.ghost` — identical weight — so a parent had to
   read the words to find the action. `.primary` on its own is a full-width
   50px block with a 12px top margin, which is right for a card footer and
   wrong inside a one-line bar, so it is re-stated here at banner scale. No new
   colour: the same gradient, the same ink.
   ========================================================================== */
.banner.is-oneline { align-items: center; padding: 10px 14px; }
.banner.is-oneline b { display: inline; margin-right: .4em; }
.banner.is-oneline .dot { margin-top: 0; }
.banner.is-oneline > span:not(.dot) { flex: 1 1 auto; min-width: 0; }
/* Both buttons: compact, and never wrapped. A two-word label broken across two
   lines is what made this bar twice as tall as its own content. */
/* COMPACT IS ABOUT WIDTH AND WRAPPING, NOT ABOUT BEING 36px TALL. That is what
   `white-space: nowrap` and `flex: 0 0 auto` are for, and both are untouched.
   The 36px height was the one part of "compact" that cost something real: it
   put both decisions of the two-step offer — Turn it on, Not now — under the
   target minimum at 440, 768 and every width from 900 up, in all three
   languages, while the `.is-oneline` rules in the phone block below were
   already asking for "real 44px targets" and getting them at 320 and 390 only.

   MEASURED BEFORE CHANGING IT, because account-security-card-ui.test.js holds
   this banner to a height ("the phone two-step banner is still ${h}px tall",
   72px when optional) and raising a control inside a budgeted box is a trade
   to prove rather than assume. Both buttons forced to 44px through CSSOM — an
   injected <style> is refused by the page's own CSP, the same trap that
   defeats addScriptTag for axe:

     320  banner 52 -> 52     390  banner 52 -> 52   (the tested viewport)
     440  banner 52 -> 60     768  banner 52 -> 60   budget 72
     900  banner 79 -> 79

   Twelve pixels of headroom at the worst width, and no movement at all at the
   viewport the assertion is made from. */
.banner.is-oneline button {
  flex: 0 0 auto; white-space: nowrap; width: auto; margin: 0;
  padding: 8px 12px; min-height: 44px; font-size: 13px; border-radius: 10px;
}
.banner.is-oneline button.primary {
  background: linear-gradient(135deg, var(--glow), #8FE7FF); color: var(--ink);
  border: 1px solid transparent; box-shadow: none;
}
/* On a narrow column the buttons drop under the sentence instead of squeezing
   it into a ribbon. Below 900px `#enrolBannerBody` is already display:none and
   this bar is the pinned strip the --phonedock block measures, so all that is
   left to keep on one line there is the title and its two buttons. */
@media (max-width: 620px) {
  .banner.is-oneline { flex-wrap: wrap; }
  .banner.is-oneline > span:not(.dot) { flex: 1 1 100%; }
}

/* ==========================================================================
   ONE VERB, ONE APPEARANCE — owner review 2026-09-03

   "BUTTON HIERARCHY IS RANDOM. On one card: 'Bedtime ›', '+15 min', 'Web
   controls ›' are neutral while 'Pause now' is red; elsewhere Pause is a
   full-width tinted block. Destructive, primary and navigational actions
   should be distinguishable at a glance and consistent across screens."

   PAUSE, PHOTOGRAPHED IN THREE PLACES ON ONE BUILD:
     Home, child card   — `.ghost.danger`: coral text, transparent ground.
     Rail, Pause all    — `.navpause`: coral text, transparent ground.
     Devices, per card  — `.devlockbtn.lock`: coral TEXT ON A CORAL WASH,
                          full width, which at a glance reads as a warning
                          BANNER rather than as a button. Two of the three sit
                          on the same screen, four inches apart, and a parent
                          has no way to know they are the same kind of thing.

   THE WASH GOES; NOTHING ELSE ABOUT THE CONTROL CHANGES. The hue still carries
   the meaning UI-DIRECTION §3 assigns it (coral = destructive/paused, mint =
   healthy), the button keeps its full width — it is the card's one action and
   that is a legitimate reason to be wide — and it keeps `.devlockbtn`'s 48px
   floor, which parent-lock-ui.test.js pins by name. What changes is that its
   WEIGHT now matches the other two, so "Pause" looks like "Pause" wherever a
   parent meets it.

   AND THE SAME FOR ITS OPPOSITE. `.unlock` (Resume) carried a mint wash for
   symmetry with the coral one; it loses it for the same reason, and keeps the
   mint that says which state it moves to.

   CONTRAST IS UNCHANGED IN BOTH THEMES because the text colours are: #FFDED9
   and var(--mint) on the dark card, and this file's own proven light-theme
   substitutes #8E2922 / #0A6242 — the values the block near the top of this
   file measured with axe precisely because bare --coral and bare --mint fail
   on white. devices-ui.test.js re-runs axe over #devices at 390x844 and
   1280x900 in both themes, so a value picked by eye here would fail the suite.
   Removing a 13-16% alpha wash from behind text that already met 4.5:1 over
   the card raises contrast; it cannot lower it.

   THE SECOND LOG OUT GOES TOO. At rail width the Devices header carried a
   `Log out` button four inches from the rail's own `Log out` row — two
   controls, identical label, identical effect, on screen together. The rail is
   where every other account-level control lives (Appearance, language, Pause
   all devices, the signed-in address), so the rail's is the one that stays.
   BELOW 900px THE PAGE HEADER'S IS THE ONLY ONE THERE IS — app.css hides
   `.navfoot` outright on a phone and only `#nav.mobile-open` brings it back —
   so this is scoped to the width where the duplicate actually exists.
   nav-reference-parity.test.js asserts a phone still has a sign-out on
   Devices; that check runs at 390x844 and is untouched.

   APPENDED, NEVER INTERLEAVED, like every feature block above.
   ========================================================================== */
.devlockbtn.lock { background: transparent; }
.devlockbtn.unlock { background: transparent; }
@media (min-width: 900px) {
  #devices .ovhead > #signoutBtn { display: none; }
}

/* --------------------------------------------------- the tablet's security log
   js/security.js's two cards, at the foot of a tablet's own page: the recovery
   PIN (a button, never the digits) and the tamper log.

   THE LEDGER REUSES THE ACTIVITY LIST'S SHAPE — `.arow.actrow`, `.acttime`,
   `.an`, `.asub` — rather than inventing a second row vocabulary, so the two
   timelines a parent reads on two screens look like the same product. Only the
   container scoping is new, because these rows are not inside #activityList. */
#securityCards .seclog .arow.actrow {
  display: grid;
  grid-template-columns: minmax(104px, 132px) minmax(0, 1fr);
  background: transparent; border: 0; border-radius: 0; margin-bottom: 0;
  border-bottom: 1px solid var(--line);
  padding: 11px 2px; gap: 10px; align-items: flex-start;
}
#securityCards .seclog .arow.actrow:last-child { border-bottom: 0; }
#securityCards .seclog .acttime {
  width: auto;
  padding-top: 2px;
  white-space: normal;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
#securityCards .seclog .secbody { flex: 1; min-width: 0; }
#securityCards .seclog .asub { margin-top: 2px; }
@media (max-width: 560px) {
  #securityCards .seclog .arow.actrow { grid-template-columns: minmax(0, 1fr); gap: 4px; }
  #securityCards .seclog .acttime { display: block; grid-column: 1; padding-top: 0; }
  #securityCards .seclog .secbody { grid-column: 1; }
}
/* The revealed PIN itself. Large and tabular because it is about to be TYPED,
   on a tablet, by somebody reading it off this screen — six digits set in the
   body font at 14px is how a 3 gets read as an 8. */
#securityCards .bigvalue {
  font-size: 20px; font-weight: 700; letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums; margin: 10px 0 4px;
}
/* CHOOSING A PIN — the control that makes "changes it on the webportal
   themselfs" true (owner decision 2.2). A subsection of the Recovery PIN card,
   not a card of its own: an h4 rather than an h3 because the h3s in this
   container ARE the cards, and device-workspace-sections.test.js counts them.
   The field is sized like the revealed value above it for the same reason —
   six digits a parent is choosing for a tablet they may later be typing them
   into, where a 3 read as an 8 costs them the lockout ladder. */
#securityCards .setpinhead {
  margin: 18px 0 4px; font-size: 15px; font-weight: 700;
  border-top: 1px solid var(--line); padding-top: 16px;
}
#securityCards .pinfield {
  display: block; margin: 8px 0 12px; width: 8em; max-width: 100%;
  font-size: 20px; font-weight: 700; letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums; text-align: center;
}

/* =========================================================================
   THE 8-CHARACTER SETUP CODE, ON THE DEVICE DETAIL SCREEN
   =========================================================================

   THE FAILURE THIS IS SIZED FOR. The setup code is minted on the tablet and
   printed ONCE, on the installer's last screen. The tablet is usually still
   off Wi-Fi at that moment, so it can never register its own key; the parent
   reads the screen, unplugs, and the code is gone. On a tablet with no parent
   PIN it is one of only TWO ways in (the other is a published constant), so
   losing it means a factory reset. The owner reported a family who did exactly
   that.

   TWO STATES, ONE CARD, AND IT IS NEVER REMOVED.

     .sc-critical   the tablet has not yet checked in with a parent PIN and an
                    escape-hatch PIN of its own, so this code is a lifeline.
                    Amber, matching `.banner.warn` value-for-value rather than
                    inventing a fourth amber, because that is the tone this
                    page already uses for "this needs you".
     .sc-quiet      the tablet is self-sufficient. The card stays, in the
                    ordinary card colours, with the code still readable — a
                    parent who scrolls looking for a code they were told was
                    saved and finds nothing concludes the product lost it.

   THE CODE ITSELF IS 22px AND MONOSPACE WITH WIDE TRACKING, because it is a
   value a parent transcribes character by character onto a tablet keyboard
   with the tablet in their other hand. Crockford base32 excludes I, L, O and U
   precisely so the remaining characters cannot be misread; a proportional
   13px body font gives that back. `user-select: all` makes one tap or one
   double-click select the whole code and nothing around it.
   ========================================================================= */
.card.setupcode { padding: 18px 20px; }
.card.setupcode h3 { margin: 0 0 10px; font-size: 15px; }
.card.setupcode p { margin: 0 0 8px; font-size: 13px; }
.card.setupcode .code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 22px; font-weight: 700; letter-spacing: .18em;
  margin: 0 0 12px; word-break: break-all; user-select: all;
}
.card.setupcode.sc-critical {
  background: rgba(255,194,75,.12); border-color: rgba(255,194,75,.45); color: #FFD98A;
}
/* The fineprint inside an amber card must stay legible on amber, so it takes
   the card's own colour rather than --haze, which is tuned for --dusk. */
.card.setupcode.sc-critical .fineprint { color: inherit; opacity: .85; }
:root:not([data-theme]) .card.setupcode.sc-critical,
:root[data-theme="light"] .card.setupcode.sc-critical {
  background: rgba(242,169,59,.15); border-color: rgba(242,169,59,.55); color: #7A4F00;
}

/* ==========================================================================
   DEEP PORTAL LAYOUT PASS — 2026-09-03

   This is intentionally one additive override layer. The earlier feature
   blocks remain useful documentation of the screens they introduced; these
   rules correct the shared geometry and hierarchy those independently-added
   blocks could not see. All colour comes from the existing theme tokens, and
   `.hide` / `.is-hidden` retain their !important authority.
   ========================================================================== */

/* A 250px rail plus the old 900px two-card breakpoint left roughly 594px for
   the page at the exact browser width used by the desktop tests. Do not split
   that already narrow region again. The high-level mounts opt into one column
   with the screen; genuinely wide windows regain their existing two-column
   layouts at 1400px. */
@media (min-width: 900px) {
  #main {
    width: 100%;
    max-width: 1580px;
    margin-inline: auto;
  }
}

@media (min-width: 900px) and (max-width: 1399.98px) {
  .screen,
  #overviewMount,
  #engageMount,
  #settingsMount,
  #childMount,
  .screen > .fc-ov {
    grid-template-columns: minmax(0, 1fr);
  }

  /* These summary rows still benefit from pairing, but four narrow statistic
     tiles do not. The identity tile needs the full first row. */
  #overviewMount > .ovtiles,
  #settingsMount > .ovtiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  #settingsMount > .ovtiles > .acctowner { grid-column: 1 / -1; }

  /* This disclosure is nested inside the child mount. Two columns here would
     recreate the same quarter-width-card problem one level down. */
  .child-settings-more-body { grid-template-columns: minmax(0, 1fr); }
}

/* Check-ins is four short, independent resting cards now; its long authoring
   forms live in bounded modals. At an ordinary 1280px desktop the 250px rail
   still leaves about 974px of content, enough for the intended tool/history
   pairs without the density that makes the other high-level screens wait
   until 1400px. Kept after the shared one-column rule so this narrow,
   screen-specific exception wins only where it is useful. */
@media (min-width: 1280px) {
  #engageMount { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Device inventory responds to the width it actually receives, not the
   viewport. A card is never asked to carry its identity, four health signals
   and actions in less than 430px unless the whole container is narrower. */
.devgroup > .devgroupbody {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 430px), 1fr));
  gap: 16px;
}

/* `devopen` is now the visible wording “Open device settings”, not a decorative
   chevron. Make that doorway read as a deliberate secondary action while the
   surrounding identity band remains the single real button. */
.devcard .devopen {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  flex: none;
  margin: 0;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--glow) 42%, var(--line));
  border-radius: 999px;
  background: color-mix(in srgb, var(--glow) 10%, var(--dusk));
  color: var(--glow);
  font-size: 11.5px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
}
/* The regular light-mode cyan misses AA at this small label size once its
   translucent pill background is composited. Keep the same hue but leave a
   real margin above 4.5:1 instead of depending on sub-pixel rounding. */
:root[data-theme="light"] .devcard .devopen { color: #075f80; }
.devcard .devopen::after { content: "›"; margin-left: 6px; font-size: 16px; line-height: .7; }

/* On a phone the named action gets its own line under the identity instead of
   stealing the device name's width. Explicit grid placement is visual only;
   the tested DOM order and the one-button accessibility contract do not move. */
@media (max-width: 560px) {
  .devcard .devbtn {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr);
    align-items: start;
    column-gap: 10px;
  }
  .devcard .devicon { grid-column: 1; grid-row: 1 / span 2; }
  .devcard .devnm { grid-column: 2; grid-row: 1; }
  .devcard .devopen { grid-column: 2; grid-row: 2; justify-self: start; margin-top: 9px; }
}

/* GoodQA ticket 482 — a per-child quick-lock button beside the existing
   `.navchild` destination button. A <button> may not contain another
   <button>, so `.navchild` and `.navchildlock` are SIBLINGS inside this
   wrapper rather than the lock icon living inside `.navchild` itself — the
   wrapper takes over the flex-item role `.navchild` used to hold directly
   inside #navChildren, at both the >=900px rail (align-items:stretch already
   gives it the full column width for free) and the <900px expanded menu
   (mirrored explicitly below, next to that breakpoint's own `.navchild`
   sizing rule). */
.navchildrow { display: flex; align-items: stretch; gap: 6px; width: 100%; }
.navchildrow .navchild { flex: 1 1 auto; min-width: 0; }
.navchildlock {
  flex: none;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  /* HER WEIGHT, OUR TOUCH TARGET — 2026-09-20. The owner's screenshot of the
     live rail (190333.png) shows this reading as a heavy grey square, much
     louder than anything else in the rail and than her own.
     MEASURED off her Home.dc.html aside: 24x24, border-radius 7px, background
     rgba(255,255,255,.12), NO border, and merely `opacity: .32` for the child
     who is not locked. Ours was 34x34 with a 1px --line border and an OPAQUE
     --dusk2 fill, which is what made it a block rather than a hint.
     Taking her fill, her radius and her borderlessness, but KEEPING 34px:
     hers is 24px, which is below every touch-target floor this product's own
     a11y suite enforces, and shrinking a control to match a drawing is not a
     trade worth making. The visual weight is hers; the hit area stays ours. */
  border-radius: 8px;
  border: 1px solid transparent;
  background: rgba(255,255,255,.12);
  color: var(--cloud);
}
.navchildlock:hover:not(:disabled) {
  border-color: var(--coral);
  color: var(--coral);
}
.navchildlock:disabled { opacity: .35; cursor: not-allowed; }
.navchildlockicon { width: 16px; height: 16px; }

/* ------------------------------------------------------------------ rail IA
   Children are people and contexts; the destinations after them operate on
   the household. Initials and live device metadata come from renderChildNav's
   existing data attributes, so no CSS-generated claim can drift from the row. */
#navRow .navchild::before {
  content: attr(data-child-initial);
  grid-column: 1;
  grid-row: 1 / span 2;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin: 0;
  border-radius: 50%;
  background: var(--sky);
  color: var(--dusk);
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 850;
  line-height: 1;
}
/* ONE LINE PER CHILD, as hers is. NOTHING IS LOST.
   Her child rows are 22px: avatar, name, a small lock. Ours were 44px
   two-line chips carrying "1 device · online" underneath, which is most of
   what the owner read as "crowded ... not just number of items, but spacing".
   The meta text is NOT deleted -- app.js already puts the identical string in
   the row's aria-label ("Maya, 1 device · online"), so a screen reader is
   unaffected, and the live DOT stays as the visual signal it always was.
   Kept as a rule rather than removed so the second line can be restored by
   deleting one declaration if the owner wants it back. */
#navRow .navchild::after {
  content: none;
}
#navRow .navchild .navdot {
  grid-column: 3;
  grid-row: 1 / span 2;
  align-self: center;
  margin: 0;
}

@media (min-width: 900px) {
  /* At rail widths these become real boxes rather than display:contents. That
     preserves their labelled group semantics in the accessibility tree and
     gives each family/account cluster one coherent vertical rhythm. */
  /* gap 2px -> 0 (redesign area "checkins", 2026-09-17): #navHousehold now
     stacks an eleventh real row (`engage`) and this gap, doubled by the ten
     seams between them, was the single largest recoverable amount in the
     rail — `.navbtn`'s own 10px vertical padding still separates adjacent
     rows visually, same reasoning as `#navRow`'s own gap trim above. */
  #navChildren,
  #navHousehold {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  #navChildrenLabel,
  #navHouseholdLabel {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-inline: -4px;
    padding: 8px 10px 4px;
  }
  #navChildrenLabel { margin-top: 4px; color: var(--attention-text); }
  #navHouseholdLabel { margin-top: 6px; }
  :root[data-theme="light"] #navRow #navChildrenLabel,
  :root[data-theme="light"] #navRow #navHouseholdLabel,
  :root[data-theme="light"] #navRow .navchild::after { color: var(--railtext); }
  #navChildrenLabel::after,
  #navHouseholdLabel::after { content: none; }

  #navRow .navchild {
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    column-gap: 10px;
    row-gap: 2px;
    min-height: 44px;
    padding: 4px 10px;
    overflow: hidden;
    border-color: transparent;
    background: transparent;
    color: var(--cloud);
    font-size: 13px;
    font-weight: 750;
    line-height: 1.15;
    white-space: nowrap;
  }
  #navRow .navchild:hover {
    background: color-mix(in srgb, var(--sky) 13%, var(--dusk2));
    border-color: color-mix(in srgb, var(--sky) 42%, var(--line));
  }
  #navRow .navchild[aria-current="page"]::before {
    background: var(--dusk);
    color: var(--glow);
  }
  #navRow .navchild[aria-current="page"]::after { color: currentColor; opacity: .78; }
}

/* The complete mobile sheet gives each child a full-width identity row while
   household destinations remain its compact two-up controls. The fixed bottom
   bar is untouched because #navChildren is not displayed there. */
@media (max-width: 899px) {
  /* The expanded mobile menu restores real group boxes. This keeps Children
     and Household as labelled accessibility groups while allowing the former
     to use full-width identity rows and the latter to retain its two-up grid. */
  body.signed-in #nav.mobile-open #navRow #navChildren,
  body.signed-in #nav.mobile-open #navRow #navHousehold {
    display: flex;
    flex: 0 0 100%;
    flex-wrap: wrap;
    min-width: 0;
    gap: 6px;
  }

  body.signed-in #nav.mobile-open #navChildrenLabel,
  body.signed-in #nav.mobile-open #navHouseholdLabel {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 8px;
    border-top: 1px solid var(--line);
    padding-top: 13px;
  }
  body.signed-in #nav.mobile-open #navChildrenLabel { color: var(--attention-text); }
  /* GoodQA ticket 482 — .navchildrow, not .navchild, is now the flex item
     #navChildren wraps a full-width row around; see this rule's own comment
     a few lines further down for why .navchild's old flex:1 1 100% moves up
     one level. */
  body.signed-in #nav.mobile-open #navRow .navchildrow { flex: 1 1 100%; min-width: 100%; }
  body.signed-in #nav.mobile-open #navRow .navchild {
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    flex: 1 1 auto;
    min-width: 0;
    column-gap: 10px;
    row-gap: 2px;
    overflow: hidden;
    border-color: color-mix(in srgb, var(--sky) 28%, var(--line));
    background: color-mix(in srgb, var(--sky) 8%, var(--dusk));
    color: var(--cloud);
    font-size: 13px;
    font-weight: 750;
    line-height: 1.15;
    white-space: nowrap;
  }
  body.signed-in #nav.mobile-open #navRow .navchild[aria-current="page"]::after {
    color: currentColor;
    opacity: .78;
  }
}

/* ---------------------------------------------------------- child workspace
   One clearly bounded child identity band sits above every child-scoped pane.
   The dedicated child tabs are a single non-wrapping strip: at narrow widths
   they scroll instead of turning into an ambiguous two- or three-row chip pile. */
.screen > .fc-head {
  width: 100%;
  min-width: 0;
  margin: 0 0 10px;
  padding: 18px 20px;
  border: 1px solid color-mix(in srgb, var(--sky) 38%, var(--line));
  border-left: 4px solid var(--sky);
  border-radius: 18px;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--sky) 12%, var(--dusk)),
    var(--dusk));
}
.screen > .fc-head .ov-eyebrow { color: var(--attention-text); }
.screen > .fc-head .ov-greet {
  margin: 5px 0 3px;
  font-size: clamp(24px, 2.2vw, 32px);
  line-height: 1.08;
}
.screen > .fc-head .ov-sub { max-width: 72ch; line-height: 1.45; }

.screen > .fc-chips {
  width: 100%;
  min-width: 0;
  margin: 0 0 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk);
}
.fc-chiplabel {
  flex: 0 0 100%;
  color: var(--haze);
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: .1em;
  line-height: 1.2;
  text-transform: uppercase;
}

.screen > .subnav {
  display: flex;
  flex-wrap: nowrap;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  gap: 5px;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  margin: 0 0 18px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk);
}
/* ONE PILL TRACK, not six bordered boxes.
   Hers is a single light rail carrying plain-text tabs, with the current one
   a filled brand pill. Ours gave every tab its own 1px border and 10px radius
   and stretched them edge to edge, so six equally-weighted boxes competed and
   nothing read as selected except a faint glow.
   `flex: 0 0 auto` rather than `1 0 auto`: hers are sized by their own labels
   and left-aligned in the rail; stretching them is what made them read as
   separate buttons. The horizontal scroll and snap behaviour below 900px is
   untouched -- that is what scroll-snap-align is for and a narrow phone still
   needs it. */
.screen > .subnav .subnavbtn {
  flex: 0 0 auto;
  min-height: 42px;
  border-radius: var(--radius-button);
  padding: 9px 16px;
  white-space: nowrap;
  scroll-snap-align: start;
  border-color: transparent;
  background: transparent;
  color: var(--text-secondary);
}
.screen > .subnav .subnavbtn:hover {
  background: var(--surface-raised);
}
.screen > .subnav .subnavbtn[aria-current="page"] {
  /* --glow / --dusk, NOT --brand / #FFFFFF.
     In LIGHT those resolve to exactly her look -- brand blue #2B6CB0 with
     white ink -- and in DARK they are the pair app.css has already proven for
     this job: `.chip[aria-pressed="true"]` uses them and its own comment
     records why ("--glow is bright cyan in dark and a dark AA-safe cyan in
     light; --dusk flips with it, keeping selected 13px tabs above 4.5:1 in
     both palettes; fixed --ink failed on light #087EA4").
     My first attempt hardcoded #FFFFFF on --brand, which is her light pairing
     applied to both themes, and zip-parity-child-cluster caught it as a DARK
     contrast failure on the Apps and Processes tabs. Reusing the proven pair
     is better than re-deriving a second one. */
  background: var(--glow);
  border-color: transparent;
  color: var(--dusk);
  /* No glow shadow. The fill IS the selected state in her design; the old
     18%-alpha --glow shadow was carrying that job alone on a bordered box. */
  box-shadow: none;
}

/* Keep the historical Overview heading available to route focus and assistive
   technology, but do not show a second title below the child identity band. */
#child:has(> .fc-head) > .toprow {
  display: flex;
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 560px) {
  .screen > .fc-head { padding: 14px 15px; border-radius: 16px; }
  .screen > .fc-head .ov-greet { font-size: 23px; }
  .screen > .subnav { gap: 4px; padding: 4px; }
  .screen > .subnav .subnavbtn { flex-grow: 0; font-size: 12px; padding-inline: 12px; }
  /* DEF-004. Six destinations in a nowrap/overflow-x:auto strip clip the
     last one or two on a phone with no visible cue. Wrap to two rows instead
     of scrolling so every destination stays discoverable. */
  .screen > .subnav { flex-wrap: wrap; overflow-x: visible; row-gap: 4px; }
  .screen > .subnav .subnavbtn { flex: 0 1 auto; }

  /* Once the child and selected task are both visible, repeating "Family
     controls", an online ratio and the scope sentence on every task page is
     costly chrome. Keep the child's name in its blue-bounded identity band;
     the device disclosure directly below still owns live device status. */
  #fc-head-processes,
  #fc-head-sites,
  #fc-head-categories,
  #fc-head-schedules {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    min-height: 56px;
    padding: 9px 12px;
  }
  #fc-head-processes > .child-headtop,
  #fc-head-sites > .child-headtop,
  #fc-head-categories > .child-headtop,
  #fc-head-schedules > .child-headtop { display: contents; }
  #fc-head-processes .ov-eyebrow,
  #fc-head-processes .ov-sub,
  #fc-head-processes .child-headtop > .pill,
  #fc-head-sites .ov-eyebrow,
  #fc-head-sites .ov-sub,
  #fc-head-sites .child-headtop > .pill,
  #fc-head-categories .ov-eyebrow,
  #fc-head-categories .ov-sub,
  #fc-head-categories .child-headtop > .pill,
  #fc-head-schedules .ov-eyebrow,
  #fc-head-schedules .ov-sub,
  #fc-head-schedules .child-headtop > .pill { display: none; }
  #fc-head-processes .ov-greet,
  #fc-head-sites .ov-greet,
  #fc-head-categories .ov-greet,
  #fc-head-schedules .ov-greet {
    margin: 0;
    font-size: 20px;
    line-height: 1.1;
  }

  /* Processes already sits inside the child frame above. Keep its historical
     title as the real focus target for route announcements, but take the
     duplicate 95px heading out of the phone's visual flow. */
  #processes:has(> .fc-head) > .ovhead:not(.fc-head) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* THE ONE BROKEN PATH TO SCHEDULE/TIME-LIMITS AT 320x568.
   #fc-head-child (Overview's own identity card) is the one fc-head variant
   the block above deliberately keeps full-height: it kept the eyebrow AND
   the scope sentence because Overview has no "selected task" of its own to
   supply that context the way Apps/Processes/Web/Time's own active tab
   already does (see #fc-head-processes &c. above, and the eyebrow's own
   MEASURED comment further up this file). At 320px width that full card,
   plus the two lines the scope sentence wraps to at this width, is tall
   enough that #childSubnav — the ONLY path to every one of those five
   tabs, including Time — renders with as little as single-digit pixels of
   clearance above the fixed bottom dock (#nav, 65px tall at this
   breakpoint): document.elementFromPoint() at a tab's own rendered center
   can return <nav id="nav"> instead of the tab, so a tap does nothing.
   Confirmed absent at 375x667 and 768x1024, where the extra height leaves
   comfortable clearance regardless of this rule — this is deliberately
   scoped to the exact width band that has none.
   The scope sentence is the one line this screen can lose without losing
   identity: the eyebrow ("Family controls") plus the child's own name in
   the heading still say whose controls these are, exactly the pairing the
   eyebrow's own comment protects. Losing a decorative restatement of that
   at 320px buys back the two wrapped lines it costs, which is enough on
   its own to clear the dock with real margin rather than a handful of px.

   The extra sticky enrolment offer still uses vertical space on a fresh
   account. Reclaiming decorative slack here — id-scoped, so it cannot
   touch Apps/Web/Categories/Time's own #fc-head-* padding or any shared
   `.fc-head`/`.fc-chips` rule other per-child screens rely on — restores a
   real margin above the fixed bottom dock. */
@media (max-width: 340px) {
  #fc-head-child .ov-sub { display: none; }
  .screen > #fc-head-child { padding: 8px 14px; margin-bottom: 6px; }
  #fc-chips-child { margin-bottom: 4px; }
}

/* Dark-on-dark in the appended disclosure made its 15px title effectively
   disappear (var(--ink) on var(--dusk2)). Primary themed text is the intended
   hierarchy in both palettes. */
.child-settings-more > summary { color: var(--cloud); }
.child-settings-more > summary strong { color: var(--cloud); }

/* --------------------------------------------------------- device workspace
   `selectPolicyView()` writes the active value on #policy. Only cards assigned
   to that workspace view participate; pre-existing `.hide` and `.is-hidden`
   still win because both remain !important earlier in this file. */
#policy[data-policy-view] > [data-policy-section],
#policy[data-policy-view] > #policyMount > [data-policy-section] { display: none; }
#policy[data-policy-view="overview"] > [data-policy-section="overview"],
#policy[data-policy-view="apps"] > [data-policy-section="apps"],
#policy[data-policy-view="time"] > [data-policy-section="time"],
#policy[data-policy-view="protection"] > [data-policy-section="protection"],
#policy[data-policy-view="tools"] > [data-policy-section="tools"],
#policy[data-policy-view="more"] > [data-policy-section="more"],
#policy[data-policy-view="overview"] > #policyMount > [data-policy-section="overview"],
#policy[data-policy-view="apps"] > #policyMount > [data-policy-section="apps"],
#policy[data-policy-view="time"] > #policyMount > [data-policy-section="time"],
#policy[data-policy-view="protection"] > #policyMount > [data-policy-section="protection"],
#policy[data-policy-view="tools"] > #policyMount > [data-policy-section="tools"],
#policy[data-policy-view="more"] > #policyMount > [data-policy-section="more"] { display: block; }
#policy [data-policy-section] { scroll-margin-top: 76px; }

#policy > .policy-head,
#policy > .statuscard,
#policy > .policy-tabs,
#policy > .banner,
#policy > #polMsg,
#policy > #policyMount {
  grid-column: 1 / -1;
  width: 100%;
  min-width: 0;
}

#policy > .policy-head {
  align-items: flex-start;
  margin: 0;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--glow) 9%, var(--dusk)),
    var(--dusk));
}
.policy-eyebrow {
  margin-bottom: 4px;
  color: var(--glow);
  font-size: 10.5px;
  font-weight: 850;
  letter-spacing: .12em;
  line-height: 1.2;
  text-transform: uppercase;
}
#policy .policy-head .nm {
  color: var(--cloud);
  font-size: clamp(24px, 2.2vw, 31px);
  font-weight: 820;
  letter-spacing: -.025em;
  line-height: 1.1;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.policy-context {
  margin-top: 6px;
  color: var(--haze);
  font-size: 13px;
  font-weight: 650;
  overflow-wrap: anywhere;
}
#policy .policy-device-id { display: none; }
.policy-back {
  width: auto;
  display: inline-flex;
  align-items: center;
  margin: 0;
  align-self: center;
  white-space: nowrap;
}
.policy-back::before { content: "←"; margin-right: 7px; }
#policy > .policy-head > #devRenameBtn { align-self: center; }

.policy-tabs {
  display: flex;
  position: sticky;
  top: 8px;
  z-index: 12;
  flex-wrap: nowrap;
  gap: 5px;
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: color-mix(in srgb, var(--dusk) 94%, transparent);
  backdrop-filter: blur(14px);
  box-shadow: 0 10px 28px color-mix(in srgb, var(--ink) 24%, transparent);
}
.policy-tab {
  flex: 1 0 auto;
  min-width: max-content;
  min-height: 42px;
  padding: 9px 14px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--haze);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  scroll-snap-align: start;
}
.policy-tab:hover { background: var(--dusk2); color: var(--cloud); }
.policy-tab[aria-pressed="true"] {
  border-color: var(--glow);
  background: var(--glow);
  color: var(--dusk);
  box-shadow: 0 7px 18px color-mix(in srgb, var(--glow) 18%, transparent);
}

details.policy-technical { padding: 0; overflow: hidden; }
.policy-technical > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 17px 20px;
  color: var(--cloud);
  cursor: pointer;
  font-size: 16px;
  font-weight: 800;
  list-style: none;
}
.policy-technical > summary::-webkit-details-marker { display: none; }
.policy-technical > summary::before { content: "▸"; color: var(--attention-text); }
.policy-technical[open] > summary { border-bottom: 1px solid var(--line); }
.policy-technical[open] > summary::before { content: "▾"; }
.policy-technical > .help { margin: 16px 20px 10px; }
.policy-tech-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 12px;
  margin: 0 20px 20px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--dusk2);
  color: var(--haze);
  font-size: 12px;
  font-weight: 700;
}
.policy-tech-row code {
  min-width: 0;
  color: var(--cloud);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 600;
  overflow-wrap: anywhere;
  user-select: all;
}

@media (max-width: 560px) {
  #policy > .policy-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
    padding: 15px;
  }
  #policy .policy-back { order: -1; justify-self: start; align-self: auto; }
  .policy-tab { flex-grow: 0; font-size: 12px; padding-inline: 12px; }
  .policy-tech-row { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------- typography
   A card title must be visibly stronger than its helper copy. The former 12px
   muted `.sect` looked like another annotation and erased page hierarchy. */
.sect {
  color: var(--cloud);
  /* HER SPEC, measured: Nunito 16.5px/800 on a 24.75px line box, no tracking.
     h1-h6 inherit --font-heading (Quicksand) from the global rule, and `.sect`
     is usually an <h2>, so a card title was rendering in Quicksand with -.01em
     tracking. She uses Quicksand for h1, the date eyebrow, rail group labels
     and BUTTONS, and Nunito for body copy, nav entries and these card titles —
     the opposite assignment to what this rule produced. */
  font-family: var(--font-body);
  font-size: 16.5px;
  font-weight: 800;
  letter-spacing: normal;
  line-height: 1.5;
  margin-bottom: 14px;
}
.sect::after { min-width: 24px; opacity: .82; }

/* ------------------------------------------------------ very narrow header
   ParentProof + Menu + theme + language exceeded the available inline size at
   360–420px. Keep every control and its accessible name, but spend less chrome
   around them; the visible Menu word becomes the familiar menu glyph only. */
@media (max-width: 420px) {
  body {
    padding-left: max(10px, env(safe-area-inset-left));
    padding-right: max(10px, env(safe-area-inset-right));
  }
  .pagehead { gap: 5px; padding: 6px; margin-bottom: 12px; border-radius: 15px; }
  .brand { min-width: 0; gap: 5px; }
  .brandmark { width: 28px; height: 28px; }
  .site-brand-logo { width: 120px; height: 30px; }
  .wm { font-size: 16px; letter-spacing: -.4px; white-space: nowrap; }
  .wm small { display: none; }
  .controls { min-width: 0; flex: none; gap: 3px; }
  body.signed-in .mobilemenubtn {
    width: 38px;
    min-width: 38px;
    min-height: 40px;
    padding: 0;
    font-size: 0;
  }
  body.signed-in .mobilemenubtn::before {
    content: "☰";
    font-size: 17px;
    line-height: 1;
  }
  .themebtn { width: 42px; height: 28px; }
  .themebtn::after { top: 4px; left: 4px; width: 18px; height: 18px; }
  .themebtn[aria-checked="false"]::after { left: 19px; }
  .themebtn .ic.sun { left: 5px; }
  .themebtn .ic.moon { right: 5px; }
  .pagehead .langsel { width: 64px; max-width: 64px; min-height: 40px; padding: 5px; font-size: 12px; }
  body.signed-in #nav.mobile-open .navfoot .langsel { width: 100%; max-width: none; }
}

/* ========================================================================
   PRODUCT SCREEN SPEC · ITERATION 1
   Clear family/household scope, a four-destination phone dock, and direct
   status navigation. These declarations intentionally sit last while the
   older release notes above remain useful history. */

/* The persistent desktop rail already names every child. Kids is the compact
   phone destination and therefore redundant in the wide rail. */
@media (min-width: 900px) {
  #navRow [data-screen="kids"] { display: none; }
  /* These module mounts are direct children of the page's two-track grid.
     Span that parent first, then let their own content use the full canvas. */
  #kidsMount,
  #householdProcessesMount,
  #preferencesMount { grid-column: 1 / -1; min-width: 0; }

  #householdProcessesMount,
  #preferencesMount {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
    align-content: start;
  }
  #householdProcessesMount > .ovhead,
  #householdProcessesMount > .ovtiles,
  #householdProcessesMount > .banner,
  #householdProcessesMount > .chips,
  #preferencesMount > .ovhead { grid-column: 1 / -1; }
  #householdProcessesMount > .card,
  #preferencesMount > .card { margin: 0; }
}

@media (min-width: 1400px) {
  #householdProcessesMount,
  #preferencesMount {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.kids-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 16px;
}
.kid-directory-card {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
}
.kid-directory-card .ovc-head {
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid var(--line);
  /* --surface-raised, not --dusk2: in light those two were both #FFFFFF, so
     this band and the card under it were the same flat white and the identity
     row stopped reading as its own region. zip-parity-child-cluster.test.js
     catches exactly that (identitySeparated), and it only started failing when
     light became the default -- the defect was always in the light palette,
     nothing was looking at it.
     The border below is still the primary separator, which is how the handoff
     does it; this only makes the band legible as a band at 375px, where a
     1.38:1 hairline is thin evidence on its own. */
  background: var(--surface-raised);
}
.kid-directory-card .avatar.lg {
  box-shadow: 0 0 0 3px var(--dusk), 0 0 0 4px var(--line);
}
.kid-directory-card .ovc-who { min-width: 0; flex: 1; }
.kid-directory-card .ovc-name {
  margin: 0;
  color: var(--cloud);
  font-size: 20px;
  overflow-wrap: anywhere;
}
.kids-card-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  padding: 14px 16px 16px;
}
.kids-card-action {
  width: 100%;
  min-width: 0;
  min-height: 44px;
  margin: 0;
  justify-content: center;
  text-align: center;
}
.hproc-child-title { min-width: 0; overflow-wrap: anywhere; }

@media (max-width: 560px) {
  .kids-grid { gap: 12px; }
  .kid-directory-card .ovc-head { padding: 14px; }
  .kids-card-actions {
    grid-template-columns: minmax(0, 1fr);
    padding: 12px 14px 14px;
  }
  .kids-card-action { min-height: 44px; }
}

@media (max-width: 899px) {
  /* Kids is the one current page in the phone dock. The selected child in the
     expanded sheet still gets a clear visual treatment without a second
     aria-current="page" announcement. */
  body.signed-in #nav.mobile-open #navRow .navchild[data-child-selected="true"] {
    border-color: var(--glow);
    background: color-mix(in srgb, var(--glow) 18%, var(--dusk));
  }
  body.signed-in #nav.mobile-open #navRow .navchild[data-child-selected="true"]::before {
    background: var(--dusk);
    color: var(--glow);
  }
}

/* A one-time policy-source migration is important, but it is not a third
   Overview dashboard. The two primary cards remain the selected device and
   profile policy; this is an actionable attention strip beneath them. */
.child-delivery-compact {
  margin: 0 0 14px;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--amber) 48%, var(--line));
  border-radius: 14px;
  background: color-mix(in srgb, var(--amber) 8%, var(--dusk));
}
.child-delivery-compact .cardhead { margin-bottom: 8px; }
.child-delivery-compact .sect { margin: 0; font-size: 14px; }
.child-delivery-compact .banner { margin: 8px 0 10px; padding: 0; border: 0; background: transparent; }
.child-delivery-compact .help,
.child-delivery-compact .fineprint { max-width: 74ch; }
.child-settings-more-body { grid-template-columns: minmax(0, 1fr); }

/* Profile-backed Time controls share #schedulesCard's one outer frame. Keep
   each editor as a plain section and use one divider between them; repeating
   the global card border, background and shadow here creates a card-in-card. */
.child-time-section { min-width: 0; }
.child-time-section + .child-time-section {
  border-top: 1px solid var(--line);
  margin-top: 20px;
  padding-top: 20px;
}

/* Each signal is now a real button with its own destination. The surrounding
   group supplies the rolled-up border; cells retain independent focus and
   never rely on hover for their meaning. */
.devcard .healthcap { cursor: default; }
.devcard .hccell {
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: 6px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.devcard .hccell:hover { border-color: var(--line); background: var(--dusk2); }
.devcard .hccell:focus-visible { outline: 2px solid var(--glow); outline-offset: 2px; }

/* Two-step enrollment remains visible but participates in normal document
   flow on phones. It can no longer cover Save, navigation, or the final card. */
@media (max-width: 899px) {
  html.enrolpinned { scroll-padding-bottom: calc(var(--phonedock, 0px) + 24px); }
  body.signed-in.enrolpinned #main { padding-bottom: 18px; }
  #enrolBanner {
    position: relative;
    inset: auto;
    z-index: auto;
    margin: 0 0 14px;
    padding: 11px 12px;
    box-shadow: none;
  }
  #enrolBannerBody { display: block; }
}

/* ========================================================================
   LIGHT THEME LIVE POLISH · 2026-09-04

   The light palette now lives in the single foundation block at the top of
   this file. These component rules remain scoped to the explicit light theme;
   the dark palette and its state colours remain unchanged. */

/* A defined edge plus a short ambient shadow separates a card from the page
   without turning nested rows into more cards. */
:root[data-theme="light"] .card {
  background: #FFFFFF;
  border-color: var(--line);
  box-shadow: var(--shadow);
}
:root[data-theme="light"] .statuscard,
:root[data-theme="light"] .ovtile,
:root[data-theme="light"] #policy > .policy-head {
  border-color: var(--line);
  box-shadow: 0 8px 24px rgba(41,68,93,.09);
}

/* Opacity blends a disabled control into its own pale surface. Solid quiet
   colours preserve the inactive cue while keeping labels and boundaries
   readable, including Safari's separately-painted disabled input text. */
:root[data-theme="light"] .inp:disabled,
:root[data-theme="light"] .limitin:disabled,
:root[data-theme="light"] .timein:disabled,
:root[data-theme="light"] .langsel:disabled {
  opacity: 1;
  color: #425A73;
  -webkit-text-fill-color: #425A73;
  background: #E0E8F0;
  border-color: #6B849D;
  cursor: not-allowed;
}
:root[data-theme="light"] .ghost:disabled,
:root[data-theme="light"] .primary:disabled,
:root[data-theme="light"] .chip:disabled,
:root[data-theme="light"] .face:disabled,
:root[data-theme="light"] .pickrow:disabled,
:root[data-theme="light"] .navfootbtn:disabled {
  opacity: 1;
  color: #425A73;
  background: #E0E8F0;
  border: 1px solid #6B849D;
  box-shadow: none;
  cursor: not-allowed;
}
:root[data-theme="light"] .toggle:disabled,
:root[data-theme="light"] .sw:disabled {
  opacity: .72;
  cursor: not-allowed;
}

/* Requests is the only rail label with a live trailing value. Give that value
   its own column at desktop width instead of joining the label text.

   NO `column-gap` HERE — DF-25, fixed 2026-09-20. It was `column-gap: 10px`,
   and column-gap applies between EVERY pair of flex items, not just the two
   you had in mind. This button's items are icon | label | badge, so the 10px
   also landed between the ICON and the LABEL:

     every other rail row   icon ends x=47, label starts x=55  (the icon's own
                            8px margin-right, and nothing else)
     Requests               icon ends x=47, label starts x=65  (8px + 10px)

   That is the one row sitting 10px right of all the others, which the owner
   could see and a token-level check could not — measured identical on both
   rows for children, svg width, every margin and padding, flex-direction,
   justify-content, pseudo-element content and badge presence. `column-gap`
   was the only property that differed, and only because this rule set it.

   WORSE THAN A STATIC OFFSET, AND THE REASON THIS IS NOT JUST SCOPED TO
   `:has(.pill.wait)`: the gap applies whenever the rule matches, so the label
   would sit at x=65 while a request is waiting and x=55 once the parent clears
   it. A nav label that moves when a count appears is a worse defect than one
   that is consistently misaligned.

   The separation the rule actually wanted is already provided by the badge's
   own `margin-left: auto` below, which pushes it to the far edge regardless. */
@media (min-width: 900px) {
  #navRow [data-screen="requests"] {
    display: flex;
    align-items: center;
  }
  #navRow [data-screen="requests"] > .pill.wait { margin-left: auto; }
}

/* The bottom dock hides Requests, but the expanded phone menu restores it.
   Reset the dock's absolute corner badge there so "Requests" and "1 waiting"
   become two aligned flex items with real breathing room. */
@media (max-width: 899px) {
  body.signed-in #nav.mobile-open #navRow [data-screen="requests"] {
    align-items: center;
    gap: 10px;
  }
  body.signed-in #nav.mobile-open #navRow [data-screen="requests"] > .pill.wait {
    position: static;
    flex: 0 0 auto;
    min-width: 0;
    margin-left: auto;
    padding: 4px 8px;
    line-height: 1.2;
  }
}

:root[data-theme="light"] #navRow [data-screen="requests"] > .pill.wait {
  background: #F9E8B7;
  border: 1px solid #C99A37;
  color: #604000;
}

/* ========================================================================
   DEVICES + DEVICE WORKSPACE LAYOUT · 2026-09-04

   Geometry only. State badges and palette overrides are owned by the light-
   theme polish block above; this layer keeps sparse device sets and uneven
   device-task cards from inheriting stretched grid shapes. */

/* Route arrival focuses a non-interactive title for assistive technology.
   Chromium treats that scripted focus as `:focus-visible`, even after a real
   pointer click, so a focus-visible override still painted a large rectangle
   around the whole heading. These nodes cannot be tabbed to or activated;
   suppress only their programmatic outline and leave every control's keyboard
   focus treatment untouched. */
.screen [tabindex="-1"]:focus { outline: none; }

/* `auto-fill` keeps empty tracks when only one or two devices exist, so their
   cards retain the same useful reading width instead of inflating to fill the
   entire inventory row. Phones still resolve to one 100%-wide track. */
#devices .devgroup > .devgroupbody {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 390px), 1fr));
}
#devices .devgroup > .devgroupbody > .devcard:only-child {
  grid-column: 1 / -1;
  width: min(100%, 620px);
  justify-self: center;
}

/* The roll-up badge and four-cell health control already carry every fact in
   this line. Keep the underlying computation intact for compatibility, but do
   not paint the same protection result a third time on a compact card. */
#devices .devcard .devstatus { display: none; }

/* Inventory hierarchy: identity first, then optional troubleshooting facts,
   four real health destinations, and one unambiguous action row. */
#devices .devcard .devcontext {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-top: 5px;
  font-size: 12px;
  line-height: 1.3;
}
#devices .devcard .devchild { color: var(--cloud); font-weight: 750; }
#devices .devcard .devconnection { color: var(--haze); font-weight: 700; }
#devices .devcard .devchild + .devconnection::before {
  content: "·";
  margin-right: 8px;
  color: var(--haze);
}
#devices .devcard .devconnection.tone-on { color: var(--done-text); }
#devices .devcard .devconnection.tone-wait { color: var(--waiting-text); }
:root[data-theme="light"] #devices .devcard .devconnection.tone-on { color: #0A6242; }
/* tone-wait needs NO light override any more: light `--amber` is #7A4F00, which
   is exactly what this rule used to paste here. Removed 2026-09-06 and
   re-measured rather than assumed — 7.13:1 on a card, 6.39:1 on an `.arow`,
   6.25:1 on the page, the same numbers the literal gave. */

#devices .devcard .devalerts { display: grid; gap: 7px; }
#devices .devcard .devalerts .banner { margin: 0; }
/* The one non-banner child this grid takes: the second half of the
   "not protecting yet" sentence, in the card's own quiet voice rather than in a
   second banner. `.fineprint` carries a 10px top margin for the stacked
   contexts it was written for; inside a grid with its own 7px gap that reads as
   a dropped line, so the margin is zeroed here exactly as the banner's is. */
#devices .devcard .devalerts .fineprint { margin: 0; }

#devices .devcard .devfacts {
  border-block: 1px solid var(--line);
  color: var(--haze);
}
#devices .devcard .devfacts-summary {
  min-height: 42px;
  padding: 11px 2px;
  cursor: pointer;
  color: var(--cloud);
  font-size: 12px;
  font-weight: 750;
}
#devices .devcard .devfacts-body {
  display: grid;
  gap: 5px;
  padding: 0 2px 11px 18px;
}
#devices .devcard .devfact {
  font-size: 11.5px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
/* OWNER, 2026-09-19: these `.banner`s used to be a full-width boxed row on
   the card itself — moved into this same disclosure buildDeviceRow's own
   comment explains (a rewrite of the exact spot the "TIGHT DEVICE ROWS"
   >=900px section already flattened `.devalerts > .banner` for). Flattened
   here too, for the same reason: `.devfact.warnnote` two rules up already
   carries a real warning (an app-version-behind notice) as plain coloured
   text with no box, right in this same list — a boxed callout beside plain
   list lines would read as two different vocabularies for one kind of fact. */
#devices .devcard .devfacts-body .banner,
#devices .devcard .devfacts-body .fineprint {
  /* `.devfacts-body`'s own grid is `repeat(auto-fit, minmax(min(100%,210px),
     1fr))` — right for short facts like "Battery: 85%", wrong for a full
     sentence, which landed squeezed into a 210px column beside its
     neighbours (measured: three side-by-side slivers instead of three
     readable lines) until this. A sentence gets the whole row. */
  grid-column: 1 / -1;
}
#devices .devcard .devfacts-body .banner {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 11.5px;
  line-height: 1.35;
}
#devices .devcard .devfacts-body .banner.warn { color: var(--waiting-text); }
#devices .devcard .devfacts-body .banner.info { color: var(--attention-text); }
#devices .devcard .devfacts-body .fineprint { margin: 0; font-size: 11px; }

#devices .devcard .devactions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  align-items: stretch;
}
#devices .devcard .devactions > button {
  width: 100%;
  min-width: 0;
  margin: 0;
}
#devices .devcard .devactions > .devmanagebtn:only-child { grid-column: 1 / -1; }
#devices .devcard .devmanagebtn {
  min-height: 48px;
  padding: 9px 10px;
  border: 1px solid var(--glow);
  background: var(--glow);
  color: var(--ink);
  font-size: 13px;
  font-weight: 800;
}
:root[data-theme="light"] #devices .devcard .devmanagebtn { color: #FFFFFF; }
#devices .devcard .devlockstate {
  grid-column: 1 / -1;
  min-height: 0;
  text-align: left;
}
#devices .devcard .devforget {
  grid-column: 1 / -1;
  width: auto;
  justify-self: start;
  border: 0;
  background: transparent;
}

/* Acquisition remains one household-level decision. The short label keeps
   Add device and Pause all side by side at the measured phone width. */
#devices #addAnotherDeviceLink,
#devices #devTopActions > #pauseAllBtn { display: inline-flex; align-items: center; justify-content: center; }
@media (max-width: 560px) {
  #devices #devTopActions > #addAnotherDeviceLink,
  #devices #devTopActions > #pauseAllBtn {
    flex: 1 1 0;
    min-width: 0;
    padding-inline: 10px;
  }
}

/* Grid rows take the tallest card's height by definition. Individual device
   task cards should keep their content height rather than turning a short name
   or resting-message card into a large empty panel beside the profile card. */
#policy > [data-policy-section] { align-self: start; }

/* An odd final action is a deliberate closing row, not an orphaned half-card.
   Technical details uses the same treatment and acts as the More footer. */
#policy > .policy-card-wide { grid-column: 1 / -1; }
.policy-technical > .policy-revision:not(:empty) {
  margin: 0 20px 18px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

/* On a phone, Back and Rename are peer actions for the named tablet. Keeping
   them on one row saves a full control-height without shrinking either target;
   the identity keeps the complete row beneath them. */
@media (max-width: 560px) {
  #policy > .policy-head {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px;
  }
  #policy > .policy-head > .who { grid-column: 1 / -1; grid-row: 2; }
  #policy > .policy-head > .policy-back {
    grid-column: 1;
    grid-row: 1;
    justify-self: start;
  }
  #policy > .policy-head > #devRenameBtn {
    grid-column: 2;
    grid-row: 1;
    width: auto;
    margin: 0;
  }

  /* Devices is the only phone screen that must retain both account exit and
     household pause. They fit as two labelled buttons on one line; stacking
     them made the first device start another full row lower. */
  #devices .ovhead > #signoutBtn { align-self: stretch; }
  #devices .ovhead > #devTopActions {
    flex: 1 1 210px;
    width: auto;
    margin-left: 0;
  }
  #devices #devTopActions > #pauseAllBtn { min-width: 0; }
}

/* ========================================================================
   DARK BADGE CONTRAST · 2026-09-04

   The Requests count normally sits on the navy rail, where amber on its
   translucent amber wash is clear. On the selected route, however, its parent
   becomes solid cyan; the wash then composites to another bright colour and
   the amber label loses its edge. Give only that selected state an ink capsule
   so the semantic amber remains readable without changing inactive Requests. */
:root:not([data-theme="light"]) #navRow [data-screen="requests"][aria-current="page"] > .pill.wait {
  background: var(--ink);
  border: 1px solid var(--ink);
  color: var(--waiting-text);
}

/* ========================================================================
   ACCOUNT + SETTINGS INFORMATION HIERARCHY · 2026-09-04

   Account-wide controls and per-device controls are separate destinations.
   These scoped rules turn the Account summary into one readable band, keep
   its section chooser on one line, and make Settings a concise directory
   whose first doorway is the per-device workspace. Existing cards and their
   handlers remain exactly where their owning modules expect them. */

#settingsMount,
#preferencesMount {
  width: 100%;
  max-width: 1160px;
  margin-inline: auto;
}

#settingsHead,
#preferencesMount > .preferences-head { margin-bottom: 0; }

/* One household summary, not four unrelated cards. The child elements keep
   their existing data and classes, while the outer band supplies the edge and
   the dividers that explain their relationship. */
#settingsTiles.ovtiles {
  gap: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--dusk);
  box-shadow: var(--shadow);
}
#settingsTiles > .ovtile {
  min-width: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
#settingsTiles > .ovtile + .ovtile { border-left: 1px solid var(--line); }
#settingsTiles > .ovtile.acctowner {
  background: color-mix(in srgb, var(--glow) 9%, var(--dusk));
  border-color: var(--line);
}

/* The chooser is navigation within Account, so it reads as one segmented
   control instead of five loose pills floating between summary and content. */
#settingsTabs.account-tabs {
  gap: 6px;
  margin: 0;
  padding: 7px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--dusk2);
}
#settingsTabs.account-tabs > .chip {
  flex: 1 1 150px;
  margin: 0;
}
.account-section-select { display: none; }

/* js/account.js renders the real role-aware page heading into #settingsHead.
   The static fallback above the mount repeated "Account" immediately before
   that heading on phones; desktop already hid it in an older breakpoint. */
#settings > .toprow { display: none; }

@media (max-width: 620px) {
  /* Five account destinations cannot advertise horizontal overflow inside an
     iPhone SE column. One native chooser keeps every live destination visible
     by name, preserves a 48px tap target and leaves the desktop segmented
     control unchanged. */
  #settingsTabs.account-tabs {
    display: block;
    overflow: visible;
    /* NO SHELL AROUND A SINGLE FULL-WIDTH SELECT. The border, the 7px padding
       and the --dusk2 fill are what make five chips read as ONE segmented
       control on a desktop; wrapped around the lone phone chooser they were a
       second border drawn 6px outside the select's own, and they cost 14px of
       vertical chrome on the narrowest screen the product supports. MEASURED
       at 390x844 before this: the strip was 66px tall for a 48px control. */
    padding: 0;
    border: 0;
    background: transparent;
  }
  #settingsTabs.account-tabs > .chip { display: none; }
  #settingsTabs.account-tabs > .account-section-select {
    display: block;
    width: 100%;
    /* 48px EXACTLY, not a min: `.inp`'s own padding pushed the rendered box to
       ~52px, so a `min-height` floor could not hold the strip inside its
       budget. 48 >= the 44px touch minimum with room to spare. */
    min-height: 48px;
    height: 48px;
    margin: 0;
    font-weight: 750;
  }
}

/* The long billing comparison needs the full reading measure. People/security
   and Data/privacy retain the existing two-up layout once the canvas is wide
   enough; below 1400px the shared responsive rule already makes them one
   column. Lone destinations span the grid and use a comfortable inner width. */
#settingsMount[data-account-group="billing"] > #planCard,
#settingsMount[data-account-group="billing"] > #billingSettingsPanel,
#settingsMount[data-account-group="alerts"] > #notifCard,
#settingsMount[data-account-group="help"] > #reportCard {
  grid-column: 1 / -1;
}

/* Lone destinations stay at a comfortable reading width, and the ledger keeps
   a bounded reading window instead of making every visit inherit its full
   history. */
@media (max-width: 1679.98px) {
  #settingsMount[data-account-group="alerts"] > #notifCard,
  #settingsMount[data-account-group="help"] > #reportCard {
    width: min(100%, 760px);
    justify-self: center;
  }
}
#settingsMount #hhEvents {
  max-height: 420px;
  overflow-y: auto;
  overscroll-behavior-block: contain;
  scrollbar-gutter: stable;
  padding-right: 4px;
}
/* Settings owns only real dashboard preferences. Put the two short fields in
   one calm row and keep the save/status relationship together beneath it. */
#preferencesMount > .preferences-appearance,
#preferencesMount > .preferences-destinations { grid-column: 1 / -1; }
.preferences-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 16px;
}
.preferences-field { min-width: 0; }
.preferences-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 16px;
}
.preferences-actions > .primary {
  flex: 0 0 auto;
  width: auto;
  min-width: 180px;
  margin: 0;
  padding-inline: 24px;
}
.preferences-actions > #preferencesLive {
  flex: 1 1 auto;
  margin: 0;
  padding: 0;
  text-align: left;
}
.preferences-actions > #preferencesLive:empty { display: none; }

/* Destination cards are a scope map: the badge answers "what will this
   affect?", while the title and action answer "where do I go?". */
.preference-destination-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 16px;
}
.preference-destination {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  min-width: 0;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: 15px;
  background: var(--dusk2);
}
.preference-destination.is-primary {
  grid-column: 1 / -1;
  padding: 16px;
  border-color: color-mix(in srgb, var(--glow) 42%, var(--line));
  background: color-mix(in srgb, var(--glow) 9%, var(--dusk));
}
/* After the full-width device doorway, the two account destinations pair up.
   Support is the intentional final card; center it across the grid instead of
   leaving an accidental empty card-sized hole beside it. */
.preference-destination-grid > .preference-destination:last-child {
  grid-column: 1 / -1;
  width: min(100%, 760px);
  justify-self: center;
}
.preference-destination-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--dusk);
  color: var(--glow);
  font-size: 16px;
  font-weight: 850;
}
/* GoodQA ticket 488 — the real inline-stroke SVG that replaced this badge's
   '▭'/'◉'/'✉'/'?' text glyph. */
.prefdesticon { width: 19px; height: 19px; }
/* `min-width: 0` LETS THE COLUMN SHRINK; `overflow-wrap` IS WHAT MAKES THAT
   SAFE. Same pair, same reason, as `.healthsummary-label` on the Devices
   cards. The grid track is `minmax(0, 1fr)` between an icon and a button, so
   on a narrow card this box gets whatever is left — MEASURED at 320x568 in
   Spanish: 68px, holding the word "Notificaciones", which renders 107px. It
   painted 39px over the "Administrar" button to its right, and the sub-line
   under it did the same with "contraseñas," (72px in the same 68px). English
   "Notifications" fits at 440 and above, which is why neither showed up in the
   language the copy was written in.
   `overflow-wrap` INHERITS, so declaring it on the copy column covers the
   title and the `.asub` beneath it without repeating itself. */
.preference-destination-copy { min-width: 0; overflow-wrap: anywhere; }
.preference-scope {
  display: inline-flex;
  width: fit-content;
  margin-bottom: 5px;
  padding: 3px 7px;
  border: 1px solid color-mix(in srgb, var(--glow) 30%, var(--line));
  border-radius: 999px;
  background: color-mix(in srgb, var(--glow) 8%, transparent);
  color: var(--glow);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  line-height: 1.2;
  text-transform: uppercase;
}
:root[data-theme="light"] .preference-scope {
  /* WAS #E0F3F8/#77B4C7/#075F80 -- teal leftovers from when light --glow was
     #087EA4. --glow is #2B6CB0 now, and NO TEAL EXISTS IN ANY OF THE 49
     HANDOFF ARTBOARDS, so these were the only teal left on this screen. Read
     the role tokens instead of hardcoding, so the next palette move carries. */
  background: var(--brand-tint);
  border-color: var(--line);
  color: var(--brand);
}
.cardhead > .preference-scope {
  flex: none;
  margin: 0;
}
.preference-destination-title {
  margin: 0 0 3px;
  color: var(--cloud);
  font-size: 15px;
  line-height: 1.25;
}
.preference-destination .asub {
  color: var(--haze);
  font-size: 12px;
  line-height: 1.4;
}
.preference-destination-open {
  width: auto;
  margin: 0;
  white-space: nowrap;
}
.preference-destination.is-primary .preference-destination-open {
  border-color: color-mix(in srgb, var(--glow) 48%, var(--line));
  color: var(--glow);
}
:root[data-theme="light"] .preference-destination.is-primary .preference-destination-open {
  /* Same teal leftover as .preference-scope above. */
  border-color: var(--line);
  color: var(--brand);
}

@media (max-width: 899px) {
  #settingsMount,
  #preferencesMount { gap: 12px; }

  /* The identity uses the first row; the three real counts share the second.
     Helper sentences disappear here because the labels already carry their
     meaning and three repeated lines were the source of most phone height. */
  #settingsTiles.ovtiles {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  #settingsTiles > .ovtile.acctowner {
    grid-column: 1 / -1;
    border-bottom: 1px solid var(--line);
  }
  #settingsTiles > .ovtile.acctowner + .ovtile { border-left: 0; }
  #settingsTiles > .ovtile:not(.acctowner) { padding: 11px 8px; }
  #settingsTiles > .ovtile:not(.acctowner) .ovt-k {
    font-size: 9.5px;
    letter-spacing: .035em;
  }
  #settingsTiles > .ovtile:not(.acctowner) .ovt-s { display: none; }

  #settingsTabs.account-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: thin;
  }
  #settingsTabs.account-tabs > .chip {
    flex: 0 0 auto;
    /* 44px, not 40. `.chip` sets min-height: 44px and this rule pulled the
       Account group chips back under it on the ONE layout that still shows
       chips rather than the native chooser — MEASURED 40px at 768x1024 in both
       themes, for all four groups, in en/es/fr. Nothing needed the four
       pixels: the row scrolls horizontally, so height was never the constraint. */
    min-height: 44px;
  }

  .preferences-fields,
  .preference-destination-grid { grid-template-columns: minmax(0, 1fr); }
  .preference-destination.is-primary { grid-column: auto; }
}

@media (max-width: 560px) {
  .preferences-actions {
    align-items: stretch;
    flex-direction: column;
  }
  .preferences-actions > .primary { width: 100%; }
  .preference-destination {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
  }
  .preference-destination-open {
    grid-column: 2;
    justify-self: stretch;
  }
}

/* ========================================================================
   HELP HUB · 2026-09-04

   Help is a short, truthful directory. It owns no duplicate forms or state:
   every card routes to the existing screen that owns the named controls. */
#supportMount {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  width: 100%;
  max-width: 1160px;
  margin-inline: auto;
}
#supportMount > .support-hub-head { margin-bottom: 0; }

.support-topic-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}
.support-topic {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
  height: 100%;
  margin: 0;
  padding: 20px;
}
.support-topic-primary {
  border-color: color-mix(in srgb, var(--glow) 42%, var(--line));
  background: linear-gradient(135deg, color-mix(in srgb, var(--glow) 10%, var(--dusk)), var(--dusk));
}
.support-topic-head {
  display: flex;
  align-items: flex-start;
  gap: 13px;
}
.support-topic-icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border: 1px solid color-mix(in srgb, var(--glow) 32%, var(--line));
  border-radius: 13px;
  background: color-mix(in srgb, var(--glow) 9%, var(--dusk2));
  color: var(--glow);
  font-size: 18px;
  font-weight: 800;
}
/* GoodQA ticket 488 — the real inline-stroke SVG that replaced this badge's
   '?'/'▭'/'☺'/'◉' text glyph. */
.supporttopicicon { width: 22px; height: 22px; }

/* GoodQA ticket 487 — Help's missing search field. A real filter over the
   topic cards and FAQ items already on the page (js/support.js's
   applySearchFilter), not a decorative box that goes nowhere. */
.support-search {
  position: relative;
  display: flex;
  align-items: center;
  margin: 0 0 18px;
}
.supportsearchicon {
  position: absolute;
  left: 15px;
  width: 18px;
  height: 18px;
  color: var(--haze);
  pointer-events: none;
}
.support-search-input {
  width: 100%;
  padding: 13px 16px 13px 44px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--dusk2);
  color: var(--cloud);
  font: inherit;
  font-size: 14px;
}
.support-search-input::placeholder { color: var(--haze); }
.support-search-input:focus-visible { outline: 2px solid var(--glow); outline-offset: 2px; }
.support-search-empty { margin-top: 10px; }
.support-topic-copy { min-width: 0; }
.support-topic-scope {
  display: inline-flex;
  margin-bottom: 6px;
  padding: 3px 7px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--dusk2);
  color: var(--haze);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  line-height: 1.2;
  text-transform: uppercase;
}
.support-topic-title {
  margin: 0 0 6px;
  color: var(--cloud);
  font-size: 17px;
  line-height: 1.3;
}
.support-topic-sub {
  margin: 0;
  color: var(--haze);
  font-size: 13px;
  line-height: 1.5;
}
.support-topic-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: auto;
}
.support-hub-action {
  flex: 1 1 auto;
  width: auto;
  min-width: fit-content;
  margin: 0;
}
.support-hub-action-primary {
  border-color: var(--glow);
  background: var(--glow);
  color: var(--dusk);
  box-shadow: 0 8px 20px color-mix(in srgb, var(--glow) 18%, transparent);
}

@media (max-width: 1199px) {
  .support-topic-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (min-width: 1200px) and (max-width: 1599.98px) {
  .support-topic-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .support-topic-grid > .support-topic:last-child { grid-column: 1 / -1; }
}

@media (max-width: 560px) {
  #supportMount { gap: 12px; }
  .support-topic { gap: 14px; padding: 16px; }
  .support-topic-head { gap: 11px; }
  .support-topic-icon { width: 38px; height: 38px; }
  .support-topic-actions { flex-direction: column; }
  .support-hub-action { width: 100%; }
}

/* GoodQA c39261e6/77b37b8e — the FAQ section support.js's own header comment
   describes. Plain <details>/<summary> so it stays keyboard- and
   screen-reader-usable with no extra script; only the affordance and rhythm
   are styled here. */
.support-faq { padding: 20px; margin: 0; }
.support-faq-title { margin: 0 0 12px; color: var(--cloud); font-size: 17px; line-height: 1.3; }
.support-faq-list { display: flex; flex-direction: column; gap: 2px; }
.support-faq-item {
  border-top: 1px solid var(--line);
  padding: 12px 0;
}
.support-faq-item:first-child { border-top: none; padding-top: 0; }
.support-faq-q {
  cursor: pointer;
  color: var(--cloud);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.4;
  list-style: none;
}
.support-faq-q::-webkit-details-marker { display: none; }
.support-faq-q::before {
  content: '+';
  display: inline-block;
  width: 1.1em;
  color: var(--glow);
  font-weight: 800;
}
.support-faq-item[open] > .support-faq-q::before { content: '−'; }
.support-faq-a { margin-top: 8px; padding-left: 1.1em; }
.support-faq-a p { margin: 0; color: var(--haze); font-size: 13px; line-height: 1.55; }
.support-faq-note { margin-top: 8px; }
.support-faq-link { margin-top: 10px; }

@media (max-width: 560px) {
  .support-faq { padding: 16px; }
}

/* ========================================================================
   DEVICE WORKSPACE COMPOSITION · 2026-09-04

   Every existing control and honesty disclosure remains present. This pass
   changes only composition: short related cards stay together, long lists
   get a keyboard-reachable reading window, and multi-part editors receive
   enough width to use the layouts they already implement. */

/* “Who is using it?” is the primary device/profile boundary. The two strings
   painted on the tablet itself are one coherent companion card rather than
   two nearly empty cards that repeat the same editing pattern. */
.policy-overview-display { min-width: 0; }
.policy-overview-fields { display: grid; gap: 14px; margin-top: 14px; }
.policy-overview-field { min-width: 0; }
.policy-overview-field > .fld { display: block; margin-bottom: 6px; }

/* The next-check-in timing remains visible. Less common side effects stay
   findable without consuming the switcher's default reading path. */
.sw-impact { margin-top: 8px; border-top: 1px solid var(--line); }
.sw-impact > summary {
  min-height: 44px;
  padding: 10px 2px;
  cursor: pointer;
  color: var(--cloud);
  font-size: 12px;
  font-weight: 750;
}
.sw-impact > .fineprint { margin: 0 0 4px 18px; }

/* Protection owns the complete explanation. Every other device task keeps the
   exact state headline and an explicit route back, without repeating the long
   paragraph above unrelated controls. */
#policy:not([data-policy-view="protection"]) > #webBanner { display: none; }
#policy[data-policy-view="protection"] > #webStatusCompact { display: none; }
#policy > .webstatuscompact {
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  margin-bottom: 0;
  padding: 10px 12px;
}
#policy > .webstatuscompact .dot { margin-top: 0; }
#webStatusCompactTitle { min-width: 0; line-height: 1.35; }
#webReviewProtectionBtn {
  width: auto;
  min-height: 44px;
  margin: 0;
  padding: 8px 11px;
  white-space: nowrap;
}
@media (max-width: 560px) {
  #policy > .webstatuscompact { grid-template-columns: 9px minmax(0, 1fr); }
  #policy > .webstatuscompact .dot { align-self: start; margin-top: 5px; }
  #webReviewProtectionBtn { grid-column: 2; justify-self: start; }
}

/* A recovery credential does not belong above every device task. Overview
   carries only this compact, actionable warning while the server says the
   code is still a lifeline; the full value and instructions live in More. */
#policy > .setupcode-compact {
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  margin-bottom: 0;
  padding: 10px 12px;
}
#policy > .setupcode-compact .dot { margin-top: 0; }
#setupCodeCriticalText { min-width: 0; line-height: 1.35; }
#setupCodeCriticalBtn {
  width: auto;
  min-height: 44px;
  margin: 0;
  padding: 8px 11px;
  white-space: nowrap;
}
@media (max-width: 560px) {
  #policy > .setupcode-compact { grid-template-columns: 9px minmax(0, 1fr); }
  #policy > .setupcode-compact .dot { align-self: start; margin-top: 5px; }
  #setupCodeCriticalBtn { grid-column: 2; justify-self: start; }
}

/* Installed inventory can contain dozens of real apps. It belongs to Apps,
   but it must not turn every visit into a 3,000px document. Two readable
   columns and a focusable contained scroller preserve the complete list. */
#installedAppsCard { grid-column: 1 / -1; }
#apphistList {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  max-height: 540px;
  overflow-y: auto;
  overscroll-behavior-block: contain;
  scrollbar-gutter: stable;
  padding: 2px 4px 2px 2px;
  border-radius: 12px;
}
#apphistList > .arow { min-width: 0; margin: 0; }
#apphistList:focus-visible { outline: 2px solid var(--glow); outline-offset: 3px; }

/* The week editor already has a container-query two-column design; it simply
   never reached its 620px trigger while the parent grid held it to one track.
   Give it the full workspace, then keep the optional schedule form centered. */
@media (min-width: 900px) {
  #timeWeekCard { grid-column: 1 / -1; }
  #schedCard {
    grid-column: 1 / -1;
    width: 100%;
  }
}

/* Protection contains three different decisions: default safeguards, web
   lookup coverage, and device-wide camera/internet controls. They now read as
   named regions of one device policy instead of one uninterrupted text wall. */
#protectionSettingsCard { grid-column: 1 / -1; }
.protection-layout {
  display: grid;
  grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr);
  grid-template-areas:
    "defaults web"
    "device web";
  align-items: start;
  gap: 16px;
  margin-top: 16px;
}
.protection-group {
  min-width: 0;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: color-mix(in srgb, var(--dusk2) 72%, var(--dusk));
}
.protection-defaults { grid-area: defaults; }
.protection-web { grid-area: web; }
.protection-device { grid-area: device; }
.protection-web > .dns-label,
.protection-device > .vpn-sep:first-child { margin-top: 0; }
/* THE OWNER'S "the website safety box is like random af under the rest of the
   main page card", AND WHAT WAS ACTUALLY WRONG.

   It was not a margin and it was not the wrong container: #websiteSafetyCard is
   already a direct child of #policy, exactly like #protectionSettingsCard, and
   both already span every track of the `.screen` grid. What made it read as a
   stray box was this rule's own `width: min(100%, 820px)` + `justify-self:
   center` — a reading-width cap applied to ONE of the two cards on the tab.

   MEASURED in Chromium on the rendered Protection tab, both themes, before this
   change (test/protection-tab-layout.test.js is the same measurement, kept):

     1440x900   #protectionSettingsCard  left 292   right 1398  width 1106
                #websiteSafetyCard       left 435   right 1255  width  820
     3840x2160  #protectionSettingsCard  left 1283  right 2807  width 1524
                #websiteSafetyCard       left 1635  right 2455  width  820

   So the card floated 143px inside its neighbour's left edge on a laptop and
   352px inside it on a 4K screen, with nothing else on the tab sharing either
   edge. A cap is only invisible when every card on the surface wears it; here
   it was the one difference on a two-card tab, which is precisely the shape a
   parent reads as "random".

   The cap is dropped rather than copied onto the card above it: Protection's
   own three-region grid (.protection-layout) is what gives that card its
   readable measure, and forcing 820px onto it would break those regions. The
   phone was never affected — below 900px `.screen` is not a grid, so
   `justify-self` did nothing and `min(100%, 820px)` resolved to 100%; both
   cards measured left 10 / right 380 at 390px before and after. */
#websiteSafetyCard { grid-column: 1 / -1; }

@media (max-width: 899px) {
  #apphistList { grid-template-columns: minmax(0, 1fr); max-height: 460px; }
  .protection-layout { display: block; margin-top: 12px; }
  .protection-group {
    padding: 16px 0;
    border: 0;
    border-radius: 0;
    background: transparent;
  }
  .protection-group + .protection-group { border-top: 1px solid var(--line); }
}

/* A hidden sixth destination made the phone workspace feel incomplete. Keep
   the sticky navigation compact, but show every destination as two rows of
   three instead of asking parents to discover a horizontal scroller. */
@media (max-width: 560px) {
  #policy > .policy-tabs {
    flex-wrap: wrap;
    overflow: visible;
    scroll-snap-type: none;
  }
  #policy > .policy-tabs > .policy-tab {
    flex: 1 1 calc((100% - 10px) / 3);
    min-width: 0;
    padding-inline: 8px;
  }
}

/* ========================================================================
   SPARSE HOME COMPOSITION · 2026-09-04

   Home mixes page-wide rows, child heroes and a variable number of factual
   status tiles. Those are three different layout jobs. Child cards now have
   their own grid, so one child can be intentionally centred and an odd final
   child no longer sits in an apparently missing left/right pair. No card
   content or state vocabulary changes here. */
.ovchildren {
  display: grid;
  /* auto-fit, not a fixed single column: one child still gets the whole row
     (no "intentionally centred" card left narrow beside empty space), but two
     or more sit side by side on a page wide enough for it — same idiom as
     .devgroupbody elsewhere in this file. A fixed single column meant a
     two-child household's second hero card, tiles and all, landed a full
     card-height lower than it needed to — measured failing
     reading-load-ui.test.js's five-second rule at 1440x900. auto-fit still
     collapses to one column on a narrow viewport, so the "no apparently
     missing left/right pair" reasoning above still holds for an odd child;
     it just stops applying that treatment when the page has room for more. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
  gap: 16px;
  min-width: 0;
}
.ovchildren > .ovcard { min-width: 0; margin: 0; }

/* Empty-household add. NOT a child of .ovchildren — that grid's 2-column
   wrap is what made a persistent "+" cost a whole extra row once two real
   cards already filled the first. This control only paints when there are
   no child cards at all. */
.ovempty-add {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 52px;
  margin: 0;
  padding: 12px 16px;
  border: 1.5px dashed var(--line);
  border-radius: 16px;
  background: transparent;
  color: var(--cloud);
  font-weight: 700;
  cursor: pointer;
}
.ovempty-add-plus { font-size: 22px; line-height: 1; }

@media (min-width: 900px) {
  /* .adv still spans: it explains the whole child set.
     .ovchildren NO LONGER DOES (W4.1). It is "Today at a glance" in the
     handoff, a column of per-child cards in the RIGHT track beside the
     decisions on the left — which is also why it stops being a 2-up grid at
     this width (see .ovchildren's own rule) and becomes one card per row. */
  #overviewMount > .adv { grid-column: 1 / -1; }
  #overviewMount > .ovchildren { grid-column: 2; }
  #overviewMount > .adv { margin-top: 0; }

  /* A single factual tile should read as a concise status card, not a quarter
     of a four-column row with three conspicuous empty tracks. */
  #overviewMount > .ovtiles[data-tile-count="1"] {
    grid-template-columns: minmax(0, 420px);
    justify-content: center;
  }
}

/* The rail leaves a narrower reading column at ordinary laptop widths, where
   two status tiles per row remain readable. When there are three, centre the
   final tile at the same width as the pair above it. */
@media (min-width: 900px) and (max-width: 1399.98px) {
  #overviewMount > .ovtiles[data-tile-count="3"] > .ovtile:last-child {
    grid-column: 1 / -1;
    width: calc((100% - 14px) / 2);
    justify-self: center;
  }
}

/* Wide desktop has room for two child heroes or three status cards. A lone
   hero gets more room than one ordinary track without becoming an unwieldy
   full-page card; any odd final hero retains the exact width of its siblings
   and is centred beneath them. */
@media (min-width: 1400px) {
  /* ONE CARD PER ROW, not two side by side. .ovchildren is now the RIGHT
     track of #overviewMount's two-track middle band (W4.1), ~513px wide, so a
     2-up grid inside it would give ~248px cards -- narrower than the 375px
     phone layout this file's own comments treat as the floor. The handoff
     stacks them the same way, one per row under "Today at a glance". */
  .ovchildren { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  /* BOTH OF THESE WERE 2-UP ARTIFACTS AND ARE NOW NO-OPS BY DESIGN.
     While .ovchildren was a two-column grid spanning the full 1198px mount,
     a lone card needed `min(100%, 780px)` so it did not stretch across the
     whole Overview, and an odd FINAL card needed `calc((100% - 18px) / 2)` so
     the centred straggler matched the width of the pair above it.
     W4.1 made .ovchildren a single column in the ~482px right track, so every
     card is already the same width and the track itself is the bound. Keeping
     the half-width rule made the third of three children render NARROWER than
     the two above it -- caught by reading-load-ui.test.js, which is why these
     are neutralised rather than deleted: the selectors are left in place, with
     the reasoning, so a future return to a 2-up layout finds them. */
  .ovchildren > .ovcard:only-child,
  .ovchildren > .ovcard:nth-child(odd):last-child:not(:only-child) {
    grid-column: 1 / -1;
    width: auto;
    justify-self: stretch;
  }

  #overviewMount > .ovtiles[data-tile-count="2"] {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  #overviewMount > .ovtiles[data-tile-count="3"] {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* ========================================================================
   CHECK-INS & QUIZZES · CONTAINED AUTHORING, 2026-09-04

   Authoring is a short-lived task, not another page-length card. The overlay
   keeps both composers inside the viewport, while the dialog itself owns the
   scroll and its decision row stays reachable. All colours are existing
   surface/status tokens in both themes. */
body.engage-dialog-open { overflow: hidden; }
.engage-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
    max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
  background: rgba(4, 10, 20, .72);
  backdrop-filter: blur(7px);
}
.engage-modal {
  position: relative;
  /* F2.3 — THE TWO MODAL WIDTHS WERE INVERTED against her artboards, measured
     live at 1512x982 by the engage lane: this base rule is the QUIZ width and
     was 820px where hers is 660; the checkin override below was 680px where
     hers is 740. So the shorter dialog was the wider one on screen. */
  width: min(100%, 660px);
  max-height: calc(100dvh - 24px);
  margin: 0;
  padding: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-color: color-mix(in srgb, var(--glow) 28%, var(--line));
  box-shadow: 0 28px 90px rgba(0, 0, 0, .48);
}
.engage-overlay[data-engage-composer="checkin"] .engage-modal { width: min(100%, 740px); }
.engage-modal > .engage-compose,
.engage-modal > .engage-build { padding: 24px; }
.engage-modal .sect:first-child { padding-right: 46px; }
.engage-modal-x {
  position: absolute;
  z-index: 2;
  top: 15px;
  right: 15px;
  width: 40px;
  height: 40px;
  padding: 0;
  display: grid;
  place-items: center;
  font-size: 22px;
  line-height: 1;
  background: var(--dusk2);
}
.engage-modal-actions {
  position: sticky;
  z-index: 1;
  bottom: -24px;
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 9px;
  margin: 18px -24px -24px;
  padding: 14px 24px max(14px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--dusk) 94%, transparent);
  backdrop-filter: blur(12px);
}
.engage-modal-actions .primary,
.engage-modal-actions .ghost { width: auto; margin: 0; flex: 0 1 auto; }
.engage-modal-actions .primary { min-width: 150px; }
.engage-show-older { margin-top: 12px; }

@media (max-width: 480px) {
  .engage-modal > .engage-compose,
  .engage-modal > .engage-build { padding: 18px; }
  .engage-modal-actions {
    bottom: -18px;
    margin: 16px -18px -18px;
    padding: 12px 18px max(12px, env(safe-area-inset-bottom));
  }
  .engage-modal-actions .primary { order: -1; width: 100%; }
  .engage-modal-actions .ghost { flex: 1 1 auto; }
}

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

/* ========================================================================
   ACCOUNT + PHONE CHROME · COMPACT, TRUTH-PRESERVING FOLLOW-UP
   ======================================================================== */

/* Secondary People & security tasks stay intact inside native disclosures.
   The current people and sign-in state remain the first two visible cards;
   password rotation and the audit ledger become short, named doorways. */
#settingsMount[data-account-group="accounts"] > #coparentCard { order: 1; }
#settingsMount[data-account-group="accounts"] > #securityCard { order: 2; }
#settingsMount[data-account-group="accounts"] > #accountPasswordDisclosure { order: 3; }
#settingsMount[data-account-group="accounts"] > #accountHistoryDisclosure { order: 4; }

.account-disclosure {
  min-width: 0;
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--dusk);
  box-shadow: var(--shadow);
}
.account-disclosure.is-empty,
.account-disclosure:has(> .account-disclosure-body > .hide) { display: none; }
.account-disclosure > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 64px;
  padding: 11px 15px;
  color: var(--cloud);
  cursor: pointer;
  list-style: none;
}
.account-disclosure > summary::-webkit-details-marker { display: none; }
.account-disclosure > summary::before {
  content: "▸";
  flex: none;
  color: var(--glow);
  font-size: 13px;
}
.account-disclosure[open] > summary::before { content: "▾"; }
.account-disclosure-copy { display: grid; gap: 3px; min-width: 0; }
.account-disclosure-copy > strong { font-size: 14px; line-height: 1.25; }
.account-disclosure-copy > span { color: var(--haze); font-size: 11.5px; line-height: 1.35; }
.account-disclosure-body { border-top: 1px solid var(--line); }
.account-disclosure-body > .card {
  margin: 0;
  padding: 16px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
/* The summary already names this region; avoid printing the same heading on
   the very next line while retaining the original node for existing wiring. */
.account-disclosure-body > .card > .sect:first-child { display: none; }

/* Security keeps the exact server count in view while the potentially long
   per-session ledger opens only when a parent asks for it. */
.security-sessions-disclosure {
  margin-top: 14px;
  border-top: 1px solid var(--line);
}
.security-sessions-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 58px;
  padding: 10px 2px;
  color: var(--cloud);
  cursor: pointer;
  list-style: none;
}
.security-sessions-summary::-webkit-details-marker { display: none; }
.security-sessions-summary::before { content: "▸"; color: var(--glow); font-size: 13px; }
.security-sessions-disclosure[open] > .security-sessions-summary::before { content: "▾"; }
.security-sessions-copy { display: grid; flex: 1 1 auto; gap: 3px; min-width: 0; }
.security-sessions-copy > strong { font-size: 14px; }
.security-sessions-copy > span { color: var(--haze); font-size: 11.5px; line-height: 1.35; }
.security-sessions-summary > .pill { flex: none; }
.security-sessions-list { padding: 0 0 4px 17px; }

/* THE ENROLMENT QR — Account › Security, drawn by js/consent.js from the SVG
   the enrol response carries (platform/src/qr.js).

   BLACK ON WHITE IN BOTH THEMES, DELIBERATELY, and this is the one place on
   this page where the theme does not get a vote. A camera looks for dark
   modules inside a light quiet zone; a symbol tinted to match a dark card, or
   left transparent over one, is a symbol that half the phones that meet it
   will not lock onto. The white plate is painted here as well as inside the
   SVG so a future change to either one cannot leave the symbol on the card's
   own background.

   THE WIDTH IS A FLOOR AND A CEILING. A version-10 symbol is 57 modules plus
   an 8-module quiet zone; below about 150px on screen the modules land under
   two device pixels each and phone cameras start missing it, and above ~260px
   it is simply a large square in the middle of a desktop card. 220px is inside
   both bounds at 390px wide and at 1200px, which is why there is no media
   query here. `image-rendering: pixelated` keeps the module edges square if a
   browser ever resamples the SVG rather than re-rasterising it. */
.security-qr {
  display: flex;
  justify-content: center;
  margin: 12px 0 4px;
  padding: 12px;
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 14px;
}
.security-qr-img {
  display: block;
  width: 100%;
  max-width: 220px;
  height: auto;
  image-rendering: pixelated;
}

/* "Cannot scan it? Type the key instead" — the fallback for an authenticator
   with no camera. Styled as a quiet disclosure, not a second heading: it must
   be findable and must not compete with the picture above it. Same
   marker-replacement trick as .security-sessions-summary, for the same reason
   (a native ▸ is not styleable and differs per browser). */
.security-manual { margin-top: 10px; }
.security-manual-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  color: var(--haze);
  font-size: 13px;
  cursor: pointer;
  list-style: none;
}
.security-manual-summary::-webkit-details-marker { display: none; }
.security-manual-summary::before { content: "▸"; color: var(--glow); font-size: 13px; }
.security-manual[open] > .security-manual-summary::before { content: "▾"; }

/* Download and Copy, side by side and BOTH OPTIONAL. They wrap rather than
   shrink below 390px: two .ghost buttons squeezed onto one line put their
   labels on two lines each, which reads as four controls. */
.security-code-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.security-code-actions > .ghost { flex: 1 1 140px; }

@media (max-width: 899px) {
  /* The segmented chooser remains swipeable, but the browser's native gutter
     no longer makes a 40px control into a 66px strip at 390px. */
  #settingsTabs.account-tabs {
    scrollbar-width: none;
    -ms-overflow-style: none;
    scroll-snap-type: x proximity;
  }
  #settingsTabs.account-tabs::-webkit-scrollbar { width: 0; height: 0; display: none; }
  #settingsTabs.account-tabs > .chip { scroll-snap-align: start; }

  /* All mandatory two-step facts stay visible. Grid gives the title and CTA
     one row and the qualifying consequence the full line below, instead of
     squeezing the paragraph into a narrow column beside the button. */
  #enrolBanner {
    display: grid;
    grid-template-columns: 9px minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 8px;
    row-gap: 5px;
    padding: 9px 11px;
  }
  #enrolBanner > .dot { grid-column: 1; grid-row: 1; margin: 0; }
  #enrolBanner > span:not(.dot) { display: contents; }
  #enrolBannerTitle {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: 12.5px;
    line-height: 1.3;
  }
  #enrolBannerBody {
    display: block;
    grid-column: 2 / -1;
    grid-row: 2;
    font-size: 11.5px;
    font-weight: 400;
    line-height: 1.35;
  }
  /* 44px for the same reason, and by the same measurement, as
     `.banner.is-oneline button` above: this is the rule that applied before
     the one-line variant took over, so it was the one undercutting the 44px
     targets the `.is-oneline` block further down already asks for. */
  #enrolBannerBtn {
    grid-column: 3;
    grid-row: 1;
    width: auto;
    min-height: 44px;
    margin: 0;
    padding: 7px 10px;
    white-space: nowrap;
  }
  #enrolBanner.is-oneline { grid-template-columns: 9px minmax(0, 1fr) auto; }
  #enrolBanner.is-oneline #enrolBannerTitle,
  #enrolBanner.is-oneline #enrolBannerBody { grid-column: 2 / -1; }
  #enrolBanner.is-oneline #enrolBannerBtn { grid-column: 2; grid-row: 3; }
  #enrolBanner.is-oneline #enrolBannerDismiss { grid-column: 3; grid-row: 3; }

  /* The optional offer follows a parent to every route, so keep only its
     decision in the phone chrome. The mandatory and recovery-code variants do
     not carry `.is-oneline` and retain the consequence row above. */
  #enrolBanner.is-oneline {
    grid-template-columns: 9px minmax(0, 1fr) auto auto;
    row-gap: 0;
    padding-block: 7px;
  }
  #enrolBanner.is-oneline #enrolBannerTitle {
    grid-column: 2;
    grid-row: 1;
  }
  #enrolBanner.is-oneline #enrolBannerBody { display: none; }
  #enrolBanner.is-oneline #enrolBannerBtn {
    grid-column: 3;
    grid-row: 1;
  }
  #enrolBanner.is-oneline #enrolBannerDismiss {
    grid-column: 4;
    grid-row: 1;
  }

}

/* ========================================================================
   DEVICES · OUTCOME FIRST, DIAGNOSTICS ON REQUEST · 2026-09-04

   A device tile now leads from identity straight to its real actions. The
   worst-case badge and child/connectivity line remain visible; the same four
   evidence-backed health destinations are retained in a native disclosure.
   This changes no state vocabulary and no control, only the default reading
   path on a dense household inventory. */

#devices .devcard > .devalerts { order: 1; }
#devices .devcard > .devactions { order: 2; }
#devices .devcard > .healthcap { order: 3; }
#devices .devcard > .devfacts { order: 4; }

#devices .devcard details.healthcap:not([open]) {
  display: block;
  padding: 0;
}
#devices .devcard details.healthcap > .healthsummary {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 9px 11px;
  color: var(--cloud);
  cursor: pointer;
  list-style: none;
}
#devices .devcard details.healthcap > .healthsummary::-webkit-details-marker { display: none; }
#devices .devcard details.healthcap > .healthsummary::before {
  content: "▸";
  flex: none;
  color: var(--glow);
  font-size: 11px;
}
#devices .devcard details.healthcap[open] > .healthsummary::before { content: "▾"; }
#devices .devcard details.healthcap[open] > .healthsummary {
  margin: -3px -5px 2px;
  padding: 6px 6px 9px;
  border-bottom: 1px solid var(--line);
}
#devices .devcard .healthsummary-label {
  flex: 1 1 auto;
  min-width: 0;
  /* `min-width: 0` IS THE POINT AND ALSO THE HAZARD, so the break has to come
     with it. It lets this flex item shrink below its content — which is what
     keeps the state pill beside it from being pushed off a 320px card — and a
     shrunk box then has no way to fit a word longer than itself. MEASURED at
     320x568 in Spanish: `t('devices.details')` is "Detalles", 47px wide, in a
     box that had shrunk to 32px, so the word painted 15px over the state text
     to its right. English "Details" fits and hid it. `.devchild` two rules
     down already carries this exact pair for the same reason. */
  overflow-wrap: anywhere;
  font-size: 12px;
  font-weight: 800;
}
#devices .devcard .healthsummary-state {
  /* NOT `flex: none` any more — that let this run straight past the
     capsule's own edge toward whatever sits to its right (the Menu/…
     buttons, at the tight end of the 2026-09-19 tight-row rewrite's
     >=900px range) instead of truncating, because nothing here could ever
     shrink. `.healthsummary-label`'s own comment a few lines up already
     solved this exact squeeze for "Details" with `min-width: 0` +
     `overflow-wrap: anywhere`; the state text gets the equivalent fix in
     its own idiom (a short state word/phrase reads better ellipsised than
     hard-wrapped mid-word). Ellipsis still shows the parent SOMETHING true
     ("Not prot…") rather than bleeding illegibly into the next control. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11.5px;
  font-weight: 750;
}
#devices .devcard .healthsummary-state.tone-on { color: var(--done-text); }
#devices .devcard .healthsummary-state.tone-wait { color: var(--waiting-text); }
#devices .devcard .healthsummary-state.tone-off,
#devices .devcard .healthsummary-state.tone-paused { color: #FFB4AC; }
:root[data-theme="light"] #devices .devcard .healthsummary-state.tone-on { color: #0A6242; }
/* tone-wait was the one sibling left on the raw --amber token, which landed at
   4.42:1 on the light ground -- an AA failure axe catches on the "Offline"
   roll-up. FIXED AT THE TOKEN 2026-09-06 instead: light `--amber` is #7A4F00,
   the very literal this rule used to paste, so the override is gone and
   `.healthsummary-state.tone-wait { color: var(--waiting-text) }` above now carries it.
   Re-measured after removal: 7.13:1 on a card, 6.39:1 on an `.arow`, 6.25:1 on
   the page — unchanged, because the colour is unchanged. */
:root[data-theme="light"] #devices .devcard .healthsummary-state.tone-off,
:root[data-theme="light"] #devices .devcard .healthsummary-state.tone-paused { color: #B23127; }

/* The child assignment is the boundary between this piece of hardware and a
   child’s shared family rules. Give that one fact a distinct trusted-info
   capsule instead of leaving it as another muted metadata fragment. */
#devices .devcard .devchild {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  max-width: 100%;
  padding: 4px 8px;
  border: 1px solid color-mix(in srgb, var(--glow) 36%, var(--line));
  border-radius: 999px;
  background: color-mix(in srgb, var(--glow) 8%, var(--dusk2));
  color: var(--glow);
}
/* Third teal site, NOT in the consolidated plan -- found by grepping for the
   literal rather than trusting the plan's two. Same leftover, same fix. */
:root[data-theme="light"] #devices .devcard .devchild { color: var(--brand); }

/* The signed-in address is already in the phone menu. Its duplicate under the
   page title consumed a full line before the only actions unique to Devices. */
@media (max-width: 560px) {
  #devices #whoEmail { display: none; }
}
/* SAME DUPLICATE, THE DESKTOP CASE — OWNER, 2026-09-19, flagged the account
   name printed under the Devices subtitle ("Household / My Devices / ... /
   Eric"). This rule only ever covered the phone width above; at >=900px
   paintNavIdentity() already paints the identical value into `#navUser` in
   the persistent rail (renderNav()'s own comment: "painted now from the same
   place #whoEmail is"), so the exact scenario the phone-width rule above
   describes — a duplicate the parent did not ask for, eating a line above
   the screen's own actions — is equally true there. 561-899px is
   deliberately left showing #whoEmail: `.navfoot` (the rail's identity row)
   is itself hidden in that range (`@media (max-width: 899px)` above), so at
   that width #whoEmail is the ONLY place the signed-in parent is named and
   removing it there would leave a gap, not fix a duplicate. */
@media (min-width: 900px) {
  #devices #whoEmail { display: none; }
}

/* ---------------------------------------------------------------- device view
   Once a setup code is no longer the tablet’s lifeline, keep the code, its
   reason and saved date visible while folding only the long operating steps.
   A critical code is opened by renderDeviceSetupCode() and remains fully
   explicit. */
.setupcode-instructions {
  margin-top: 10px;
  border-top: 1px solid var(--line);
}
.setupcode-instructions > summary {
  min-height: 44px;
  padding: 11px 2px;
  color: var(--cloud);
  cursor: pointer;
  font-size: 12px;
  font-weight: 800;
}
.setupcode-instructions > .fineprint { margin: 0 0 8px 18px; }

/* “Who is using it?” is child-owned context inside a device workspace. The
   assignment and the doorway to child rules share a row; adding another child
   sits beside the face picker. All warnings, forms, timing and undo states stay
   full width underneath. */
#deviceAssignmentCard,
#ppChildrenPolicy > .card {
  border-color: color-mix(in srgb, var(--glow) 32%, var(--line));
  background: linear-gradient(145deg,
    color-mix(in srgb, var(--glow) 6%, var(--dusk)),
    var(--dusk) 54%);
}
#deviceAssignmentCard {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(122px, auto);
  align-items: start;
  column-gap: 12px;
}
#deviceAssignmentCard > .sect,
#deviceAssignmentCard > #swNotLive,
#deviceAssignmentCard > #swProfileFormHost,
#deviceAssignmentCard > .help,
#deviceAssignmentCard > .sw-impact,
#deviceAssignmentCard > #swResume,
#deviceAssignmentCard > #swMsg,
#deviceAssignmentCard > #swUndoBtn,
#deviceAssignmentCard > #swUndoNote { grid-column: 1 / -1; }
#deviceAssignmentCard > #swRunning { grid-column: 1; grid-row: 3; margin: 3px 0 0; }
#deviceAssignmentCard > #swFaces { grid-column: 1; grid-row: 4; margin: 7px 0 4px; }
#deviceAssignmentCard > #ppGoChild {
  grid-column: 2;
  grid-row: 3;
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: 8px 10px;
}
#deviceAssignmentCard > #swAddProfileBtn {
  grid-column: 2;
  grid-row: 4;
  align-self: center;
  width: 100%;
  min-height: 44px;
  margin: 7px 0 4px;
  padding: 8px 10px;
}
#deviceAssignmentCard:not(:has(> #ppGoChild)) > #swRunning,
#deviceAssignmentCard:not(:has(> #ppGoChild)) > #swFaces { grid-column: 1 / -1; }

@media (max-width: 350px) {
  #deviceAssignmentCard { grid-template-columns: minmax(0, 1fr); }
  #deviceAssignmentCard > #swRunning,
  #deviceAssignmentCard > #swFaces,
  #deviceAssignmentCard > #ppGoChild,
  #deviceAssignmentCard > #swAddProfileBtn {
    grid-column: 1;
    grid-row: auto;
  }
}

/* ========================================================================
   DEVICES PHONE ENTRY · 2026-09-04

   The inventory itself is compact now; the remaining delay was page chrome:
   three unrelated actions, three long task pills and account-session help all
   arrived before the first device. Keep the reference screen's two household
   actions in the header, put account exit back in the global Menu where it is
   available from every screen, and use one native task chooser where the
   translated tab labels have only a phone column to share. No panel or action
   is removed; selectDevicePanel() owns both presentations of the same state. */

.device-task-select { display: none; }

/* RETIRED 2026-09-09 BY THE CONVERSION, not by a preference. This block
   existed because `#deviceTabs` was a `.chip` strip and the generic chip rule
   only paints `aria-pressed`, so a tablist had no selected state at all. The
   strip is `.tabs`/`.tab` now and base.css paints `aria-selected` directly, in
   the reference's own shape (a raised pane, `--dusk2` + an inset `--track`
   ring) rather than the solid `--glow` fill a chip needs to read as pressed.
   Deleting it also means the selected TAB no longer wears the same saturated
   cyan as `Add device`, the screen's one primary action — PARITY-DECISIONS
   §3's "colour carries meaning": cyan is trusted+primary, and navigation
   state is neither. */

/* Add is the constructive doorway; Pause all is the coral safety action.
   Both retain the shared button vocabulary and minimum target size. */
#devices #addAnotherDeviceLink {
  border-color: transparent;
  border-radius: var(--radius-pill);
  background: var(--primary-cta-gradient);
  color: var(--primary-cta-text);
  font-weight: 750;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--glow) 18%, transparent);
}
:root[data-theme="light"] #devices #addAnotherDeviceLink { color: var(--primary-cta-text); }

@media (max-width: 899px) {
  /* Account exit already lives in the global Menu at this breakpoint. Keeping
     a second copy only on Devices made an account action look device-scoped. */
  #devices .ovhead > #signoutBtn { display: none; }
}

/* THE ONE GAP THE PRIMITIVE'S OWN BREAKPOINT DOES NOT COVER HERE. base.css
   tightens `.tab` to 12px/4px padding at <=600px — the reference's second stop
   — but THIS strip is hidden at <=620px and handed to #deviceTaskSelect, so it
   is first on screen at 621px, where the tightening has already stopped
   applying. MEASURED after the 2026-09-09 conversion, sweeping 621..1000px in
   all three languages: French "Association manuelle" is clipped by 4px from
   621px to 699px; en and es never clip. Three equal tracks and
   `white-space: nowrap` cut it where the old wrapping `.chips` row would have
   reflowed. Keyed on this screen because the 620px hand-off is this screen's,
   not the primitive's.

   KEYED ON `[role="tab"]`, NOT ON `.tab`. base.css owns that class name and no
   other stylesheet may write it (PORTAL-LAYOUT-LAYER §5); the first version of
   this rule said `.tab` and base-css-isolation.test.js failed it — the second
   time in one change that naming a base.css class in app.css looked harmless.
   The attribute is what the markup already carries and what the primitive
   itself keys its selected state on. */
@media (max-width: 700px) {
  #devices .device-tabs [role="tab"] { padding-inline: 6px; font-size: 12px; }
}

@media (max-width: 620px) {
  #devices .ovhead { gap: 10px; }
  #devices .ovhead > #devTopActions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    flex: 1 0 100%;
    width: 100%;
    gap: 8px;
  }
  #devices #devTopActions > #addAnotherDeviceLink,
  #devices #devTopActions > #pauseAllBtn {
    width: 100%;
    min-height: 44px;
    padding: 8px 10px;
  }
  #devices #devTopActions > #pauseAllMsg { grid-column: 1 / -1; }

  #devices .device-tabs { display: none; }
  #devices > .device-task-select { display: block; margin: 0 0 8px; }
  #devices > .device-task-select > .inp {
    min-height: 48px;
    margin: 0;
    padding-block: 10px;
    font-weight: 700;
  }

  /* The page heading and selected task already say Devices. Retain this h2 for
     document structure and assistive technology without printing it a third
     time above the first category. */
  #devicePanelStatus > .device-inventory-title {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  #devices > .devstack > #devicePanelStatus { padding: 16px; }
}

/* ========================================================================
   HOUSEHOLD PROCESSES · COMPACT ACCOUNT SUMMARY · 2026-09-04

   These are three facts about one household, not three separate tasks. Keep
   every count and its qualifying copy, but group them into one scan line so a
   child and the doorway to that child's controls reach the phone's first
   viewport. The scope sentence remains fully visible because the source data
   is profile-wide and must never be presented as device-attributed evidence. */

#householdProcessesMount > .hproc-summary {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk);
  box-shadow: var(--shadow);
}
#householdProcessesMount > .hproc-summary > .ovtile {
  min-width: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
#householdProcessesMount > .hproc-summary > .ovtile + .ovtile {
  border-left: 1px solid var(--line);
}

#householdProcessesMount > .hproc-scope-note {
  grid-column: 1 / -1;
  margin: 2px 2px 4px;
  color: var(--haze);
  font-size: 12.5px;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
#householdProcessesMount > .hproc-scope-note > b {
  margin-right: .4em;
  color: var(--cloud);
}

@media (max-width: 619px) {
  #householdProcessesMount > .hproc-summary > .ovtile {
    padding: 10px 8px;
    overflow-wrap: anywhere;
  }
  #householdProcessesMount > .hproc-summary .ovt-k {
    font-size: 10px;
    line-height: 1.25;
  }
  #householdProcessesMount > .hproc-summary .ovt-v {
    margin-top: 4px;
    font-size: 20px;
    line-height: 1.15;
  }
  #householdProcessesMount > .hproc-summary .ovt-s {
    margin-top: 2px;
    font-size: 10.5px;
    line-height: 1.25;
  }
  #householdProcessesMount .hproc-child-summary { padding-block: 4px; }
}

/* ========================================================================
   PHONE CHROME · IPHONE SE ALIGNMENT · 2026-09-04

   The 375px acceptance viewport previously wrapped the brand and controls
   into two rows: 87px of header, with the 64px language select clipping even
   “English”. The controls fit in one row when their chrome, rather than their
   function, is compact: the product name stays visible, Menu keeps its 44px
   target and accessible name, and the full language names remain selectable.

   The four persistent destinations also need to read as navigation rather
   than four loose words. Their SVGs are decorative children of the existing
   translated buttons, so route behaviour and accessible names are unchanged. */
.navdockicon { display: none; }
/* Inline footer SVGs need an explicit intrinsic box on the desktop rail too.
   Without this, the browser gives a bare SVG its 300x150 fallback size and the
   entire account/action footer is pushed below the viewport. */
.navfooticon {
  display: block;
  width: 16px;
  height: 16px;
  flex: 0 0 16px;
  color: currentColor;
}
.enrol-dismiss-icon { display: none; }

@media (max-width: 420px) {
  body.signed-in #navRow .navbtn {
    gap: 0;
    padding-block: 2px;
    font-size: 9.5px;
    line-height: 1;
    min-height: 52px;
  }
  body.signed-in #navRow .navbtn > .navdockicon {
    width: 18px;
    height: 18px;
    flex-basis: 18px;
  }
  .pagehead {
    flex-wrap: nowrap;
    align-items: center;
    gap: 5px;
    min-height: 58px;
  }
  .pagehead > .brand {
    flex: 0 1 auto;
    min-width: 0;
  }
  .pagehead > .controls {
    flex: 0 0 auto;
    min-width: 0;
    flex-wrap: nowrap;
    gap: 5px;
  }
  body.signed-in .mobilemenubtn {
    width: 44px;
    min-width: 44px;
    min-height: 44px;
  }
  .pagehead .langsel {
    width: 84px;
    max-width: 84px;
    min-height: 44px;
    padding-inline: 7px;
  }

  /* The optional security offer follows a parent from screen to screen. Keep
     both decisions as real 44px targets without making French and Spanish
     squeeze the title into three lines. The translated "Not now" remains the
     button's accessible name; only its phone rendering becomes a close mark. */
  #enrolBanner.is-oneline {
    grid-template-columns: 9px minmax(0, 1fr) auto auto;
    padding-block: 3px;
    column-gap: 6px;
  }
  #enrolBanner.is-oneline #enrolBannerBtn {
    min-height: 44px;
  }
  #enrolBanner.is-oneline #enrolBannerDismiss {
    display: grid;
    grid-column: 4;
    grid-row: 1;
    place-items: center;
    width: 44px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
  }
  #enrolBanner.is-oneline .enrol-dismiss-icon {
    display: inline-block;
    width: 20px;
    height: 20px;
    position: relative;
  }
  #enrolBanner.is-oneline .enrol-dismiss-icon::before,
  #enrolBanner.is-oneline .enrol-dismiss-icon::after {
    content: "";
    position: absolute;
    top: 9px;
    left: 2px;
    width: 16px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
  }
  #enrolBanner.is-oneline .enrol-dismiss-icon::before { transform: rotate(45deg); }
  #enrolBanner.is-oneline .enrol-dismiss-icon::after { transform: rotate(-45deg); }
  #enrolBanner.is-oneline .enrol-dismiss-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* A 320px layout still keeps every control and full language name on the same
   line. At that exceptional width the wordmark carries the identity by itself;
   the duplicate decorative mark yields the few pixels the row needs.

   MEASURED, NOT GUESSED: 80px clipped "Français", which needs 82.4px at this
   size — the longest of the three language names, and the one a 320px French
   phone would be left reading as "Franc,ai". 88px clears it with room for the
   select's own chevron, and the wordmark shrinking by a point pays for it so
   the row still fits on one line. The test that caught this measures every
   language at every breakpoint in both themes, which is why a French-only
   overflow surfaced at all rather than shipping. */
@media (max-width: 340px) {
  .pagehead .brandmark { display: none; }
  .pagehead .site-brand-logo { width: 104px; height: 26px; }
  .pagehead .wm { font-size: 14px; }
  .pagehead .langsel { width: 88px; max-width: 88px; }
}

/* THE BUDGET ABOVE IS THE SIGNED-OUT ONE — brand + language select — and it
   is correct for that screen. It is not the signed-in one: jump.js inserts
   #jumpOpenHeader ("Find a control") as the FIRST child of .pagehead
   .controls on every signed-in screen, so the signed-in row is actually
   brand + jump button + Menu, and this query predates that insert entirely
   (jump.js's own header comment: "available from every signed-in screen").
   `.controls` is `flex: 0 0 auto` (never shrinks) while `.wm` is
   `white-space: nowrap` with `overflow: visible`, so the two fixed-size
   buttons simply took whatever width they needed and the wordmark bled
   into whatever was left.
   MEASURED at 320x568, signed in, before this rule: h1 right edge at
   x=97.1px, .controls left edge at x=91.7px — 5.4px of the wordmark
   ("...f") painted under the button. This shrinks the two SIGNED-IN
   buttons' own padding/type instead of the wordmark again (already at its
   floor above), which reopens real room rather than hiding the same
   overflow smaller.
   NOT JUST ENGLISH: jump.js's own button label is translated ("Buscar un
   control" / "Trouver un contrôle"), and both are LONGER than "Find a
   control" — an earlier version of this rule (padding-inline:9px,
   font-size:13px) passed in English while actually leaving es at -3.9px
   and fr at -17.5px of overlap, worse than the English bug this rule
   exists to fix. These values, together with .mobilemenubtn's own <=340px
   budget further down this file, were sized against all three shipped
   locales, not just English — see the es/fr cases in
   portal-usability-round4-billing-nav-header.test.js. */
@media (max-width: 340px) {
  body.signed-in .pagehead #jumpOpenHeader {
    padding-inline: 6px;
    font-size: 11px;
  }
  body.signed-in .pagehead > .controls { gap: 4px; }
  /* .mobilemenubtn's own narrower budget lives further down this file,
     AFTER the equal-specificity `@media (max-width: 899px) body.signed-in
     .mobilemenubtn` rule that already sets its padding/gap/font-size — a
     copy here would lose that tie on source order alone and silently do
     nothing, which is exactly what happened the first time this was
     written inline at this spot. */
}

@media (max-width: 899px) {
  /* The mobile rule near the dock's layout resets current text to --ink with
     greater specificity than the shared light-theme rule. White is 4.65:1 on
     the selected cyan fill; the dark ink it replaced was only 4.08:1. */
  :root[data-theme="light"] body.signed-in #navRow .navbtn[aria-current="page"] {
    color: #FFFFFF;
  }

  body.signed-in #navRow .navbtn {
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding-block: 5px;
  }
  body.signed-in #navRow .navbtn > .navdockicon {
    display: block;
    order: -1;
    width: 21px;
    height: 21px;
    flex: 0 0 21px;
    overflow: visible;
    pointer-events: none;
  }
  body.signed-in #navRow .navbtn[aria-current="page"] > .navdockicon {
    stroke-width: 2;
  }

  /* The expanded Menu is a list, not a dock. Restore its horizontal rhythm
     while keeping the same icons beside the four destinations that have one. */
  body.signed-in #nav.mobile-open #navRow .navbtn {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    padding-block: 10px;
  }
  body.signed-in #nav.mobile-open #navRow .navbtn > .navdockicon {
    width: 19px;
    height: 19px;
    flex-basis: 19px;
  }
}

/* ========================================================================
   DEVICES · ONE COMPACT PHONE DECISION · 2026-09-04

   A collapsed inventory tile now spends its space on the four facts a parent
   scans: device, child, connection and protection. The identity button is the
   single Settings doorway; Pause/Resume stays beside it in the default path;
   four signal destinations and all troubleshooting facts share one disclosure.
   Nothing is hidden when that disclosure opens. */

#devices .devcard > .devactions { order: 2; }
#devices .devcard > .healthcap { order: 3; }
#devices .devcard .devactions:empty { display: none; }
#devices .devcard .devactions > .devlockbtn:only-child { grid-column: 1 / -1; }

/* Technical facts are the final full-width section of the same disclosure as
   the four independently actionable protection signals. */
#devices .devcard details.healthcap > .devfacts-body {
  grid-column: 1 / -1;
  margin-top: 2px;
  padding: 10px 6px 2px;
  border-top: 1px solid var(--line);
}

@media (max-width: 420px) {
  #devices .devrow.devcard {
    gap: 6px;
    padding: 10px;
  }

  /* Settings only shares the NAME row. Letting its column narrow the platform,
     child and connection lines made Spanish grow a card by another full line.
     The whole band remains the native button and comfortably over 44px. */
  #devices .devcard .devbtn {
    position: relative;
    display: block;
    min-height: 60px;
    padding: 0;
  }
  #devices .devcard .devicon {
    display: none;
  }
  #devices .devcard .devnm {
    width: 100%;
  }
  #devices .devcard .devopen {
    position: absolute;
    inset: 0 0 auto auto;
    min-height: 32px;
    margin: 0;
    padding: 6px 8px;
  }
  #devices .devcard .devnmrow {
    flex-wrap: nowrap;
    padding-right: 82px;
  }
  /* The folded Details row already carries the same roll-up state. Keeping a
     second badge beside the name costs a full line in French ("Non protégé")
     and makes the device harder to identify on a 375px screen. */
  #devices .devcard .devnmrow > .pill { display: none; }
  #devices .devcard .devnmtext {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    white-space: normal;
    overflow: hidden;
    overflow-wrap: anywhere;
  }
  #devices .devcard .devplat {
    margin-bottom: 1px;
    font-size: 9.5px;
    letter-spacing: .07em;
  }
  #devices .devcard .devcontext {
    gap: 3px 6px;
    margin-top: 3px;
    font-size: 11.5px;
    line-height: 1.2;
  }
  #devices .devcard .devchild { padding: 3px 7px; }
  #devices .devcard .devchild + .devconnection::before { margin-right: 6px; }

  #devices .devcard .devactions { column-gap: 6px; row-gap: 3px; }
  #devices .devcard .devactions > button { min-height: 44px; }
  #devices .devcard details.healthcap > .healthsummary { min-height: 44px; }
}

/* ========================================================================
   CHILD OVERVIEW · IPHONE SE DEVICE CHOOSER · 2026-09-04

   A child can own several devices, but the Overview controls act on one. On
   phones, show that choice as one calm summary and reveal the existing device
   chips only when a parent asks to change it. Desktop keeps the full chip row. */

.fc-device-current { display: none; }
.fc-device-options {
  flex: 1 1 100%;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.fc-device-options > .chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

@media (max-width: 560px) {
  .screen > .fc-chips {
    display: block;
    margin-bottom: 8px;
    padding: 0;
    overflow: hidden;
  }

  /* The same meaning is visible inside the selected-device button; retain the
     original group label for assistive technology without spending a row. */
  .screen > .fc-chips > .fc-chiplabel {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .fc-device-current {
    display: grid;
    grid-template-columns: 7px minmax(0, 1fr) auto;
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 56px;
    margin: 0;
    padding: 8px 10px;
    border: 0;
    border-radius: 13px;
    background: transparent;
    color: var(--cloud);
    text-align: left;
  }
  .fc-device-current > .fc-dot { grid-column: 1; }
  .fc-device-current-copy {
    grid-column: 2;
    min-width: 0;
    display: grid;
    gap: 1px;
  }
  .fc-device-current-kicker {
    color: var(--haze);
    font-size: 10px;
    font-weight: 800;
    line-height: 1.15;
    letter-spacing: .08em;
    text-transform: uppercase;
  }
  .fc-device-current .fc-chipname { max-width: 100%; }
  .fc-device-current-status {
    min-width: 0;
    overflow: hidden;
    color: var(--haze);
    font-size: 11.5px;
    line-height: 1.2;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .fc-device-change {
    grid-column: 3;
    color: var(--glow);
    font-size: 12px;
    font-weight: 800;
    white-space: nowrap;
  }
  :root[data-theme="light"] .fc-device-change { color: #075f80; }

  .fc-device-options {
    display: none;
    flex-wrap: nowrap;
    gap: 6px;
    padding: 8px;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    border-top: 1px solid var(--line);
    -webkit-overflow-scrolling: touch;
  }
  .fc-device-options::-webkit-scrollbar { display: none; }
  .fc-chips[data-picker-open="true"] > .fc-device-options,
  .fc-chips:not(:has(> .fc-device-current)) > .fc-device-options {
    display: flex;
  }
  .fc-device-options > .chip {
    flex: 0 0 auto;
    min-height: 44px;
    scroll-snap-align: start;
  }
  .fc-chips:not(:has(> .fc-device-current)) > .fc-device-options {
    border-top: 0;
  }
  .fc-chips:not(:has(> .fc-device-current)) > .chip {
    min-height: 44px;
    margin: 8px;
  }

  .screen > .subnav .subnavbtn { min-height: 44px; }
}

/* ========================================================================
   PARENTTAP · IPHONE SE INBOX · 2026-09-04

   This screen is already inside #parenttap > .card. Its module used to add a
   second card and then place every part of a request in generic `.arow`'s one
   horizontal flex line. At 375px that left roughly 251px for the child, the
   voice note and four decisions together. One surface, one container, and one
   vertical request rhythm keeps the child identity distinct from the action. */

.screen[tabindex="-1"]:focus { outline: none; }

#parenttap > .card {
  grid-column: 1 / -1;
  width: min(100%, 900px);
  margin-inline: auto;
}
#parenttapMount,
.ptapPanel,
.ptapList,
.ptapRow,
.ptapWho,
.ptapVoice,
.ptapReplyBox { min-width: 0; }

.ptapScreenHead {
  display: flex;
  align-items: center;
  gap: 10px;
}
.ptapBack {
  flex: none;
  min-height: 44px;
  margin: 0;
  white-space: nowrap;
}
.ptapTitle {
  flex: 1;
  min-width: 0;
  margin: 0;
  color: var(--cloud);
  font-size: clamp(24px, 4vw, 30px);
  line-height: 1.12;
  letter-spacing: -.025em;
}
.ptapCount { padding: 5px 9px; }
.ptapIntro {
  max-width: 620px;
  margin: 8px 0 18px;
  font-size: 13.5px;
  line-height: 1.5;
}

.ptapEmpty {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--dusk2);
}
.ptapEmptyIcon {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex: none;
  border-radius: 13px;
  background: color-mix(in srgb, var(--mint) 15%, transparent);
  color: var(--done-text);
  font-size: 20px;
  font-weight: 900;
}
.ptapEmptyCopy { min-width: 0; }
.ptapEmptyTitle { display: block; color: var(--cloud); font-size: 15px; }
.ptapEmptyCopy > .muted {
  margin: 3px 0 0;
  padding: 0;
  text-align: left;
  line-height: 1.4;
}
.ptapSetupNote {
  margin: 14px 0 0;
  padding: 12px 2px 0;
  border-top: 1px solid var(--line);
  line-height: 1.45;
}

/* HISTORY (GoodQA 0680e4f8, EPIC #405 completion pass) — the same
   border-top-separated-section shape .ptapSetup below already uses, so a
   parent reads "open asks, then resolved history, then setup" as three
   visually consistent sections rather than one native list and two
   differently-styled afterthoughts. Rows inside .ptapHistoryList are real
   .ptapRow cards (requestCard is reused verbatim — see historySection's own
   comment in parenttap.js), so nothing here needs to restyle .arow/.ptapRow
   itself. */
.ptapHistory {
  margin: 18px 0 0;
  padding: 14px 0 0;
  border-top: 1px solid var(--line);
}
.ptapHistoryTitle {
  margin: 0;
  font-size: 17px;
  color: var(--cloud);
}
.ptapHistoryHelp { margin: 4px 0 12px; line-height: 1.45; }
.ptapHistoryRefresh { margin: 0 0 12px; }
.ptapHistoryList { display: grid; gap: 10px; }
.ptapHistoryLoadMore { margin: 12px 0 0; }

/* REMOTE PARENTTAP SETUP — one row per child, and it must survive 375x667.
   Single column at every width: the row is a name, a switch and a SENTENCE,
   and the sentence is the part that stops the switch lying about the tablet,
   so it is never the part that gets truncated. The switch keeps a 44px
   target — the same floor .ptapActions > .chip holds on a phone. */
.ptapSetup {
  margin: 18px 0 0;
  padding: 14px 0 0;
  border-top: 1px solid var(--line);
}
.ptapSetupTitle {
  margin: 0;
  font-size: 17px;
  color: var(--cloud);
}
.ptapSetupIntro { margin: 4px 0 12px; line-height: 1.45; }
.ptapSetupList { display: grid; gap: 10px; }
.ptapSetupRow {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk2);
  min-width: 0;
}
.ptapSetupHead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  min-width: 0;
}
.ptapSetupName { min-width: 0; overflow-wrap: anywhere; }
.ptapSetupHead > .chip { min-height: 44px; flex: none; }
.ptapSetupStatus { margin: 0; line-height: 1.45; overflow-wrap: anywhere; }
.ptapSetupButtons { margin: 0; padding: 0; text-align: left; line-height: 1.4; }

/* A ParentTap ask has a heading, optional voice body, status and actions. It is
   deliberately not generic `.arow`'s single-line row even though it retains
   that class for the shared surface treatment. */
.arow.ptapRow {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  margin-bottom: 10px;
  padding: 15px;
}
.ptapRow:last-child { margin-bottom: 0; }
.ptapHead {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.ptapIcon {
  display: block;
  width: 44px;
  height: 44px;
  flex: 0 0 44px;
  object-fit: contain;
}
.ptapIconFallback {
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: var(--dusk);
  color: var(--glow);
  font-size: 17px;
}
.ptapWho { display: grid; gap: 2px; }
.ptapChildName {
  color: var(--glow);
  font-size: 10px;
  font-weight: 850;
  line-height: 1.2;
  letter-spacing: .08em;
  text-transform: uppercase;
}
/* Fourth teal site. Same #087EA4-era leftover as .preference-scope. */
:root[data-theme="light"] .ptapChildName { color: var(--brand); }
.ptapWho > .an {
  font-size: 16px;
  font-weight: 750;
  line-height: 1.25;
}
.ptapWho > .asub { margin-top: 1px; font-size: 12px; line-height: 1.3; }

.ptapVoice {
  display: grid;
  gap: 10px;
  padding: 12px;
  border-left: 3px solid var(--glow);
  border-radius: 0 13px 13px 0;
  background: color-mix(in srgb, var(--glow) 6%, var(--dusk));
}
.ptapVoice > p { margin: 0; line-height: 1.45; }
.ptapTranscript { color: var(--cloud); font-size: 14px; }
.ptapAudio {
  display: block;
  width: 100%;
  max-width: 100%;
  min-height: 44px;
}
.ptapPlay { justify-self: start; min-height: 44px; }
.ptapAudioError { margin: 0; }
.ptapAnswered { margin: 0; color: var(--done-text); font-weight: 750; }

.ptapActions {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
}
.ptapActions > .chip,
.ptapReplyBox > .chip {
  width: 100%;
  min-width: 0;
  min-height: 44px;
  margin: 0;
  padding: 9px 8px;
}
.ptapActions > .chip::before { margin-right: 5px; font-weight: 900; }
.ptapYes::before { content: "✓"; }
.ptapNo::before { content: "×"; }
.ptapActions > .chip:nth-child(3)::before { content: "◷"; }
.ptapActions > .chip:nth-child(4)::before { content: "↗"; }
.ptapReplyBox {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
}
.ptapReplyInput {
  grid-column: 1 / -1;
  margin: 0;
}

/* ParentTap shares the Requests destination instead of adding another rail
   item. Keep its doorway to one compact row: title + honest subset count on
   the left, one action on the right. This replaces a three-block card that
   consumed roughly a quarter of an iPhone SE screen before the inbox began. */
#parenttapLinkCard {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px 14px;
  padding: 14px 16px;
}
.ptapLinkCopy { min-width: 0; }
.ptapLinkHead {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px 9px;
}
#parenttapLinkCard .sect { margin: 0; }
#parenttapLinkCard .help {
  margin: 4px 0 0;
  line-height: 1.4;
}
#parenttapLinkWaiting { padding: 3px 7px; }
#parenttapOpenBtn {
  min-height: 44px;
  margin: 0;
  white-space: nowrap;
}

body.signed-in .mobilemenubtn { position: relative; }
body.signed-in .mobilemenubtn > .menuwait {
  position: absolute;
  top: 2px;
  right: 2px;
  display: grid;
  place-items: center;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: var(--amber);
  color: var(--ink);
  font-size: 10px;
  font-weight: 900;
  line-height: 1;
}
:root[data-theme="light"] body.signed-in .mobilemenubtn[aria-expanded="true"] {
  color: #FFFFFF;
}
:root[data-theme="light"] body.signed-in .mobilemenubtn > .menuwait {
  border-color: #C99A37;
  background: #F9E8B7;
  color: #604000;
}

@media (max-width: 480px) {
  #parenttapLinkCard { gap: 9px; padding: 12px 13px; }
  #parenttapLinkCard .help {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }
  #parenttapOpenBtn {
    display: grid;
    place-items: center;
    width: 44px;
    min-width: 44px;
    height: 44px;
    padding: 0;
    font-size: 0;
  }
  #parenttapOpenBtn::after {
    content: "›";
    font-size: 25px;
    font-weight: 800;
    line-height: 1;
  }
  .ptapBack {
    display: grid;
    place-items: center;
    width: 44px;
    min-width: 44px;
    height: 44px;
    padding: 0;
    font-size: 0;
  }
  .ptapBack::before {
    content: "‹";
    font-size: 28px;
    font-weight: 800;
    line-height: 1;
  }
  #parenttap > .card { padding: 14px; }
  .ptapTitle { font-size: 24px; }
  .ptapIntro { margin-bottom: 14px; }
  .arow.ptapRow { gap: 10px; padding: 12px; }
  .ptapActions { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
  .ptapActions > .chip { min-height: 46px; }
  /* 375x667: the name and the switch stack rather than competing for one
     line, so a long child name never squeezes the switch below its 44px
     target or pushes it off the row. */
  .ptapSetupHead { align-items: stretch; flex-direction: column; }
  .ptapSetupHead > .chip { width: 100%; min-height: 46px; }
  .ptapSetupRow { padding: 11px; }
}

@media (max-width: 340px) {
  #parenttap > .card { padding: 12px; }
  .ptapEmpty { align-items: flex-start; padding: 12px; }
  .ptapVoice { padding: 10px; }
}

/* ========================================================================
   DEVICE WORKSPACE · IPHONE SE CHOOSER · 2026-09-04

   Six equal-width translated buttons cannot fit a 320px device workspace;
   the longest French labels overpaint their neighbours and the two-row sticky
   rail consumes more than 100px before the selected task. Keep those buttons
   unchanged on wide screens and synchronize one native 48px chooser on phones.
   The device inventory loses only its redundant outer card shell, not its
   grouping, health disclosure, actions or tabpanel semantics. */

.policy-task-select {
  display: none;
  grid-column: 1 / -1;
  width: 100%;
  min-width: 0;
}
.policy-back-short { display: none; }

@media (max-width: 560px) {
  #devices > .devstack > #devicePanelStatus {
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
  }

  #policy > .policy-tabs { display: none; }
  #policy > .policy-task-select {
    position: sticky;
    top: 8px;
    z-index: 12;
    display: block;
    padding: 5px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: color-mix(in srgb, var(--dusk) 94%, transparent);
    backdrop-filter: blur(14px);
    box-shadow: 0 10px 28px color-mix(in srgb, var(--ink) 24%, transparent);
  }
  #policyTaskSelect {
    width: 100%;
    min-height: 48px;
    margin: 0;
    font-weight: 750;
  }
  #policy [data-policy-section] { scroll-margin-top: 70px; }

  #policy > .policy-head > .policy-back {
    min-width: 0;
    max-width: 100%;
  }
  .policy-back-full { display: none; }
  .policy-back-short {
    display: block;
    max-width: 30vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ========================================================================
   SIGNED-IN CHROME · ONE CALM PHONE HEADER · 2026-09-04

   The header is identity plus Menu. Account utilities live in that Menu at
   phone/tablet widths and in the same footer on the desktop rail. The original
   language select remains the single state source for compatibility, but the
   signed-in control is three compact flag buttons with full accessible names. */

.navlanguage {
  display: grid;
  grid-template-columns: repeat(3, 44px);
  gap: 6px;
  width: max-content;
}
body.signed-in #navLangSlot > #langSel[hidden] { display: none !important; }
.navlangbtn {
  display: grid;
  place-items: center;
  /* 44px is the TOUCH floor and it is right on a phone; in the desktop rail
     footer four 44px boxes plus the theme control were ~180px of chrome under
     a nav whose whole job is to be quiet. Her footer is a single 54px identity
     block and nothing else. Owner, on the live build: "the flags and color
     changer should be way smaller".
     The floor is kept for touch and relaxed only at >=900px with a fine
     pointer — the same rule already used for her 34px buttons and 41px nav
     rows. See the media query below. */
  min-width: 44px;
  min-height: 44px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  color: var(--cloud);
  font: inherit;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.navlangbtn:hover { background: var(--dusk2); }
.navlangflag {
  display: block;
  width: 24px;
  height: 16px;
  border-radius: 2px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--cloud) 24%, transparent);
  overflow: hidden;
  pointer-events: none;
}
.navlangbtn[aria-pressed="true"] {
  border-color: var(--glow);
  background: color-mix(in srgb, var(--glow) 20%, var(--dusk2));
  box-shadow: inset 0 0 0 1px var(--glow);
}
/* Fifth and last teal site. The base rule two blocks up already reads
   var(--glow) and now resolves correctly in light, so this override exists
   only to restate the OLD teal --glow. Kept rather than deleted because the
   base rule's color-mix against --dusk2 gives a near-white fill on a white
   card; this keeps a readable selected state, now in the brand hue. */
:root[data-theme="light"] .navlangbtn[aria-pressed="true"] {
  border-color: var(--brand);
  background: var(--brand-tint);
  box-shadow: inset 0 0 0 1px var(--brand);
}

#startCard {
  margin: 0 0 16px;
  background: color-mix(in srgb, var(--glow) 7%, var(--dusk2));
  box-shadow: none;
}
#startCard .sect { margin-bottom: 6px; }
#startCard .fineprint { max-width: 64ch; margin: 0; }
#startCard #startGet { width: fit-content; margin-top: 12px; }

@media (max-width: 899px) {
  body.signed-in .pagehead {
    flex-wrap: nowrap;
    align-items: center;
    min-height: 58px;
    gap: 8px;
  }
  body.signed-in .pagehead > .brand {
    flex: 1 1 auto;
    min-width: 0;
  }
  body.signed-in .pagehead .wm small { display: none; }
  body.signed-in .pagehead > .controls {
    flex: 0 0 auto;
    min-width: 0;
    flex-wrap: nowrap;
  }
  body.signed-in .pagehead #themeToggle,
  body.signed-in .pagehead #langSel { display: none !important; }
  body.signed-in .mobilemenubtn {
    width: auto;
    min-width: 0;
    min-height: 44px;
    padding: 8px 12px;
    gap: 7px;
    font-size: 13px;
    line-height: 1;
    white-space: nowrap;
  }
  body.signed-in .mobilemenubtn::before {
    content: "☰";
    font-size: 17px;
    line-height: 1;
  }

  body.signed-in #nav.mobile-open .navfootbtn {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 44px;
    box-sizing: border-box;
    padding: 9px 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: transparent;
    color: var(--cloud);
    font: inherit;
    font-size: 13px;
    font-weight: 650;
    text-align: left;
  }
  body.signed-in #nav.mobile-open .navfootbtn:hover { background: var(--dusk2); }
  body.signed-in #nav.mobile-open .navtheme .nf-ic { flex: none; color: var(--haze); }
  body.signed-in #nav.mobile-open .navtheme .nf-label { flex: 1 1 auto; }
  body.signed-in #nav.mobile-open .navtheme .nf-val { flex: none; color: var(--haze); }
  body.signed-in #nav.mobile-open .navpause {
    color: var(--coral);
    border-color: color-mix(in srgb, var(--coral) 48%, var(--line));
  }
  body.signed-in #nav.mobile-open .navsignout { justify-content: center; color: var(--haze); }
  body.signed-in #nav.mobile-open #navLangSlot { width: 100%; }
  body.signed-in #nav.mobile-open .navlanguage { display: grid; }

  #startCard #startGet { width: 100%; }
}

/* The rest of the SIGNED-IN header's own <=340px budget (see the comment
   beside `#jumpOpenHeader` above) — placed AFTER the `@media (max-width:
   899px) body.signed-in .mobilemenubtn` rule just above on purpose: that
   rule and this one set the same properties at EQUAL selector specificity,
   so whichever comes later in the file is the one that actually wins,
   whatever either media query's own width number says. */
@media (max-width: 340px) {
  body.signed-in .mobilemenubtn {
    padding: 6px;
    gap: 3px;
    font-size: 11px;
  }
}

/* The signed-in phone header must budget for the visible PNG lockup, not the
   now screen-reader-only .wm text. The later <=899px Menu rule restored its
   full text width, which clipped the logo under Spanish/French jump labels at
   375px. Keep Menu's accessible text in the DOM and show its familiar glyph
   in a 44px target, so the full logo and Find a control remain visible. */
@media (max-width: 420px) {
  body.signed-in .mobilemenubtn {
    width: 44px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    gap: 0;
    font-size: 0;
  }
}

/* ========================================================================
   PHONE MENU · A REAL LIST, NOT A WRAPPED CHIP CLOUD

   The compact dock and the expanded menu share #navRow, but they do not share
   a layout job. In the open sheet, two-up translated chips produced uneven
   rows, squeezed live badges into labels, and pushed the third language flag
   outside a 320px viewport. The sheet now uses the whole space below the
   header and gives every destination one predictable icon/label/value row. */
@media (max-width: 899px) {
  body.signed-in #nav.mobile-open {
    top: calc(max(18px, env(safe-area-inset-top, 0px)) + 59px);
    right: max(12px, env(safe-area-inset-right, 0px));
    bottom: max(10px, env(safe-area-inset-bottom, 0px));
    left: auto;
    width: min(420px, calc(100vw - max(12px, env(safe-area-inset-left, 0px)) - max(12px, env(safe-area-inset-right, 0px))));
    max-height: none;
    box-sizing: border-box;
    padding: 10px;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
  }

  /* The header already carries the product identity. Repeating the logo in the
     sheet spent a full row and made the first destination look detached. */
  body.signed-in #nav.mobile-open .navbrand { display: none; }

  body.signed-in #nav.mobile-open #navRow,
  body.signed-in #nav.mobile-open #navRow #navChildren,
  body.signed-in #nav.mobile-open #navRow #navHousehold {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 2px;
    width: 100%;
  }

  body.signed-in #nav.mobile-open #navRow .navlabel {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    min-height: 26px;
    margin: 5px 0 1px;
    padding: 5px 8px 3px;
    border-top: 1px solid var(--line);
    color: var(--haze);
    font-size: 10px;
    font-weight: 850;
    letter-spacing: .08em;
    line-height: 1;
    text-transform: uppercase;
  }
  body.signed-in #nav.mobile-open #navRow #navChildrenLabel { color: var(--attention-text); }

  body.signed-in #nav.mobile-open #navRow .navbtn,
  body.signed-in #nav.mobile-open #navRow .navadd,
  body.signed-in #nav.mobile-open #navRow #navHousehold > .navbtn,
  body.signed-in #nav.mobile-open #navRow #navChildren > .navbtn {
    display: flex;
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
    min-height: 44px;
    box-sizing: border-box;
    padding: 8px 10px;
    border-color: transparent;
    border-radius: 10px;
    background: transparent;
    color: var(--cloud);
    font-size: 13px;
    font-weight: 700;
    line-height: 1.2;
    text-align: left;
    white-space: normal;
  }
  body.signed-in #nav.mobile-open #navRow .navbtn:hover,
  body.signed-in #nav.mobile-open #navRow .navadd:hover { background: var(--dusk2); }
  body.signed-in #nav.mobile-open #navRow .navbtn[aria-current="page"] {
    border-color: color-mix(in srgb, var(--glow) 42%, var(--line));
    background: color-mix(in srgb, var(--glow) 18%, var(--dusk2));
    color: var(--cloud);
  }
  :root[data-theme="light"] body.signed-in #nav.mobile-open #navRow .navbtn[aria-current="page"] {
    color: var(--ink);
  }

  body.signed-in #nav.mobile-open #navRow .navbtn > .navdockicon,
  body.signed-in #nav.mobile-open #navRow .navadd > .navdockicon {
    display: block;
    order: -1;
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
    color: var(--haze);
    pointer-events: none;
  }
  body.signed-in #nav.mobile-open #navRow .navbtn[aria-current="page"] > .navdockicon {
    color: var(--glow);
  }
  body.signed-in #nav.mobile-open #navRow .navbtn > .naventrylabel,
  body.signed-in #nav.mobile-open #navRow .navadd > .naventrylabel {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  body.signed-in #nav.mobile-open #navRow .navchild {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    min-height: 54px;
    border-color: color-mix(in srgb, var(--sky) 24%, var(--line));
    background: color-mix(in srgb, var(--sky) 7%, transparent);
  }

  /* Requests owns a trailing value column. It never participates in the label
     flow, so both the count and the useful state word fit without touching the
     destination label. */
  body.signed-in #nav.mobile-open #navRow .navbtn > .navbadge,
  body.signed-in #nav.mobile-open #navRow [data-screen="requests"] > .pill.wait {
    position: static;
    flex: 0 0 auto;
    min-width: 24px;
    margin-left: auto;
    padding: 3px 7px;
    line-height: 1.2;
    text-align: center;
  }
  body.signed-in #nav.mobile-open .navfoot {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 8px;
    padding-top: 8px;
  }
  body.signed-in #nav.mobile-open .navfootlook {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr);
    align-items: center;
    gap: 6px;
    width: 100%;
    min-width: 0;
  }
  body.signed-in #nav.mobile-open .navtheme {
    display: grid;
    place-items: center;
    width: 44px;
    min-width: 44px;
    height: 44px;
    min-height: 44px;
    padding: 0;
  }
  body.signed-in #nav.mobile-open .navtheme .nf-label,
  body.signed-in #nav.mobile-open .navtheme .nf-val { display: none; }
  body.signed-in #nav.mobile-open .navtheme .nf-ic,
  body.signed-in #nav.mobile-open .navtheme > .navdockicon {
    display: block;
    width: 20px;
    height: 20px;
    margin: 0;
  }
  body.signed-in #nav.mobile-open #navLangSlot,
  body.signed-in #nav.mobile-open .navlanguage {
    width: 100%;
    min-width: 0;
  }
  body.signed-in #nav.mobile-open .navlanguage {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 6px;
  }
  body.signed-in #nav.mobile-open .navlangbtn {
    width: 100%;
    min-width: 0;
  }

  body.signed-in #nav.mobile-open .navfoot .navuser {
    display: grid;
    grid-template-columns: 32px minmax(0, 1fr);
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 44px;
    box-sizing: border-box;
    margin: 2px 0 0;
    padding: 5px 8px;
  }
  body.signed-in #nav.mobile-open .navfoot .navuser .nu-mail {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  body.signed-in #nav.mobile-open .navpause,
  body.signed-in #nav.mobile-open .navsignout { justify-content: flex-start; }
  body.signed-in #nav.mobile-open .navfootbtn > .navdockicon,
  body.signed-in #nav.mobile-open .navfootbtn > .navfooticon {
    display: block;
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
  }
}

@media (max-width: 340px) {
  body.signed-in #nav.mobile-open {
    right: max(8px, env(safe-area-inset-right, 0px));
    width: calc(100vw - max(8px, env(safe-area-inset-left, 0px)) - max(8px, env(safe-area-inset-right, 0px)));
  }
}

/* ========================================================================
   DEVICES · CALM INVENTORY LIST · 2026-09-05

   Device cards are records, not a tile gallery. One full-width row keeps the
   identity, primary Settings doorway and secondary actions aligned, and gives
   the optional detail disclosure the entire inventory column when it opens.
   The content canvas stays bounded on 4K rather than stretching indefinitely. */

#devices .devgroup > .devgroupbody {
  width: 100%;
  max-width: 1120px;
  margin: 10px auto 0;
  grid-template-columns: minmax(0, 1fr);
}
#devices .devgroup > .devgroupbody > .devcard,
#devices .devgroup > .devgroupbody > .devcard:only-child {
  grid-column: 1;
  width: 100%;
  max-width: none;
  min-width: 0;
  box-sizing: border-box;
  justify-self: stretch;
}
#devices .devcard > .healthcap { min-width: 0; }
#devices .devcard details.healthcap[open] {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
}

@media (min-width: 561px) {
  /* One dependable header grid: icon, identity/status, primary doorway. Long
     names may wrap in their own column without pushing Settings off the row. */
  #devices .devcard .devbtn {
    display: grid;
    grid-template-columns: 40px minmax(0, 1fr) auto;
    align-items: center;
    gap: 12px;
    width: 100%;
  }
  #devices .devcard .devicon {
    grid-column: 1;
    grid-row: 1;
  }
  #devices .devcard .devnm {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
  }
  #devices .devcard .devopen {
    grid-column: 3;
    grid-row: 1;
    min-height: 44px;
    padding-inline: 14px;
  }

  /* Secondary actions share one even row. Three released maintenance actions
     get three equal tracks instead of the old two-plus-orphan pattern. */
  #devices .devcard .devactions {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  }
  #devices .devcard .devactions:has(> button:nth-of-type(3)) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  /* A narrow phone gets one predictable action per row. The common live case
     has only Pause/Resume, so its compact height is unchanged. */
  #devices .devcard .devactions { grid-template-columns: minmax(0, 1fr); }
  #devices .devcard .devactions > button { grid-column: 1; }
}

/* ========================================================================
   DEVICES · COMPACT SECONDARY FOOTER · 2026-09-05

   Settings is already the primary doorway in the identity row. A single
   Pause/Resume action and the folded health disclosure are supporting tools;
   drawing each as a full-width band made a three-device desktop inventory
   look like six unrelated forms. Keep them in one calm footer while closed,
   then give the diagnostic disclosure the complete row when a parent opens
   it. This is layout only: the native controls, labels, targets, state and
   acknowledgement handling are unchanged. */

#devices .devrow.devcard {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "identity identity"
    "alerts alerts"
    "details actions";
  column-gap: 8px;
  row-gap: 8px;
}
#devices .devcard > .devbtn { grid-area: identity; }
#devices .devcard > .devalerts { grid-area: alerts; }
#devices .devcard > .healthcap {
  grid-area: details;
  align-self: stretch;
}
#devices .devcard > .devactions {
  grid-area: actions;
  display: flex;
  align-items: stretch;
  justify-content: flex-end;
  gap: 8px;
}
#devices .devcard > .devactions > button {
  width: auto;
  min-width: 112px;
  padding-inline: 16px;
}
#devices .devcard:has(> .devactions:empty) > .healthcap {
  grid-column: 1 / -1;
}
#devices .devcard:has(> details.healthcap[open]) {
  grid-template-areas:
    "identity identity"
    "alerts alerts"
    "details details"
    "actions actions";
}
#devices .devcard:has(> details.healthcap[open]) > .devactions {
  justify-content: flex-end;
}

@media (max-width: 560px) {
  #devices .devrow.devcard {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  #devices .devcard > .devactions > button {
    min-width: 92px;
    padding-inline: 12px;
  }
  #devices .devcard > .devactions > .device-menu-trigger {
    min-width: 54px;
    padding-inline: 8px;
  }
  /* More than one secondary action will not fit beside a translated details
     summary on an iPhone SE. Put that uncommon maintenance case on its own
     row, but keep the ordinary one-action device card compact. */
  #devices .devcard:has(> .devactions > button:nth-of-type(3)) {
    grid-template-areas:
      "identity identity"
      "alerts alerts"
      "details details"
      "actions actions";
  }
  #devices .devcard:has(> .devactions > button:nth-of-type(3)) > .devactions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  #devices .devcard:has(> .devactions > button:nth-of-type(3)) > .devactions > button {
    width: 100%;
    min-width: 0;
  }
}

/* ========================================================================
   DEVICES · RECORD LIST + READABLE DIAGNOSTICS · 2026-09-05

   The inventory is one list per hardware family, not a card nested inside a
   card for every device.  Identity and the Settings doorway stay first; child
   assignment is visually distinct from connection state; and the optional
   health/details disclosure owns the full record width while open. */

#devices > .devstack > #devicePanelStatus {
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
}
#devices #devicePanelStatus > .device-inventory-title { margin: 2px 0 12px; }

#devices .devgroup,
#devices .devgroup:first-child {
  width: min(100%, 1120px);
  box-sizing: border-box;
  margin: 14px auto 0;
  padding: 14px 16px 0;
  overflow: clip;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: var(--dusk);
  box-shadow: var(--shadow);
}
#devices .devgroup:first-child { margin-top: 0; }
#devices .devgroup > .devgrouphead {
  min-height: 48px;
  padding: 0 0 13px;
}
#devices .devgroup > .devgrouphead::before {
  order: 3;
  margin-left: auto;
  font-size: 13px;
}
#devices .devgroupicon,
#devices .devcard .devicon {
  color: var(--glow);
  border: 1px solid color-mix(in srgb, var(--glow) 26%, var(--line));
  background: color-mix(in srgb, var(--glow) 8%, var(--dusk2));
}
#devices .devgroupicon .device-kind-icon {
  width: 17px;
  height: 17px;
}
#devices .devcard .devicon .device-kind-icon {
  width: 20px;
  height: 20px;
}

#devices .devgroup > .devgroupbody {
  width: auto;
  max-width: none;
  margin: 0 -16px;
  gap: 0;
  border-top: 1px solid var(--line);
}
#devices .devgroup > .devgroupbody > .devcard,
#devices .devgroup > .devgroupbody > .devcard:only-child {
  padding: 17px 16px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
#devices .devgroup > .devgroupbody > .devcard + .devcard {
  border-top: 1px solid var(--line);
}
@media (hover: hover) {
  #devices .devgroup > .devgroupbody > .devcard:hover {
    background: color-mix(in srgb, var(--glow) 2.5%, transparent);
  }
}

#devices .devcard .devbtn {
  box-sizing: border-box;
  padding: 6px;
  border-radius: 13px;
}
@media (hover: hover) {
  #devices .devcard .devbtn:hover {
    background: color-mix(in srgb, var(--glow) 6%, transparent);
  }
}
#devices .devcard .devbtn:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 2px;
}
#devices .devcard .devnmtext { font-size: 18px; }

#devices .devcard .devchild {
  gap: 5px;
  padding-inline: 8px 10px;
}
#devices .devcard .devchild > .devchild-icon {
  order: -1;
  width: 14px;
  height: 14px;
}
#devices .devcard .devconnection {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
#devices .devcard .devchild + .devconnection::before {
  width: 7px;
  height: 7px;
  margin: 0;
  border-radius: 50%;
  background: currentColor;
  content: "";
}

#devices .devcard .devopen {
  gap: 6px;
  min-height: 44px;
  padding: 9px 12px;
}
#devices .devcard .devopen::after { content: none; }
#devices .devcard .devopen > .devopen-icon {
  order: -1;
  width: 16px;
  height: 16px;
}

#devices .devcard > .devactions {
  align-items: center;
  flex-wrap: wrap;
}
#devices .devcard > .devactions > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
#devices .devcard .devaction-icon {
  order: -1;
  width: 16px;
  height: 16px;
}
/* A pending/offline delivery sentence belongs to the whole record, not inside
   the narrow Pause button column. `display: contents` lets that existing node
   take a full grid row without changing the tested identity/details/actions
   sibling structure or moving any acknowledgement state in the DOM. */
#devices .devcard:has(> .devactions > .devlockstate:not(:empty)) {
  grid-template-areas:
    "identity identity"
    "alerts alerts"
    "details actions"
    "delivery delivery";
}
/* DEF-005. The rule above is un-media'd, so on a phone it silently overrides
   the stacked three-button layout below whenever a card is ALSO showing a
   pending/offline delivery sentence, splitting "Resume" into "Resu"/"me".
   A paused, unacknowledged device hits both rules — this one must win. */
@media (max-width: 560px) {
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)):has(> .devactions > button:nth-of-type(3)) {
    grid-template-areas:
      "identity identity"
      "alerts   alerts"
      "details  details"
      "actions  actions"
      "delivery delivery";
  }
}
#devices .devcard > .devactions:has(> .devlockstate:not(:empty)):not(:has(> button:nth-of-type(2))) {
  display: contents;
}
#devices .devcard > .devactions:has(> .devlockstate:not(:empty)):not(:has(> button:nth-of-type(2))) > button {
  grid-area: actions;
}
#devices .devcard > .devactions:has(> .devlockstate:not(:empty)):not(:has(> button:nth-of-type(2))) > .devlockstate {
  grid-area: delivery;
  min-width: 0;
  padding-inline: 2px;
  text-align: left;
}
#devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)) {
  grid-template-areas:
    "identity identity"
    "alerts alerts"
    "details details"
    "actions actions"
    "delivery delivery";
}

#devices .devcard details.healthcap {
  overflow: hidden;
  background: color-mix(in srgb, var(--dusk2) 72%, transparent);
}
#devices .devcard details.healthcap > .healthsummary {
  gap: 9px;
  box-sizing: border-box;
}
#devices .devcard details.healthcap > .healthsummary::before {
  order: 4;
  margin-left: 2px;
}
#devices .devcard .healthsummary-icon {
  width: 17px;
  height: 17px;
  color: var(--glow);
}
#devices .devcard details.healthcap[open] {
  display: block;
  padding: 0 12px 12px;
  background: var(--dusk);
}
#devices .devcard .healthsignals {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
#devices .devcard details.healthcap[open] > .healthsummary {
  margin: 0 -12px 2px;
  padding: 10px 12px;
}
#devices .devcard .healthsectionlabel,
#devices .devcard .devfacts-title {
  grid-column: 1 / -1;
  margin: 2px 0 0;
  color: var(--haze);
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: .09em;
  line-height: 1.25;
  text-transform: uppercase;
}
#devices .devcard .hccell {
  gap: 9px;
  min-height: 56px;
  padding: 8px;
  border-color: var(--line);
  background: var(--dusk2);
}
#devices .devcard .hcdot {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  margin: 0;
  border-radius: 9px;
}
#devices .devcard .hcdot-green {
  color: var(--done-text);
  background: color-mix(in srgb, var(--mint) 13%, var(--dusk));
}
#devices .devcard .hcdot-amber {
  color: var(--waiting-text);
  background: color-mix(in srgb, var(--amber) 13%, var(--dusk));
}
#devices .devcard .hcdot-red {
  color: #FFB4AC;
  background: color-mix(in srgb, var(--coral) 13%, var(--dusk));
}
#devices .devcard .healthsignal-icon {
  width: 16px;
  height: 16px;
}
#devices .devcard .hcname { color: var(--cloud); }
#devices .devcard details.healthcap > .devfacts-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
  gap: 8px 18px;
  margin: 2px 0 0;
  padding: 12px 2px 2px;
}
#devices .devcard .devfact {
  position: relative;
  padding-left: 13px;
}
#devices .devcard .devfact::before {
  position: absolute;
  top: .52em;
  left: 1px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--haze);
  content: "";
}

@media (min-width: 900px) {
  #devices .devgroup > .devgrouphead { padding-right: 280px; }
  #devices .devgroup > .chips { top: 14px; right: 16px; }
}

@media (max-width: 620px) {
  #devices .devgroup,
  #devices .devgroup:first-child {
    padding: 11px 11px 0;
    border-radius: 15px;
  }
  #devices .devgroup > .devgroupbody { margin-inline: -11px; }
  #devices .devgroup > .devgroupbody > .devcard,
  #devices .devgroup > .devgroupbody > .devcard:only-child { padding: 13px 11px; }
  #devices .devcard .healthsignals {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
  }
  #devices .devcard details.healthcap[open] {
    padding-inline: 9px;
  }
  #devices .devcard details.healthcap[open] > .healthsummary {
    margin-inline: -9px;
    padding-inline: 10px;
  }
}

@media (max-width: 420px) {
  #devices .devgroup > .devgroupbody > .devcard,
  #devices .devgroup > .devgroupbody > .devcard:only-child {
    padding-block: 10px;
  }
  #devices .devrow.devcard { row-gap: 6px; }
  /* Two columns, never three. The 2026-09-05 inventory polish gave Settings a
     grid column of its own beside the WHOLE identity block, which is exactly
     the layout the ≤420px rules further up retired, and for the same reason
     recorded in their comment: a third column narrows the platform, child and
     connection lines, so Spanish wrapped two of them and every device card
     grew past the iPhone SE density budget. Keep the device glyph that polish
     restored — the card should still say what kind of hardware this is — but
     take Settings back out of flow so it shares the NAME row only. */
  #devices .devcard .devbtn {
    position: relative;
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr);
    align-items: start;
    column-gap: 8px;
    min-height: 64px;
    padding: 4px;
  }
  #devices .devcard .devicon {
    display: grid;
    grid-column: 1;
    grid-row: 1;
    width: 34px;
    height: 34px;
  }
  #devices .devcard .devicon .device-kind-icon {
    width: 17px;
    height: 17px;
  }
  #devices .devcard .devnm {
    grid-column: 2;
    grid-row: 1;
    width: auto;
  }
  /* The name row — and only the name row — reserves the space the lifted
     Settings label occupies, so nothing beneath it can run under the pill. */
  #devices .devcard .devnmrow {
    align-items: center;
    min-height: 30px;
    padding-right: 96px;
  }
  #devices .devcard .devopen {
    position: absolute;
    grid-column: auto;
    grid-row: auto;
    inset: 4px 4px auto auto;
    min-width: 44px;
    min-height: 30px;
    margin: 0;
    padding: 6px 9px;
  }
  #devices .devcard .devnmtext { font-size: 16px; }
  #devices .devcard .devcontext {
    align-items: flex-start;
    margin-top: 5px;
  }
  #devices .devcard .devchild {
    white-space: normal;
    overflow-wrap: anywhere;
  }
  #devices .devcard .healthsummary-label { font-size: 11.5px; }
  #devices .devcard .healthsummary-state { font-size: 11px; }
  #devices .devcard .hccell {
    min-height: 60px;
    padding: 7px;
  }
}

/* Device workspace follow-up: preserve clear groups on narrow screens and
   keep every visible secondary action comfortably tappable. */
#screenshotCard > a.fineprint {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: 8px;
}

@media (max-width: 899px) {
  #protectionSettingsCard {
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
  }
  #protectionSettingsCard > .sect,
  #protectionSettingsCard > .help { margin-inline: 4px; }
  .protection-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "defaults" "web" "device";
    gap: 12px;
    margin-top: 12px;
  }
  .protection-group { padding: 14px; }
}

@media (max-width: 560px) {
  /* The full Schedules card is the phone doorway; repeating it one card above
     made an empty week look like two different schedule creators. */
  #timeWeekAddScheduleBtn { display: none; }
}

.protection-details {
  margin-top: 10px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}
.protection-details > summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--glow);
  cursor: pointer;
  font-size: 13px;
  font-weight: 800;
}
.protection-details[open] > summary { margin-bottom: 8px; }

/* ==========================================================================
   THE SCALABLE DEVICE LIST — LANE E, 2026-09-06
   ==========================================================================
   Appended at the end of the file, the convention every feature block above
   already follows: purely ADDITIVE selectors, so a merge with concurrent work
   on another screen is a clean append on both sides.

   WHAT WAS WRONG. #devList drew every device a household owns, in two
   collapsible groups, always — no search, no sort, no status filter, no paging
   — and there was no way to see more about one device without navigating off
   the list entirely. This block styles the toolbar, the pager and the
   full-width expanded detail that platform/public/js/devices.js now builds.

   NO NEW HEX AND NO NEW COLOUR MEANINGS (UI-DIRECTION-2026-08-22 §3). Every
   value below is one of this file's own tokens, so both themes are correct by
   construction: light is reached through :root[data-theme="light"] (and
   :root:not([data-theme]) before the toggle has run), which re-points --dusk2,
   --line, --haze and --cloud, and nothing here hard-codes any of them.

   TOUCH TARGETS. `.inp` is already min-height 48px; the two buttons this block
   introduces carry their own 44px floor rather than inheriting one, because
   `.devactionbtn` and `.ghost` are sized for a card, not for a pager.

   NO HORIZONTAL PAGE SCROLL AT 320px. Nothing here has a fixed width: the
   toolbar wraps, the provenance grid is one column until 640px, and every cell
   that can carry a long device or child name breaks inside the word rather
   than pushing the page wider. */

/* ---- the toolbar: search, filter, sort, and what is being shown ---------- */
.devtools { margin: 0 0 12px; }
.devtools-summary { display: none; }
@media (max-width: 559px) {
  #devices .devtools { margin-bottom: 8px; }
  #devices .devtools-summary {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--dusk);
    color: var(--cloud);
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
  }
  #devices .devtools-summary::-webkit-details-marker { display: none; }
  #devices .devtools-summary::before {
    content: "▸";
    margin-right: 8px;
    color: var(--haze);
  }
  #devices .devtools-disclosure[open] > .devtools-summary::before { content: "▾"; }
  #devices .devtools-row { margin-top: 8px; }
  #devices .devtools-count { margin-top: 4px; }
}

/* Phone entry hierarchy for households with enough devices to need list
   management: keep safety notices and all actions, but do not force a parent
   to scroll past repeated introductory copy and three optional controls. */
@media (max-width: 559px) {
  #devices:has(#devListTools) .ovhead .ov-eyebrow,
  #devices:has(#devListTools) .ovhead .ov-sub { display: none; }
  #devices:has(#devListTools) .ovhead { gap: 4px; margin-bottom: 0; }
  #devices:has(#devListTools) .ovhead .ov-title { margin: 0; }
  #devices .ovhead > #devTopActions { gap: 6px; }
  #devices > .device-task-select { margin-bottom: 2px; }
  #devices > .device-task-select > .inp { min-height: 44px; }
  #devices .devtools-count {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  #devices #devList > .banner.warn {
    padding-block: 7px;
    font-size: 12px;
    line-height: 1.3;
  }
  #devices .devtools { margin-bottom: 4px; }
  #devices .devgroup:has(> .devgroupactions) { position: relative; }
  #devices .devgroup:has(> .devgroupactions) > .devgrouphead { padding-right: 105px; }
  #devices .devgroup > .devgroupactions {
    position: absolute;
    top: 10px;
    right: 16px;
    padding: 0;
    margin: 0;
  }
  #devices .devgroup > .devgroupactions > button {
    min-height: 44px;
    padding: 7px 10px;
    font-size: 12px;
  }
}
.devtools-row {
  display: grid; gap: 8px;
  grid-template-columns: 1fr;
}
/* One column on a phone (three stacked controls a thumb can hit), search over
   the two selects from 560px, and all three in a row once there is room. */
@media (min-width: 560px) {
  .devtools-row { grid-template-columns: 1fr 1fr; }
  .devtools-row > .devtools-search { grid-column: 1 / -1; }
}
@media (min-width: 900px) {
  .devtools-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
  .devtools-row > .devtools-search { grid-column: auto; }
}
.devtools-row > .inp { min-width: 0; }
/* Safari paints a search field with its own inset shadow and rounded ends,
   which reads as a different control from every other input on the page. */
.devtools-search { -webkit-appearance: none; appearance: none; }
.devtools-count { margin: 8px 0 0; font-size: 12px; }
.devtools-count:empty { display: none; }

/* ---- the pager ---------------------------------------------------------- */
.devpager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin: 12px 0 0; flex-wrap: wrap;
}
.devpagerbtn { min-height: 44px; padding: 10px 18px; width: auto; flex: 0 1 auto; }
.devpagerat {
  flex: 1 1 auto; text-align: center; color: var(--haze);
  font-size: 12px; font-weight: 700;
}

/* ---- a search that matched nothing -------------------------------------- */
.devempty { padding: 6px 0 2px; }
.devempty > .muted { margin: 0 0 10px; }

/* ---- the doorway into the detail, inside the card's own disclosure -------
   NOT in `.devactions`, and that was measured: app.css's CALM INVENTORY LIST
   rules put the health disclosure and the action row in one grid row on a
   phone and give the actions their own row as soon as `.devactions` holds a
   second button, which took a 390px card from 205px to 233px and broke
   devices-ui.test.js's "device records stay calm" budget. The card's own
   `details.healthcap` — whose summary already says "Details" — costs the
   collapsed card nothing. Full width inside the disclosure so it reads as the
   disclosure's own footer rather than as a third health destination. */
.devdetailbtn {
  min-height: 44px; width: 100%; margin-top: 10px;
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 14px;
  color: var(--cloud); font-family: var(--ui); font-size: 13px; font-weight: 700;
  padding: 11px 14px; cursor: pointer; text-align: left;
}

/* ---- the full-width expanded detail -------------------------------------
   `.devgroupbody` is a `repeat(auto-fill, minmax(min(100%, 380px), 1fr))` grid,
   so a panel dropped into it would otherwise take one 380px cell beside the
   next device's card. `1 / -1` is what makes it the full width of the list — the
   same one-line trick `.devgroupbody > .muted` already uses two hundred lines
   up for the "no devices for this child" line. */
.devgroup > .devgroupbody > .devdetail { grid-column: 1 / -1; }
.devdetail {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--dusk); padding: 14px; margin: 0;
}
.devdetail-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
/* The always-visible way out. `min-width` as well as `min-height`, because a
   short translated label ("Close" / "Cerrar" / "Fermer") on a `.ghost` sized by
   its text is exactly how a control ends up 40px wide while being tall enough
   to look fine. */
.devdetail-close { min-height: 44px; min-width: 88px; width: auto; margin-left: auto; }
.devdetail-title { flex: 1 1 auto; }
.devdetail-title {
  margin: 0; font-size: 15px; font-weight: 750; letter-spacing: -.01em;
  color: var(--cloud); overflow-wrap: anywhere;
}
.devdetail > .help { margin: 6px 0 10px; }
.devdetail > .banner { margin: 0 0 10px; }
.devdetail-pending { display: flex; flex-direction: column; gap: 3px; }
.devdetail-applied, .devdetail-shared, .devdetail-rung { margin: 8px 0 0; }
/* §3.11's clear-filter-cache. A rule with a button and three short lines under
   it, separated from the settings table above by the same 12px the action row
   below uses — it is a CONTROL, not another fact about the device, and the gap
   is what says so. The button keeps the 44px touch target every other control
   on this panel has, and does NOT stretch: it is deliberately not as prominent
   as "Open settings" beneath it. */
.devclearcache { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.devclearcache-btn { min-height: 44px; width: auto; min-width: 160px; }
.devclearcache-note, .devclearcache-keeps,
.devclearcache-pending, .devclearcache-applied, .devclearcache-error { margin: 6px 0 0; }

.devdetail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.devdetail-actions > button { min-height: 44px; width: auto; flex: 1 1 auto; min-width: 160px; }

/* ---- the provenance grid: value, and where the value came from ----------
   NOT a <table>. A table needs a minimum width to stay readable and would be
   the one thing on this page that scrolls sideways at 320px. One column per
   fact, stacked until there is room for three. */
.devprov { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.devprov-row {
  display: grid; grid-template-columns: 1fr; gap: 2px 12px;
  padding: 10px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--dusk2);
}
/* The header row is a label for the three columns, so it is meaningless while
   they are stacked — each stacked row already names its own parts. */
.devprov-head { display: none; }
@media (min-width: 640px) {
  .devprov-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr);
    align-items: start;
  }
  .devprov-head {
    display: grid; background: none; border: none; padding: 2px 10px 0;
    font-size: 10.5px; font-weight: 800; letter-spacing: .09em;
    text-transform: uppercase; color: var(--haze);
  }
}
.devprov-name { font-size: 13px; font-weight: 700; color: var(--cloud); overflow-wrap: anywhere; }
.devprov-value { font-size: 13px; color: var(--cloud); overflow-wrap: anywhere; }
.devprov-src { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* `.pill` is `white-space: nowrap` so a badge never breaks mid-word; a child's
   name inside a provenance chip can be long enough to need to. */
.devprov-chip { white-space: normal; overflow-wrap: anywhere; }
.devprov-note { color: var(--haze); overflow-wrap: anywhere; }

/* WHY THERE IS NO SPECIAL CASE FOR A ONE-DEVICE HOUSEHOLD HERE, and this was
   MEASURED in a real browser rather than reasoned about.

   An earlier draft of this block carried two `:first-child:nth-last-child(2)`
   rules to stop a lone card snapping from the centred `min(100%, 620px)` of
   `#devices .devgroup > .devgroupbody > .devcard:only-child` to half width the
   moment its detail became a sibling. That rule is DEAD: the CALM INVENTORY
   LIST block (2026-09-05, ~500 lines above) re-declares the same grid as a
   single `minmax(0, 1fr)` column inside a 1120px canvas and makes every card —
   `:only-child` explicitly included — full width of it. So there is no
   centred-620px state left to protect, and the draft rules would have RE-CREATED
   the bug they were written to prevent: they matched exactly when a detail was
   open, and would have squeezed the card back to 620px on every press of
   Details. Deleted rather than kept "just in case"; the browser suite
   (device-list-responsive.test.js) measures the card before and after the panel
   opens, so a real regression here fails loudly instead of being guessed at. */

/* ==========================================================================
   AN AMBER HEALTH VALUE FAILED CONTRAST IN LIGHT — MEASURED, NOT GUESSED
   ==========================================================================
   FOUND BY OPENING THE DISCLOSURE. The four health cells inside a device
   card's `details.healthcap` have never been scanned by axe with that
   disclosure OPEN — every existing run measured the collapsed card — and the
   device-list pass both puts its own doorway inside that disclosure and keeps
   it open across repaints, so parents will now have it open far more often.
   The first scan that opened it reported, at 390x844 in light, on all four
   cells of a tablet that has not reported yet:

     Element has insufficient color contrast of 4.42 (foreground #9b6500,
     background #eef3f8, font size 8.6pt (11.5px), font weight normal).
     Expected contrast ratio of 4.5:1

   `.hcval` inherits `.hcdot-amber + .hctxt`'s colour, which is bare
   `var(--amber)` — #9B6500 in light at the time. Amber had no light override
   here at all, unlike green and red two hundred lines up, which is how it
   slipped through.

   RESOLVED AT THE TOKEN, 2026-09-06 — and the scoped #8F5D00 patch that used to
   sit at the end of this file is gone with it. The reason that patch existed
   ("moving `--amber` would re-open `.banner.warn`'s separately measured 7.2:1
   decision") did not survive being checked: `.banner.warn` never used the
   token, it pastes #7A4F00 by hand, and 7.2 was that colour's number, not
   #9B6500's. #9B6500's real number on white is 4.94.

   A full browser census then found this cell was one of NINE amber text
   surfaces below AA in light, not one of two — `.ovstrip b` at 3.70, and
   `.devstatus.tone-wait`, `.badge.attn`, `.kd-stateunknown b`,
   `.statusline-warn` and `.asub.web` between 3.95 and 4.42. A per-surface patch
   was never going to finish. Light `--amber` is now #7A4F00 (its token comment
   carries the working), which takes this cell to 6.39:1 on the `.hccell` fill
   it actually sits on and 7.13:1 on a card. */

/* The household's children, INSIDE the "Children & profiles" tab rather than as
   a second bordered panel nested in it. `#devicePanelProfiles` is already a
   `.card`; this is a section of that card, separated by the same hairline the
   rest of this stylesheet uses to divide one job from the next, not by a second
   border and shadow. js/profiles.js decides which of the two shapes it builds —
   see hostFor() there. */
#devicePanelProfiles > #ppChildrenDevices > .pp-children-section {
  border-top: 1px solid var(--line);
  margin-top: 18px;
  padding-top: 16px;
}
#devicePanelProfiles > #ppChildrenDevices > .pp-children-section > .ghost {
  min-height: 44px;
}

/* ==========================================================================
   THE VOLUME LOCK — LANE E, 2026-09-06
   ==========================================================================
   The parent-facing half of VolumeLock.kt, on the child's own settings screen.
   Tokens only, so both themes are correct by construction.

   `.rowline` is a flex row with `.ghost { flex: none }`, which is right for one
   button beside an input and wrong for a PAIR of choices that must both stay
   pressable at 320px in a language whose labels are half again as long
   ("Laisse-la le changer"). These two rows wrap and share the width instead. */
/* `.rowline.child-volume-actions`, NOT `.child-volume-actions` — and a real
   browser at 320px is what said so. `.rowline .ghost { flex: none }` is (0,2,0)
   and beat a bare `.child-volume-actions > button` at (0,1,1), so the choices
   went on sizing themselves to their own text and the longer label
   ("Let Alexandra-Josephine change it") hung straight out of the card. Matching
   the row class as well makes this (0,2,1) and lets it win where it must.
   `min-width: min(100%, 148px)` for the same reason: a fixed 148px floor is an
   overflow waiting to happen in the narrowest card this product has. */
.rowline.child-volume-actions { flex-wrap: wrap; }
.rowline.child-volume-actions > button {
  flex: 1 1 auto;
  min-width: min(100%, 148px);
  min-height: 44px;
  width: auto;
}
.child-volume-extras { margin-top: 14px; }
.child-volume-extras > .fld { display: block; margin-top: 12px; }
.child-volume-extras > select.inp { min-height: 48px; }
/* The per-tablet lines. `.arow` is the shared list row this screen already uses
   for the same "one item, why, and a pill" shape; the override sentence under a
   tablet's name must be able to break inside a long device or child name rather
   than pushing the card wider than the page. */
.child-volume-tablets { margin-top: 16px; }
.child-volume-tablets > .arow > .an { min-width: 0; }
.child-volume-tablets .asub { overflow-wrap: anywhere; }
/* Amber is this stylesheet's "a thing waiting for the parent" (§3), and a
   tablet quietly following its own volume instead of the child's is exactly
   that. No new colour and no new meaning. */
.child-volume-tablets .asub.warnnote { color: var(--waiting-text); }

/* ==========================================================================
   THE LIGHT AMBER PATCH THAT USED TO LIVE HERE IS GONE — 2026-09-06
   ==========================================================================
   It repainted `.hcdot-amber + .hctxt` and `.warnnote` to #8F5D00 and had to be
   the LAST rule in the file, because two of the sites it covered declare
   `color: var(--waiting-text)` further down and at equal specificity the later rule
   wins. A correct colour that only works from one line in one file is a fuse,
   not a fix: the next `color: var(--waiting-text)` appended below it would have put
   the failure straight back with nothing to catch it.

   The token is fixed instead. Light `--amber` was #9B6500 above a comment
   quoting a DIFFERENT colour's measurement; it is #7A4F00 now, derived against
   the pale fills amber text is actually painted on, and the working is on the
   token itself in the `:root[data-theme="light"]` block at the top of this file.
   Four other light overrides that pasted #7A4F00 by hand went with this one.

   Its own `@media (prefers-color-scheme: light)` half was worse than redundant:
   light is reached ONLY through `data-theme="light"` here (see the DARK IS THE
   DEFAULT note by the token), so in that untoggled state the DARK tokens are in
   force and the page is dark — and the half painted a dark brown #8F5D00 onto
   it. Deleting it leaves those two selectors on `--amber`'s dark #FFC44B, which
   is the right colour for a dark page. */

/* ==========================================================================
   THE APPS SEARCH FIELD, ITS CLEAR CONTROL, AND THE LINE THAT SAYS WHAT IS
   NARROWING THE LIST
   ==========================================================================

   ONE FLEX ITEM, NOT TWO. `.rowline` (line 818) is `display: flex` with no
   wrap, and `.rowline .ghost { flex: none }` makes a button in it unshrinkable.
   This row already carries a search field and two selects, and
   zip-parity-child-cluster.test.js measures that the Apps screen never scrolls
   sideways at 320px. A fourth sibling is how that goes red, so the Clear button
   is positioned INSIDE the field's own box and the field reserves room for it
   with padding rather than with width. `min-width: 0` is what lets the wrapper
   shrink at all — a flex item's default `min-width: auto` is its content's
   intrinsic width, which for a 16px input is wider than a 320px phone.

   THE NATIVE ✕ IS TURNED OFF, and that is a gain rather than a loss. A
   `type="search"` field paints a clear button in Chromium and Safari, NOTHING
   in Firefox, and in none of the three is it reachable by Tab or given a name a
   screen reader can announce. Two affordances that disagree per browser is
   worse than one that behaves the same everywhere; Escape in the field does the
   same job for a parent who never reaches for the mouse (app.js's keydown
   handler).

   44px SQUARE. Below 500px this is hit with a fingertip, which is the floor
   test/tools/sweep-ui.mjs reports against, and it fits inside the field's own
   48px minimum height. */
.invsearchbox { position: relative; display: flex; flex: 1 1 auto; min-width: 0; }
.invsearchbox > .inp { padding-inline-end: 48px; }
#invSearch::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; display: none; }
.invsearchclear {
  position: absolute; inset-inline-end: 2px; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; padding: 0; background: transparent;
  display: flex; align-items: center; justify-content: center;
  color: var(--haze); font-size: 15px; line-height: 1;
}
.invsearchclear:hover { color: var(--cloud); }

/* COLLAPSED, NOT `display: none`. This element is a `role="status"` live
   region, and the whole point of one is that assistive technology is already
   watching it when its text arrives. A region hidden with `display: none` is out
   of the accessibility tree entirely, so the FIRST narrowing — the one a parent
   most needs announced, because it is the one that just emptied their list —
   is the announcement most likely to be missed. Zeroing its box leaves the
   region rendered and watched while taking no space on a screen that has
   nothing to say. */
#invNarrowed { margin-top: 8px; }
#invNarrowed:empty { margin: 0; height: 0; overflow: hidden; }

/* REVERSED-DIRECTION QR PAIRING (OWNER §3.18(2)). The QR is a viewBox-only SVG (qr.js), so it
   is sized here. Always on a white card, whatever the theme: a themed dark-on-dark QR does not
   scan (qr.js says the same on its own side). */
.qr-pair-view { margin-top: 12px; }
.qr-pair-view:empty { margin: 0; }
.qr-pair-code {
  display: inline-block;
  background: #ffffff;
  padding: 12px;
  border-radius: 10px;
}
.qr-pair-code svg, .qr-pair-code img.qr-pair-svg { display: block; width: 220px; height: 220px; }

/* FAVORITES parent surface (OWNER-DECISIONS §3.21) — the per-favorite glyph
   preview, paused pill and icon-picker row. Tokens only: cyan (--glow, the
   trusted+primary accent, the same one the tablet tints a tile with) for the
   glyph badge; the paused pill reuses `.pill.off` (coral, UI-DIRECTION rule 4).
   A favorite row is `.fav-row.arow`, so `.arow`'s flexbox already lays it out —
   these rules only add the glyph, let the text take the middle, and group the
   icon field with its preview. */
.fav-row-glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  background: rgba(105,216,255,.16); color: var(--glow);
  font-weight: 700; font-size: 16px; line-height: 1;
}
.fav-row-text { flex: 1; min-width: 0; }
.fav-row-titleline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fav-form-iconrow { display: flex; align-items: center; gap: 10px; }
.fav-form-iconrow .inp { flex: 1; min-width: 0; }

/* ===========================================================================
   §3.20 (WEB) — the per-lock control panel on Devices > [tablet]. One row per
   lock: a name, the DEVICE-CONFIRMED state pill (mint on / amber sending /
   neutral off), an optional honest detail line, and its independent control.
   The hairline and tokens are the ones .fc-row and the card borders already
   draw, so this adds structure, not a new palette (UI-DIRECTION rule 4).
   =========================================================================== */
.klocks { margin: 6px 0 2px; }
.klock {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  border-top: 1px solid var(--line); margin-top: 12px; padding-top: 12px;
}
.klock:first-child { border-top: 0; margin-top: 4px; padding-top: 0; }
.klock-main { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
.klock-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.klock-name { font-size: 14px; font-weight: 650; }
.klock-detail { margin: 0; font-size: 12px; color: var(--haze); }
.klock-note { margin: 8px 0 0; }
.klock .ghost.small, .klock .primary.small { flex: none; }
/* The rotation segmented control — Free / Vertical / Horizontal, each its own
   independent choice; the current one is the primary (cyan) fill. */
.krot { display: flex; gap: 6px; flex: none; flex-wrap: wrap; }
.krot .ghost.small, .krot .primary.small { min-width: 0; }
/* F7 (2026-09-12): `.klock`'s default row layout put the label
   (`.klock-main`) and the three-button `.krot` group SIDE BY SIDE with
   `align-items: center`. Once `.krot` wrapped its three buttons onto a second
   line — which it did at 1280/900/375px, the three widths this was checked
   against — the shorter single-line label sat vertically CENTERED against the
   now-taller button block, so the first row of buttons read as sitting ABOVE
   the label at a glance. `.klock-rot` stacks the two instead: the label is
   always the full-width top line, the choices are always a single row
   underneath (flex-wrap: nowrap, each button sharing the row rather than
   keeping its natural width), so there is exactly one visual order at every
   width this was checked against, and no line for `.krot` to wrap onto. */
.klock.klock-rot { flex-direction: column; align-items: stretch; }
.klock.klock-rot .klock-main { flex: none; }
/* MEASURED (kiosk-rotation-volume-ui.test.js), not assumed: `.primary` and
   `.ghost` carry DIFFERENT `min-height`s (50px / 44px — general button rules,
   unrelated to this row), so the CURRENT choice (`.primary.small`) and its two
   siblings (`.ghost.small`) are genuinely different heights. `align-items:
   center` — explicit, not the container's own default — keeps that from
   reading as two rows: the shorter button sits centred inside the row's own
   height rather than sharing only its bottom (or only its top) edge with the
   taller two, which is what a plain `nowrap` alone still measured as. */
.klock.klock-rot .krot { flex-wrap: nowrap; align-items: center; }
.klock.klock-rot .krot .ghost.small,
.klock.klock-rot .krot .primary.small {
  flex: 1 1 0; min-width: 0; padding-left: 6px; padding-right: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The volume LEVEL slider sits on its own sub-row under the volume lock. */
.klock-vol .klock-main { gap: 8px; }
/* F8 (2026-09-12): no height was set at all, so the native <input type=range>
   rendered at whatever the UA defaults to — ~16px in Chrome/Firefox, under the
   24px minimum touch target. `min-height` grows the element's own box (and
   with it the slider's hit area) without a browser-specific reset of its
   native appearance. */
.kvol-slider { width: 100%; accent-color: var(--glow); min-height: 24px; }
.kvol-slider:disabled { opacity: .5; }
/* item 2 of the 2026-09-12 investigation — the Devices list card's lock
   indicator. Mint (UI-DIRECTION §3: healthy/applied), the same meaning
   `.pill.on` already carries for a CONFIRMED lock on the per-lock panel this
   line is drawn from the identical predicate for (window.PP.kioskTruth). */
.devsub.devlock { color: var(--done-text); }
/* PP-20 (child-browser-grant-state-not-reported-back). Same rule, same mint
   (UI-DIRECTION §3: healthy/applied) — drawn only when vitals.browserGranted
   is true, i.e. the launcher itself confirmed the browser grant is live. */
.devsub.devbrowsergrant { color: var(--done-text); }
.child-onboarding{margin-top:16px}.co-step{display:grid;gap:12px}.co-choice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px;border:1px solid var(--line);border-radius:12px}.co-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}@media(max-width:560px){.co-choice{align-items:flex-start}.co-choice button{min-width:96px}.co-actions>*{flex:1 1 100%}}
/* GoodQA #478: the wizard's 'time' step locked-periods builder. Reuses
   .arow/.an (schedule rows), .chips/.chip (day picker) and .timerow/.timein
   (From/Until) verbatim -- see play-migration.js's renderTimeStep() header
   -- so only the section's own vertical rhythm needs a rule here. */
.co-periods{display:grid;gap:8px;margin-top:12px}
/* GoodQA #33 (remaining half) — THE APPS-ACKNOWLEDGMENT CHECKBOX. Same root
   cause and same proven pattern as this file's own "THE CHECKBOX FIX"
   (search this file, `.loginpanel .consentRow`) and settings-fidelity.css's
   §9h (`#privacyCard .consentRow` etc.): index.html's unconditional
   `<meta name="color-scheme" content="light dark">` with no `color-scheme`
   ever declared for ackToggle()'s `#coAppsAck` (play-migration.js), so a
   dark-preferring OS/browser paints its own near-black checkbox over this
   light-themed step regardless of this app's own `data-theme`. MEASURED
   with a real Chromium capture (colorScheme:'dark' context, data-theme
   forced 'light') before this rule existed: a solid dark square over the
   "ParentProof cannot filter content within apps..." notice
   (docs/OWNER-DECISIONS.md §0.2's own required acknowledgment copy).
   Scoped to `.co-ack` (ackToggle()'s own wrapper), not the shared
   `.consentRow` rule above, for the same reason that rule stays scoped to
   `.loginpanel`: a global native-control repaint is its own verified pass,
   not a drive-by here.

   NOTE FOR WHOEVER TOUCHES play-migration.js's step order NEXT: 'apps' was
   retired from K_ASK_AGE/K_AGE_KNOWN on 2026-09-21 (see that file's own
   header comment), so choices(ctx, pane, 'apps', ...) — and therefore
   ackToggle()/#coAppsAck — is not reachable from the live wizard today.
   This rule is still correct for the code exactly as shipped (the function
   and its markup were not deleted, only stopped being called), verified by
   directly exercising the real, unmodified ackToggle() against this rule
   rather than a hand-built lookalike. See
   plans/WAL-2026-09-22-goodqa-33.md for the full verification record and
   the separate, larger gap this pass found and did NOT fix: the live Apps
   screen (js/apps.js) that per-app allow/block actually moved to has no
   within-app-content acknowledgment gate at all. */
:root[data-theme="light"] #childOnboarding .co-ack,
:root:not([data-theme]) #childOnboarding .co-ack { color-scheme: light; }
:root[data-theme="dark"] #childOnboarding .co-ack { color-scheme: dark; }
#childOnboarding .co-ack input[type="checkbox"] { accent-color: var(--glow); }
/* GoodQA #477: the "Child added" one-shot confirmation (checkmark, heading,
   two actions), shown only for THIS session's own just-finished transition
   -- see play-migration.js's childAddedCard(). --mint is this file's
   existing done/applied token (UI-DIRECTION-2026-08-22 §3); --dusk is the
   same on-accent ink color .chip[aria-pressed="true"] already pairs with
   --glow for identical contrast reasoning. */
.co-added{margin-top:16px;text-align:center}
.co-added-check{width:48px;height:48px;margin:0 auto 10px;border-radius:999px;background:var(--mint);color:var(--dusk);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700}
.co-added .co-actions{justify-content:center}

/* ROUND-5 AUDIT (GoodQA #433). `.pill`'s own base rule (above, this file)
   forces `white-space: nowrap` — correct for every OTHER pill on this
   screen (a one/two-word state like "Current" or "Active"), wrong for the
   capacity fit-warning pill (css/billing.css's `.billing-fit-warning`),
   which carries a full sentence ('billing.tierOverCapacity' —
   "Doesn't fit your household's current usage" and its es/fr equivalents).
   At the repo's own 320x568 phone floor, `.billing-fit-warning`'s real
   rendered width (inside a tier card inside #billingSettingsPanel's own
   padding) measured ~200px while the pill's forced-nowrap natural width
   measured ~259px — a ~10px overflow past the viewport's own right edge.
   Scoped to this one pill only, and only at the narrow widths where the
   card is actually this tight: every other `.pill` on this screen (Current,
   Active, invoice status) keeps its normal one-line treatment everywhere. */
@media (max-width: 340px) {
  .billing-fit-warning .pill {
    white-space: normal;
  }
}

/* 2026-09-19 design pass — the inventory is a record list, not nested cards. */
#devices > .devstack > #devicePanelStatus {
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
#devices .devgroup,
#devices .devgroup:first-child {
  width: min(100%, 1120px);
  margin: 10px auto 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
#devices .devgroup:first-child { margin-top: 0; }
#devices .devgroup > .devgrouphead {
  min-height: 44px;
  padding: 0 2px 8px;
}
#devices .devgroup > .devgroupbody {
  margin: 0;
  border-top: 1px solid var(--line);
}
#devices .devgroup > .devgroupbody > .devcard,
#devices .devgroup > .devgroupbody > .devcard:only-child {
  padding: 10px 2px;
}
#devices .devcard .devnmtext { font-size: 16px; }

/* ==========================================================================
   TIGHT DEVICE ROWS — OWNER, 2026-09-19, looking at the live list:
   "this is the layout i want.. real simple.. tight clean rows .. not huge
   fucking sprawleed cards", with the columns named directly:
   "[Name] [Status] [Last Seen] [Last Kid] [Menu] [...]".

   NOT A NEW COMPONENT. Every element below already existed on the card —
   name, rollup pill, the profile line, the status line, and (newly lifted
   out of the facts disclosure) last seen. This only stops them stacking
   into a tall block and lays them along one line instead, so nothing is
   removed and no honesty rule changes: the pill is still deviceRollup()'s
   worst-of signal, `.devstatus` still carries its own tone, and a
   requested-but-unconfirmed pause still reads Pending through the same
   `.devlockstate` it always did.
   ========================================================================== */
/* REWRITTEN, having actually measured the live result of the first cut
   instead of trusting a screenshot at face value: .devbtn ("identity") is
   NOT the whole row. #devices .devrow.devcard is itself a 3-area outer grid
   ("identity details actions" — the 2026-09-05 CALM INVENTORY LIST rule
   below, and the earlier compact-footer rule above it) that gives identity
   roughly HALF the row's width, with the "details" disclosure trigger and
   the Menu/…/Pause cluster taking the rest. A rigid inner grid trying to
   hold name + last-seen + status as three fixed-minimum columns inside that
   narrower half is what crushed the name to zero width on the live site —
   there was no measurement step before deploying it, only a screenshot read
   at a glance. Flex + wrap degrades instead of crushing: whatever does not
   fit on line one moves to line two, at any width, rather than every column
   fighting the others for a slice of a cell too narrow for four minimums. */
@media (min-width: 900px) {
  #devices .devcard .devbtn {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
  }
  /* row-wrap, not column: .devnmrow (name + the rollup pill — the owner's
     "[Name] [Status]") is forced onto its own full-width line below, and
     everything after it (last seen, then the child/online line) flows on
     line two, wrapping to a THIRD line only if it truly does not fit —
     never crushing the name, which a rigid column-count grid cannot do. */
  #devices .devcard .devnm {
    display: flex;
    flex-flow: row wrap;
    align-items: center;
    column-gap: 10px;
    row-gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
  }
  /* WRAP, NOT SHRINK — the adversarial review's real finding: a fixed floor
     on the outer identity column alone cannot fix this, because the pill
     shares .devnmrow's single line with the name and has `flex: none` (it
     never shrinks), so at any width where identity's floor minus the icon
     minus the pill leaves too little room, `.devnmtext` was the only thing
     left to give — down to a scrollWidth/clientWidth mismatch as extreme as
     306px/1px, measured at a 900px viewport with a real .wrap/#nav layout
     around it (the first fix's own harness omitted that chrome entirely and
     so never saw this). `flex-wrap: wrap` plus a real floor on the name
     itself means the PILL drops to its own line under the name instead —
     the name is never asked to shrink below a legible width; the row
     becomes two lines only in the narrow band where there truly isn't room
     for both, which is a normal, honest degradation, not a silent crush. */
  #devices .devcard .devnmrow {
    flex: 1 1 100%;
    flex-wrap: wrap;
    row-gap: 2px;
    min-width: 0;
  }
  #devices .devcard .devnmtext {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 120px;
    flex: 1 1 auto;
  }
  /* CORRECTED: the first cut invented a `.devfactline` wrapper that does not
     exist in buildDeviceRow and styled `.devseen`/`.devchild` as if they were
     its children — none of that selector matched anything, which is exactly
     how the name still went missing after that "fix." The real siblings
     inside `.devnm` are `.devsub.devseen` (last seen) and `.devcontext`
     (which already wraps `.devchild` + `.devconnection`, the "[Last Kid]"
     and online/offline facts) — ordered here to read left to right exactly
     as the owner listed the columns: Name+Status on line one (Status is the
     rollup pill already inline in `.devnmrow`), then Last Seen, then Last
     Kid, on line two. `.devstatus` is NOT a visible column — app.js's own
     comment on it says it is kept only "as a compatibility hook for current
     reducers/tests" and `#devices .devcard .devstatus { display: none; }`
     (this file, the CALM INVENTORY LIST section) already hides it; nothing
     here needs to fight that rule. */
  #devices .devcard .devsub.warnnote { flex: 1 1 100%; order: 1; margin: 0 0 2px; }
  #devices .devcard .devsub.devseen { order: 2; margin: 0; white-space: nowrap; color: var(--haze); font-size: 11.5px; }
  #devices .devcard .devcontext { order: 3; margin: 0; }
  /* Platform/lock/browser-grant sublines and the telemetry line stay in the
     facts disclosure and on the device screen the … opens — this row shows
     only name, status pill, last seen and last-kid/online. */
  #devices .devcard .devplat,
  #devices .devcard .devlock,
  #devices .devcard .devbrowsergrant,
  #devices .devcard .devtelemetry { display: none; }
  /* NOT TWO "Device menu"s: the explicit Menu button in .devactions already
     says this. Kept in the DOM — it is still the band's own destination
     name for anything reading structure, and parent-lock-ui.test.js asserts
     it — and hidden only where the real button is visible beside it. */
  #devices .devcard .devopen { display: none; }

  /* CLAWING BACK WIDTH FOR IDENTITY — SUPERSEDED 2026-09-20 by the
     "PAIRED-DEVICES LIST AS ONE GRID COMPONENT" block at the end of this
     file, and the declaration itself is REMOVED rather than left to lose on
     cascade order, because it carried `!important`.

     WHAT IT SAID, AND WHY IT IS NOT LOST: the "details" track did not need a
     reserved `minmax(150px, auto)` of its own, so it was shrunk to its
     content to give the identity column the freed space. The replacement
     keeps that finding — the details track is now a FIXED 96–104px, which is
     narrower still — and fixes the half of it that was the actual defect:
     `auto auto` for the last two tracks is CONTENT-SIZED, so identity's 1fr
     resolved to a different width on every row and every field inside it
     drifted (measured: the protection pill at four different x positions in
     six rows). An `!important` that pins a content-sized track is exactly the
     "silent, order-dependent collision" this comment was warning about, one
     level up. */
  #devices .devcard > .healthcap { justify-self: start; }
}
/* THE TWO THINGS THAT ACTUALLY MADE EVERY ROW TALL, measured on the live
   list: the Details disclosure rendered as its own bordered box floating in
   the middle of the row, and each warning printed as a full-width padded
   banner underneath it (two of them on an unsupervised tablet). Neither
   piece of information is removed here — the disclosure still opens the same
   four-cell health panel, and every warning still says exactly what it said.
   They just stop being boxes. */
@media (min-width: 900px) {
  #devices .devcard > details.healthcap {
    border: 0;
    background: none;
    padding: 0;
    margin: 0;
    align-self: center;
  }
  #devices .devcard > details.healthcap > summary { padding: 4px 2px; font-size: 12px; }
  /* A warning is a line of text in the row, not a slab under it. */
  #devices .devcard > .devalerts { grid-column: 1 / -1; }
  #devices .devcard > .devalerts > .banner {
    margin: 2px 0 0;
    padding: 4px 0 0;
    border: 0;
    background: none;
    font-size: 12px;
    line-height: 1.45;
  }
}
/* One weight for the row's own controls, so [Menu] and [...] stop competing
   with each other and with the pause control the way four differently-tinted
   buttons did. `.devdeep` is the overflow that opens the deep device screen. */
#devices .devcard > .devactions { align-items: center; gap: 8px; }
#devices .devcard .devdeep {
  min-width: 44px;
  flex: 0 0 auto;
  font-size: 18px;
  line-height: 1;
  letter-spacing: .06em;
}

@media (max-width: 560px) {
  #devices .devrow.devcard {
    grid-template-columns: minmax(160px, 1fr) minmax(92px, auto);
  }
  #devices .devcard > .devactions { min-width: 0; }
  #devices .devcard > .devactions > button {
    max-width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

@media (min-width: 900px) {
  #devices .devrow.devcard {
    grid-template-columns: minmax(280px, 1fr) minmax(150px, auto) auto;
    grid-template-areas:
      "identity details actions"
      "alerts alerts alerts";
    align-items: center;
    row-gap: 6px;
  }
  #devices .devcard:has(> details.healthcap[open]) {
    grid-template-areas:
      "identity identity actions"
      "alerts alerts alerts"
      "details details details";
  }
}

/* ==========================================================================
   DEVICES · THE PAIRED-DEVICES LIST AS ONE GRID COMPONENT
   OWNER BRIEF, 2026-09-20: "Fix the 'Paired devices' list as one coherent
   responsive component. Do not patch the highlighted areas with isolated
   margins, offsets, absolute positioning, or row-specific CSS."

   WHAT WAS ACTUALLY WRONG, MEASURED ON THE LIVE BUILD (2acc4252) AT 1440px
   WITH SIX SEEDED DEVICES — .scratch/paired-devices-list/before-metrics.json:

     protection pill   left = 1283 | 1281 | 326  | 1306   (four positions)
     online/offline    left = 577  | 574  | 540           (three)
     Details summary   left = 270  | 861                  (two)
     Device menu       left = 823  | 1034 | 818           (three)
     "…"               left = 1051 | 1154 | 1043          (three)
     Pause             left = 1171 | 1274 | 1163          (three)
     row heights       169 | 170 | 114 | 170 | 170 | 170

   EVERY ONE of those is the same root cause: each row was its own grid whose
   tracks were CONTENT-SIZED (`minmax(150px, auto)` for details, bare `auto`
   for the action cluster), so the 1fr identity column — and therefore every
   field inside it — resolved to a different width on every row, and a row
   that happened to have no delivery sentence took a completely different
   grid-template-areas branch. Two rows out of six were laid out by a
   DIFFERENT rule than the other four.

   THE FIX IS ONE RULE, NOT SIX PATCHES: every track except the identity
   column is now a fixed width, so the identity column resolves identically on
   every row of the list and each field lands on the same x on every row.
   Content-dependent sizing is exactly what "drifting columns" means; there is
   no way to have both it and aligned columns.

   THREE DELIBERATE LAYOUTS, EACH APPLIED TO THE WHOLE ROW (the brief: "A row
   must either remain a complete desktop row or switch as a whole to a
   deliberate compact/card layout"):

     >= 1220px   WIDE ROW — one line:
                 [icon|name / last seen] [child] [state] | Details | Menu | … | Pause
     561–1219px  COMPACT ROW — two lines: the identity band, then one control
                 bar (Details left, the three controls right). Same tracks on
                 every row.
     <= 560px    PHONE CARD — unchanged. Every rule below is gated at 561px or
                 above precisely so the phone layout, its DEF-005 delivery-row
                 fix and the assertions in devices-ui.test.js
                 (details/pause top-aligned, 44px targets, .devopen visible,
                 .healthsummary-state visible) keep working byte for byte.

   WHY .devactions BECOMES `display: contents` AT >= 561px, AND WHY THAT IS
   NOT A TRICK: the brief asks for named areas for "secondary actions" and
   "primary action" SEPARATELY, and buildDeviceRow emits both inside one
   `.devactions` div. `display: contents` promotes its children onto the row
   grid so each can take its own named area, WITHOUT touching the DOM —
   parent-lock-ui.test.js pins the row's three children by index
   (row.children[0..2]) and .devnm's five children by index, so a DOM change
   here is not available. Same reasoning for `display: contents` on `.devnm`
   / `.devnmrow` / `.devcontext`: all three are plain divs with no ARIA role,
   so promoting them is invisible to assistive tech, and it is what lets the
   device name, the profile chip and the protection pill sit in three real
   grid columns while still living inside the ONE identity <button> that
   opens the device menu (owner, 2026-09-19 — the identity band and the "…"
   are two deliberate doorways and are NOT merged here).

   NOT CHANGED, DELIBERATELY:
   - `.devlockbtn.lock/.unlock` keep their transparent ground. The owner
     review of 2026-09-03 ("ONE VERB, ONE APPEARANCE", ~line 7092) removed the
     coral/mint wash so Pause looks the same weight everywhere it appears. The
     brief's "Resume may use the positive treatment; Pause may use the
     caution treatment" is ALREADY satisfied by the coral/mint text+border
     those two classes carry, so the inverted hierarchy is fixed from the
     other side: the secondary buttons lose their solid `var(--track)` fill
     (which is what made them read as disabled AND heavier than the primary).
   - "Applied ✓" vs "Pending…" — `.devlockstate` is only ever re-created by
     buildDeviceRow, is never display:none'd by anything below, and keeps its
     own `.applied`/`.failed`/phase classes. No status text is hidden here.
   ========================================================================== */

/* -------- 1. THE GROUP HEADER: BULK CONTROLS OUT OF THE ROWS' AIRSPACE ----
   `.devgroup > .chips` was `position: absolute; top: 8px/14px; right: 0/16px`
   over the <summary>, and `.devgroupactions` was a normal-flow right-aligned
   band immediately above the first device row. Measured at 1440 on the live
   build: chips occupied y 336–380, .devgroupactions y 364–417 (they OVERLAP
   by 16px) and the first device row started at y 418 — one pixel below the
   Pause-all button. The brief: "Put bulk controls in a clearly separated
   group header aligned with the list grid, with enough space that they cannot
   collide with the first device row."
   Float, not absolute: `.chips` is `display: flex` and therefore establishes
   an independent formatting context, so it is placed BESIDE the float instead
   of under it — filters left, bulk action right, both in normal flow, both
   contributing their real height to the header. `clear: both` on the body is
   what makes the collision structurally impossible rather than tuned away. */
@media (min-width: 900px) {
  #devices .devgroup > .devgrouphead { padding-right: 0; }
  #devices .devgroup > .chips {
    position: static;
    top: auto;
    right: auto;
    max-width: none;
    justify-content: flex-start;
  }
}
#devices .devgroup > .devgroupactions {
  float: right;
  padding: 0;
  margin: 0 0 8px 12px;
}
#devices .devgroup > .chips { margin: 0 0 10px; row-gap: 6px; }
#devices .devgroup > .devgroupbody { clear: both; }
/* A BULK CAUTIONARY ACTION, NOT AN ORDINARY NEUTRAL FILTER (brief §5). The
   values are `.ghost.danger`'s own, verbatim — the treatment #pauseAllBtn in
   the page header already uses for this same verb — including both
   light-theme substitutes, because bare --coral measures 4.38:1 on white. No
   sixth colour and no new token: red stays "danger/unsafe" per UI-DIRECTION §3. */
#devices .devgroup > .devgroupactions > button {
  min-height: 44px;
  color: var(--coral);
  border-color: rgba(255, 107, 94, .45);
  font-weight: 750;
}
:root[data-theme="light"] #devices .devgroup > .devgroupactions > button { color: #B23127; }
:root:not([data-theme]) #devices .devgroup > .devgroupactions > button { color: #B23127; }

/* ONE NEUTRAL TREATMENT AT EVERY WIDTH, and this half is PAINT ONLY — no
   width, height, padding or display is touched here, so the phone card's
   geometry (devices-ui.test.js pins its 44px targets, its <=205px height and
   its top-aligned Details/Pause footer at 375 and 320) is byte-identical.
   `background: var(--track)` is a solid mid-grey fill; it is the whole reason
   "Device menu" and "…" read as DISABLED while outweighing the one control on
   the row that actually does something. `:not(.setup)` keeps the cyan
   maintenance variant's own tint, which is a different control with a
   different meaning. */
#devices .devcard .devactionbtn:not(.setup) {
  background: transparent;
  border-color: var(--border-subtle);
  color: var(--text-secondary);
}
@media (hover: hover) {
  #devices .devcard .devactionbtn:not(.setup):hover {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--brand) 38%, var(--border-subtle));
    background: color-mix(in srgb, var(--brand) 8%, transparent);
  }
}

/* -------- 2. ONE CONSISTENT NEUTRAL TREATMENT FOR SECONDARY ACTIONS -------
   `.devactionbtn` carried `background: var(--track)` — a solid mid-grey fill.
   The owner: "The large solid-grey 'Device menu' and ellipsis buttons look
   disabled while receiving more visual weight than the primary action." Both
   secondaries now share ONE outline treatment, and `.devdeep` becomes a
   compact square icon button instead of a wide pill. Tokens only. */
@media (min-width: 561px) {
  #devices .devcard .devactionbtn:not(.setup) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: auto;
    min-height: 44px;
    padding: 0 14px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text-secondary);
    font-size: 12.5px;
    font-weight: 700;
    white-space: nowrap;
  }
  @media (hover: hover) {
    #devices .devcard .devactionbtn:not(.setup):hover {
      color: var(--text-primary);
      border-color: color-mix(in srgb, var(--brand) 38%, var(--border-subtle));
      background: color-mix(in srgb, var(--brand) 8%, transparent);
    }
  }
  #devices .devcard .devdeep {
    width: 44px;
    min-width: 44px;
    padding: 0;
    font-size: 19px;
    line-height: 1;
    letter-spacing: 0;
  }
  /* The primary keeps its own hue and its 44px floor, and now matches the
     secondaries' height and radius so the cluster reads as one control set. */
  #devices .devcard .devlockbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    width: auto;
    min-height: 44px;
    padding: 0 14px;
    border-radius: var(--radius-pill);
    font-size: 13px;
    font-weight: 750;
    white-space: nowrap;
  }
  /* The Details disclosure is a control in the same row, so it gets the same
     height and radius. It is NOT a box: the bordered slab it used to draw in
     the middle of the row is what made every row look like a form. */
  #devices .devcard > details.healthcap {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
  }
  #devices .devcard details.healthcap > .healthsummary {
    justify-content: flex-start;
    min-height: 44px;
    padding: 0 10px;
    border-radius: var(--radius-pill);
    border: 1px solid transparent;
    font-size: 12.5px;
  }
  /* MEASURED: at a 96px details track the word "Details" broke as "Det" /
     "ails" — a two-line control in a row whose whole point is one line. The
     track is fixed, so the label must be told it is a label and not a
     paragraph; the track itself is sized from this measurement, not guessed. */
  #devices .devcard .healthsummary-label { white-space: nowrap; }
  @media (hover: hover) {
    #devices .devcard details.healthcap > .healthsummary:hover {
      border-color: var(--border-subtle);
    }
  }
  /* ONE STATUS, ONE PLACE (brief §6). `.healthsummary-state` renders
     `t(deviceRollup(d).key)` — the SAME string as the row's rollup pill, so
     every row said its protection state twice, in two different styles,
     140px apart (measured: ["Unsupervised","online","Unsupervised"] per row).
     The pill is the one that stays, because it sits in the dedicated
     protection-state column. Hidden ONLY at >= 561px: below that the pill
     itself is hidden (`.devnmrow > .pill` in the <=560 block above) and this
     element IS the status, which devices-ui.test.js asserts at 375 and 320. */
  #devices .devcard .healthsummary-state { display: none; }
  /* Not a spacer and not a reserved empty band: `min-height: 2.4em` on the
     delivery sentence held two lines of blank space open on every paused row
     even when the sentence is one line. */
  #devices .devcard .devlockstate {
    min-height: 0;
    text-align: left;
    font-size: 11.5px;
  }
}

/* -------- 3. THE SHARED ROW GRID -----------------------------------------
   The `:has()` variants are restated here for one reason only: the un-media'd
   DEF-005 rules above (`#devices .devcard:has(> .devactions > .devlockstate
   :not(:empty))`, ~line 11579) are MORE SPECIFIC than a bare
   `#devices .devrow.devcard`, so a row with a pending delivery sentence would
   otherwise keep the old four-band areas and be the one row out of six that
   is laid out differently — which is defect §3 in the brief. Restating the
   same selectors at the same specificity, later in the cascade, is what makes
   "all rows use the same structure regardless of device state" true rather
   than nearly true. Those rules still own the phone, where they are correct. */
@media (min-width: 561px) {
  #devices .devrow.devcard {
    display: grid;
    grid-template-columns: 116px minmax(0, 1fr) 148px 44px 128px;
    grid-template-areas:
      "identity identity identity  identity identity"
      "details  .        secondary overflow primary";
    align-items: center;
    column-gap: 10px;
    row-gap: 8px;
  }
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)) {
    grid-template-areas:
      "identity identity identity  identity identity"
      "details  .        secondary overflow primary";
  }
  /* Opening the disclosure moves it, with its summary, to its own full-width
     band under the controls — the one deliberate reflow, and it is an
     interaction state, not a device state. */
  #devices .devcard:has(> details.healthcap[open]),
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)) {
    grid-template-areas:
      "identity identity identity  identity identity"
      ".        .        secondary overflow primary"
      "details  details  details   details  details";
  }

  #devices .devcard > .devbtn { grid-area: identity; }
  #devices .devcard > .healthcap { grid-area: details; justify-self: start; align-self: center; }
  #devices .devcard:has(> details.healthcap[open]) > .healthcap { justify-self: stretch; }
  /* Promote the action children onto the row grid — see the header comment. */
  #devices .devcard > .devactions { display: contents; }
  #devices .devcard > .devactions > .device-menu-trigger { grid-area: secondary; }
  #devices .devcard > .devactions > .devdeep { grid-area: overflow; }
  #devices .devcard > .devactions > .devlockbtn { grid-area: primary; }
  /* The delivery sentence and the ghost-row Forget belong to the whole
     record, so they take implicit full-width rows UNDER the control bar
     rather than being squeezed into the 128px primary column (which is how
     "Resume" used to split into "Resu"/"me" — DEF-005). Implicit, not named:
     a named row would reserve its gap on every row that has neither. */
  #devices .devcard > .devactions > .devlockstate {
    grid-column: 1 / -1;
    justify-self: start;
    min-width: 0;
  }
  #devices .devcard > .devactions > .devforget {
    grid-column: 1 / -1;
    justify-self: end;
    margin: 0;
  }
  /* Every button in the cluster fills its own fixed track, so the three
     controls are the same size on every row whatever the row's state. */
  #devices .devcard > .devactions > button { width: 100%; min-width: 0; }
  #devices .devcard > .devactions > .devdeep { width: 44px; }
  #devices .devcard > .devactions > .devforget { width: auto; }
}

/* -------- 4. THE IDENTITY BAND ------------------------------------------- */
@media (min-width: 561px) {
  #devices .devcard .devbtn {
    display: grid;
    /* meta is bounded rather than elastic so "Last seen ..." starts at the
       same x on every row AND sits next to the facts it belongs with; the
       slack goes to the seen track's right, where it is the row's one
       deliberate breathing space instead of a hole in the middle of it. */
    grid-template-columns: 36px minmax(0, 250px) minmax(0, 1fr) 116px;
    grid-template-areas:
      "icon name name  state"
      "icon meta seen  state";
    align-items: center;
    column-gap: 12px;
    row-gap: 3px;
    width: 100%;
    min-height: 0;
    padding: 6px 8px;
  }
  #devices .devcard .devnm { display: contents; }
  #devices .devcard .devnmrow { display: contents; }
  #devices .devcard .devicon {
    grid-area: icon;
    width: 36px;
    height: 36px;
    position: static;
    inset: auto;
  }
  #devices .devcard .devnmtext {
    grid-area: name;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 16px;
  }
  /* THE PROTECTION-STATE COLUMN — one predictable location, one component
     style, vertically centred against the device identity. `flex: none` on
     the pill is why it could never shrink and why the NAME was the only
     thing left to give in the old flex row; here it has a track of its own
     and neither can take the other's space. */
  #devices .devcard .devnmrow > .pill {
    grid-area: state;
    justify-self: start;
    align-self: center;
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  #devices .devcard .devsub.devseen {
    grid-area: seen;
    justify-self: start;
    margin: 0;
    white-space: nowrap;
    color: var(--text-secondary);
    font-size: 11.5px;
  }
  #devices .devcard .devcontext {
    grid-area: meta;
    display: grid;
    grid-template-columns: minmax(0, 168px) minmax(0, auto);
    align-items: center;
    column-gap: 8px;
    margin: 0;
    min-width: 0;
    overflow: hidden;
  }
  #devices .devcard .devchild {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  #devices .devcard .devconnection { flex: none; }
  /* The one identity sub-line that is actionable and time-sensitive (the
     recovery-PIN pending warning) keeps a full-width band of its own rather
     than fighting for a column. */
  #devices .devcard .devsub.warnnote {
    grid-column: name-start / -1;
    grid-row: 3;
    margin: 2px 0 0;
  }
  /* Troubleshooting sublines stay in the Details disclosure at every width
     where the row layout is in force — unchanged from the 2026-09-19 pass,
     restated here because that pass scoped them to >= 900px only. */
  #devices .devcard .devplat,
  #devices .devcard .devlock,
  #devices .devcard .devbrowsergrant,
  #devices .devcard .devtelemetry { display: none; }
  /* NOT TWO "Device menu"s. `.devopen` is the identity band's own chip and
     `.device-menu-trigger` is the real focusable button beside it; both say
     t('deviceMenu.title') and both open the same overlay (DEF-005). The
     button is the one that stays. Kept in the DOM — parent-lock-ui.test.js
     and devices-ui.test.js both read it — and hidden only where the real
     button is visible next to it, which below 561px it is not. */
  #devices .devcard .devopen { display: none; }
  #devices .devcard .devnmrow { padding-right: 0; }
}

/* -------- 5. WIDE ROW: SIX NAMED COLUMNS, ONE LINE -----------------------
   1280px, and the number is MEASURED rather than picked: the >= 900px nav
   rail takes ~300px of the window, so the list itself is ~300px narrower
   than the viewport. The six fixed tracks (34 icon + 178 profile + 104 state
   + 112 details + 140 secondary + 44 overflow + 120 primary + gaps) cost
   ~780px, and what is left over is the device-name column.

     window 1220 -> list 920 -> name column 104px: FIVE of six seeded names
                    truncated (measured: nameClipped 36/443/66/25px).
     window 1280 -> list 980 -> name column 164px: only the deliberately
                    absurd 68-character fixture name truncates.
     window 1440 -> list 1116 -> name column 300px.

   So the wide row starts at 1280. Between 561 and 1279 the compact two-line
   row above is the deliberate layout — it gives the name the whole row width
   — not a degraded fallback. */
@media (min-width: 1280px) {
  #devices .devrow.devcard,
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)) {
    grid-template-columns: minmax(432px, 1fr) 112px 140px 44px 120px;
    grid-template-areas: "identity details secondary overflow primary";
    column-gap: 12px;
    row-gap: 6px;
  }
  #devices .devcard:has(> details.healthcap[open]),
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)) {
    grid-template-areas:
      "identity .       secondary overflow primary"
      "details  details details   details  details";
  }
  #devices .devcard .devbtn {
    grid-template-columns: 34px minmax(80px, 1fr) 178px 104px;
    grid-template-areas:
      "icon name profile state"
      "icon seen conn    state";
    column-gap: 12px;
    row-gap: 2px;
  }
  /* Promoted so the child chip and the connection state take the two columns
     the brief names ("profile", and the status column's second line) instead
     of flowing one after the other at a content-dependent x. */
  #devices .devcard .devcontext { display: contents; }
  #devices .devcard .devchild {
    grid-area: profile;
    justify-self: start;
    max-width: 100%;
  }
  #devices .devcard .devconnection {
    grid-area: conn;
    justify-self: start;
    font-size: 11.5px;
  }
  /* The separator dot was a mid-sentence "·" between two inline facts; in two
     stacked cells it becomes the status dot in front of the connection word. */
  #devices .devcard .devchild + .devconnection::before {
    margin-right: 5px;
    content: "\25CF";
    font-size: 8px;
    vertical-align: middle;
  }
  #devices .devcard .devsub.devseen {
    grid-area: seen;
    justify-self: start;
  }
  #devices .devcard .devnmtext { font-size: 17px; }
}

/* PHONE DEVICE INVENTORY — one scannable identity band, two action rows.
   The existing 375px layout stacked Details, Menu, Settings and Pause in four
   separate 44px rows, leaving the everyday control below the fold. These
   same native controls remain visible and keyboard reachable in DOM order:
   identity; Details + Device menu; Settings + Pause/Resume. The compact icon
   already communicates device type; low-priority platform and activity text
   belong in the detailed views rather than crowding the list.
   The requested-but-unconfirmed delivery sentence stays visible below them. */
@media (max-width: 560px) {
  #devices .devrow.devcard,
  #devices .devcard:has(> .devactions > button:nth-of-type(3)),
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)) {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "identity identity identity"
      "details details secondary"
      "overflow primary primary";
    align-items: stretch;
    column-gap: 6px;
    row-gap: 6px;
  }
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)),
  #devices .devcard:has(> .devactions > .devlockstate:not(:empty)):has(> .devactions > button:nth-of-type(3)) {
    grid-template-areas:
      "identity identity identity"
      "details details secondary"
      "overflow primary primary"
      "delivery delivery delivery";
  }
  #devices .devcard:has(> details.healthcap[open]),
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > button:nth-of-type(3)),
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)) {
    grid-template-areas:
      "identity identity identity"
      "details details details"
      "secondary secondary secondary"
      "overflow primary primary";
  }
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)),
  #devices .devcard:has(> details.healthcap[open]):has(> .devactions > .devlockstate:not(:empty)):has(> .devactions > button:nth-of-type(3)) {
    grid-template-areas:
      "identity identity identity"
      "details details details"
      "secondary secondary secondary"
      "overflow primary primary"
      "delivery delivery delivery";
  }
  #devices .devcard > .devbtn { grid-area: identity; min-width: 0; min-height: 44px; }
  #devices .devcard > .healthcap { grid-area: details; min-width: 0; margin: 0; }
  #devices .devcard > .devactions,
  #devices .devcard:has(> .devactions > button:nth-of-type(3)) > .devactions { display: contents; }
  #devices .devcard > .devactions > .device-menu-trigger { grid-area: secondary; }
  #devices .devcard > .devactions > .devdeep { grid-area: overflow; }
  #devices .devcard > .devactions > .devlockbtn { grid-area: primary; }
  #devices .devcard > .devactions > .devlockstate {
    grid-area: delivery;
    min-width: 0;
    margin: 0;
    padding: 0 2px;
    text-align: left;
  }
  #devices .devcard > .devactions > .devforget {
    grid-column: 1 / -1;
    justify-self: end;
  }
  #devices .devcard > .devactions > button { width: 100%; min-width: 0; min-height: 44px; margin: 0; }
  #devices .devcard > .devactions > .devdeep { width: 44px; padding-inline: 0; }
  #devices .devcard .devplat,
  #devices .devcard .devtelemetry { display: none; }
  #devices .devcard .devbtn { padding: 2px 4px; }
  #devices .devcard .devnm { gap: 0; }
  #devices .devcard .devnmrow { min-height: 0; }
  #devices .devcard .devcontext { margin-top: 1px; }
  #devices .devcard .devsub.devseen { margin-top: 1px; }
  #devices .devcard details.healthcap > .healthsummary {
    min-height: 44px;
    padding-inline: 8px;
    gap: 5px;
  }
  #devices .devcard .healthsummary-icon { width: 14px; height: 14px; flex: 0 0 14px; }
  #devices .devcard .healthsummary-label { font-size: 11px; white-space: nowrap; }
  #devices .devcard .healthsummary-state { min-width: 0; font-size: 10.5px; text-align: right; }
}
@media (max-width: 340px) {
  /* Two-line factory names are normal at 320px; recover the 44px-control
     rhythm from the gutter, not by shrinking the device name or touch area. */
  #devices .devrow.devcard,
  #devices .devcard:has(> .devactions > button:nth-of-type(3)) { row-gap: 4px; }
  #devices .devgroup > .devgroupbody > .devcard,
  #devices .devgroup > .devgroupbody > .devcard:only-child { padding-block: 9px; }
}

@media (max-width: 559px) {
  /* This must follow the later group-card overrides above; keep the Online
     heading and group action at 44px while giving the first remote menu full
     clearance above the fixed bottom navigation on a 700px-high phone. */
  body.signed-in:has(#enrolBanner:not(.hide)) #devices:has(#devListTools) {
    margin-top: -20px;
  }
  #devices .devgroup,
  #devices .devgroup:first-child { padding-top: 6px; }
  #devices .devgroup > .devgrouphead {
    min-height: 44px;
    padding-bottom: 6px;
  }
}
