/* ===========================================================================
   SETTINGS SURFACE — DESIGN FIDELITY
   #preferences ("Settings") and #settings ("Account")
   lane: fidelity-settings · 2026-09-20

   WHY THIS FILE EXISTS. Every number below is MEASURED on both sides by one
   getComputedStyle routine: hers from the binding 2026-09-16 handoff
   (Settings.dc.html, Account.dc.html, served at 127.0.0.1:8799), ours from a
   real seeded server. Raw readings and the before/after renders are in
   .scratch/settings-lane/. This is not a set of spot fixes — it is her SHAPE
   applied to our two screens, property by property.

   WHY IT IS A SEPARATE SHEET. app.css belongs to the coordinator this session,
   and index.html cannot be edited by this lane either. js/overview.js solved
   the same problem first and documents the pattern: the module injects its own
   <link>, which is CSP-legal under `style-src 'self'` (an external same-origin
   stylesheet is not an inline style). If whoever owns index.html would rather
   declare it there, deleting the two injectFidelityStyles() functions in
   js/preferences.js and js/account.js and adding one <link> is the whole
   change.

   WHAT IS DELIBERATELY *NOT* HERE. Three global rules that are genuinely the
   coordinator's call, because they change every screen and not just these two.
   They are written up with current/target values in
   .scratch/fidelity-appcss-settings.patch. The largest:
   `:root[data-theme="light"] .card` (app.css:8189, a 2026-09-04 rule) pins
   `border-color: var(--line)` — .16, the CONTROL weight — over the .08 CARD
   weight that `.card` (app.css:1251) and the owner's own ruling both call for.
   That is live on every card in the product. This file corrects it for these
   two mounts only, by ID specificity, and does not reach past them.

   HER CONSTANTS, which everything below is derived from:
     page column   820px, flush left against the content gutter
     card          #FFF, 1px rgba(22,35,63,.08), radius 22, padding 26,
                   shadow 0 1px 2px rgba(22,35,63,.03),
                          0 6px 18px rgba(22,35,63,.04);  16px between cards
     card title    16.5px/800, ink, and NO rule drawn after it
     field label   12.5px/700, ink
     control       1px rgba(22,35,63,.16), radius 12, 48px tall
     row           flex, gap 16, padding 0 0 16, 1px bottom hairline;
                   the LAST row has neither the padding nor the hairline
     row title     14px/700 ink · row sub 12.5px/400 muted
     brand pill    #E8F1FA / #1B4A73, 12px/800, 5px 13px, 999px
     muted pill    #F1F3F6 / grey,    12px/800, 5px 13px, 999px
     buttons       999px; primary 11px 20px 13.5px/700,
                          outline  10px 18px 13px/700

   THREE PLACES THIS DELIBERATELY DOES NOT COPY HER, each for a reason that is
   already written down somewhere in this repo:
     1. control FILL — hers is #FFFFFF, ours stays var(--field-fill) #F5F7F9.
        The owner ruled it; app.css:1320 records the 1.00:1 contrast measured
        on production that caused the ruling.
     2. muted TEXT — hers is #8A93A3, which is 3.0:1 on white. app.css's own
        token note reserves --text-tertiary-ui (#8A93A3) for NON-TEXT uses and
        gives glyphs --text-tertiary (#6D778A). Every muted line below uses
        var(--text-tertiary): one step darker than hers, and it keeps 1.4.3.
     3. control FONT-SIZE — hers is 15px, ours stays 16px. app.css:1315
        ("Everything a thumb types into here is 16px") is an iOS zoom decision.

   TOKENS, NOT LITERALS. Her hairline measures rgba(22,35,63,.07) and
   --border-subtle is rgba(22,35,63,.08); the 0.01 is invisible and the token
   is what carries the value into dark theme, where a hardcoded light-theme
   rgba would have vanished. Same for every colour below.
   =========================================================================== */


/* ---------------------------------------------------------------- 1. COLUMN
   HER PAGE IS ONE 820px COLUMN, FLUSH LEFT — not a centred band and not two
   tracks. Ours was max-width 1160 (rendering 1140) split into two 561px
   columns at >=900px, which is the single biggest reason these screens did not
   read like hers: her rows are 766px wide and ours were 1094px, so the same
   copy sat in a completely different measure.
     max-width     1160px -> 820px
     margin-inline auto   -> 0        (hers starts hard against the gutter:
                                       x=286 on a main that starts at 230)
     columns       repeat(2, 1fr) -> one track,  gap 18px -> 16px            */
#settingsMount,
#preferencesMount {
  max-width: 820px;
  margin-inline: 0;
}
@media (min-width: 900px) {
  #settingsMount,
  #preferencesMount {
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
  }
}
@media (min-width: 1400px) {
  /* app.css gives #householdProcessesMount and #preferencesMount two tracks
     together at this width. Named alone here so the other lane's screen is
     left exactly as it is. */
  #preferencesMount { grid-template-columns: minmax(0, 1fr); }
}


/* ------------------------------------------------------------ 2. PAGE HEAD
   `.ovhead .ov-*` is shared with Home, Overview and the child screens, so this
   is scoped to these two mounts rather than changed at source.
     eyebrow  12.5px -> 13px · letter-spacing 1px -> 1.04px
              colour var(--brand) #2B6CB0 -> var(--brand-deep) #1B4A73
     h1       32px/48 -> 30px/45 · letter-spacing -.32px -> -.3px
     sub      16px -> 15.5px                                                 */
#preferencesMount > .ovhead .ov-eyebrow,
#settingsMount > .ovhead .ov-eyebrow {
  font-size: 13px;
  letter-spacing: 1.04px;
  color: var(--brand-deep);
}
#preferencesMount > .ovhead .ov-greet,
#settingsMount > .ovhead .ov-greet {
  font-size: 30px;
  line-height: 45px;
  letter-spacing: -.3px;
}
#preferencesMount > .ovhead .ov-sub,
#settingsMount > .ovhead .ov-sub { font-size: 15.5px; }


/* ---------------------------------------------------------------- 3. CARDS
   padding 22px -> 26px (hers, on all five Settings cards and both Account
   cards — she uses one value), and the CARD edge restored to the .08 the
   owner ruled and `.card` already asks for. See this file's header for why
   that is not simply fixed at source: `:root[data-theme="light"] .card`
   (0,3,0) pins .16 globally and `#preferencesMount > .card` (1,1,0) is how
   this lane reaches past it without changing anyone else's screen.          */
#preferencesMount > .card,
#settingsMount > .card {
  padding: 26px;
  border-color: var(--border-subtle);
}


/* ----------------------------------------------------------- 4. CARD TITLE
   THE TRAILING GRADIENT RULE GOES. `.sect::after` (app.css:1259) paints
   `linear-gradient(90deg, var(--line), transparent)` across whatever the
   title does not fill. NONE of her 49 artboards has it; her card titles are
   plain text. This is the detail that makes our cards read as a developer UI
   and hers as a product.

   BEHAVIOUR CHECK (the ed3eaf68 trap, where a pure-CSS display rule silenced
   every confirmation message with zero test failures): this hides a ::after
   PSEUDO-ELEMENT. No JS can write into one — `content` is the only thing that
   ever fills it, and here that content is the literal "". Every
   `.textContent =` / `.innerHTML =` write site in js/preferences.js and
   js/account.js was checked against this selector and none is reachable by
   it; `.sect` itself is untouched and still renders its heading. Nothing
   user-facing goes silent.

   `.sect`'s own `margin: 0 2px 12px` also insets the title 2px from the card's
   content edge, so the heading sat 2px right of the help text and the fields
   beneath it. Hers are flush. Her title-to-next-element gap is 18px.         */
#preferencesMount .sect::after,
#settingsMount .sect::after { display: none; }
#preferencesMount > .card > .sect,
#preferencesMount > .card > .cardhead > .sect,
#settingsMount > .card > .sect,
#settingsMount > .card > .cardhead > .sect { margin: 0 0 18px; }
#preferencesMount > .card > .cardhead,
#settingsMount > .card > .cardhead { margin-bottom: 0; }


/* ------------------------------------------------------- 5. LABELS & FIELDS
   Her field labels are DARK and bold — they read as part of the form. Ours
   were muted grey at 600, which reads as help text.
     .fld   12px/600 var(--haze) -> 12.5px/700 var(--text-primary)
            margin 0 2px 7px -> 0 0 8px   (hers: no inset, 7px to the control)
     .help  12px var(--haze) -> 12.5px var(--text-tertiary)
     .inp   padding 13px -> 11px 14px, which lands her 48px with our 16px text
            (16px text + 13px block padding + 2px border measured 52px)
     two-up field row gap 14px -> 16px (hers: 704 - 313 - 375)               */
#preferencesMount .fld,
#settingsMount .fld {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0 0 8px;
}
#preferencesMount .help,
#settingsMount .help {
  font-size: 12.5px;
  color: var(--text-tertiary);
}
#preferencesMount .inp,
#settingsMount .inp {
  padding: 11px 14px;
  min-height: 48px;
}
#preferencesMount .preferences-fields {
  gap: 16px;
  margin-top: 18px;
}


/* ------------------------------------------------- 6. THE DESTINATION ROWS
   THE BIG ONE, and the thing the owner has been pointing at. Ours was a
   2-column grid of nested boxed cards with a centred orphan on the last row.
   HERS is a single column of plain rows separated by a hairline, inside one
   card — the same shape as her Notifications rows, her Two-step row and both
   her Danger-zone rows, which are all one pattern.

   6a · the container: two tracks -> one, gap 12 -> 0 (her rows are separated
   by their own padding and rule, not by grid gap).                          */
#preferencesMount .preference-destination-grid {
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  margin-top: 18px;
}

/* 6b · THE CENTRED ORPHAN. app.css:8681 gives the LAST destination
   `width: min(100%,760px); justify-self: center`. In a 2-column grid that
   stopped a card-sized hole; in a 1-column list it renders the final row
   narrower and centred — the floating "Help" box visible in
   .scratch/settings-lane/ours-preferences-full.png. The app.css rule and the
   source-grep assertion that pins it (test/settings-billing-ui.test.js:131)
   are both LEFT IN PLACE and simply neutralised here, so nothing goes red. */
#preferencesMount .preference-destination-grid > .preference-destination:last-child {
  width: auto;
  justify-self: stretch;
}

/* 6c · the row. Measured on hers: flex, gap 16, padding 0 0 16, 1px bottom
   hairline, and the last row has neither.
     border        1px all round var(--line) -> bottom only, --border-subtle
     border-radius 15px -> 0 · background var(--dusk2) -> none
     padding       14px -> 0 0 16px · gap 12px -> 16px                       */
#preferencesMount .preference-destination {
  border: 0;
  border-bottom: 1px solid var(--border-subtle);
  border-radius: 0;
  background: none;
  padding: 0 0 16px;
  margin-top: 16px;
  gap: 16px;
}
#preferencesMount .preference-destination:first-child { margin-top: 0; }
#preferencesMount .preference-destination:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

/* 6d · the "primary" first row. app.css:8672 tints it with --glow at 9% and
   borders it at 42%. SHE HAS NO TINTED ROW ANYWHERE — in her design
   prominence comes from ORDER and from the action's own wording. The row
   keeps its `is-primary` class (portal-family-ia-modules.test.js asserts
   destinations[0] carries it, and it is still the semantic first doorway),
   keeps its position, and keeps its distinct "Choose a device" action. It
   only loses the tinted box. */
#preferencesMount .preference-destination.is-primary {
  background: none;
  border-color: var(--border-subtle);
  padding: 0 0 16px;
}

/* 6e · the icon. GoodQA ticket 488's inline-stroke SVG SURVIVES — only the
   38px bordered box around it goes, because her rows carry no icon chrome.
   NOTE the fallback: js/preferences.js writes a '›' glyph into this span when
   ctx.icon() returns nothing. 24px with the inherited 16px/850 type still
   renders it; this box is deliberately not sized to zero or clipped. */
#preferencesMount .preference-destination-icon {
  width: 24px;
  height: 24px;
  border: 0;
  background: none;
  border-radius: 0;
  align-self: center;
}
#preferencesMount .prefdesticon { width: 22px; height: 22px; }

/* 6f · row copy. title 15px -> 14px, margin-bottom 3px -> 2px (hers: title
   bottom 825, sub top 827). sub 12px -> 12.5px, var(--haze) ->
   var(--text-tertiary), line-height 1.4 -> 1.5. */
#preferencesMount .preference-destination-title {
  font-size: 14px;
  margin: 0 0 2px;
}
#preferencesMount .preference-destination .asub {
  font-size: 12.5px;
  color: var(--text-tertiary);
  line-height: 1.5;
}

/* 6g · the right-hand cluster. NEW element, added by js/preferences.js in the
   same change: `.preference-destination-meta` holds the scope pill and the
   action side by side — exactly her Two-step row, where a muted "Not enabled"
   pill sits 10px from a "Turn on" button. */
#preferencesMount .preference-destination-meta {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}

/* 6h · the scope pill, now quiet metadata beside the action rather than an
   uppercase chip stacked above the title. Her MUTED pill, measured off "Not
   enabled": fill #F1F3F6 (= --surface-raised), no border, 12px/800, 5px 13px,
   999px, sentence case.
     font-size 10px -> 12px · padding 3px 7px -> 5px 13px · border 1px -> 0
     background var(--brand-tint) -> var(--surface-raised)
     colour     var(--brand) -> var(--text-tertiary)
     letter-spacing .06em -> 0 · text-transform uppercase -> none
   text-transform does not change textContent, so the /Per device/ assertion
   in portal-family-ia-modules.test.js is unaffected by the case change. */
#preferencesMount .preference-destination .preference-scope {
  font-size: 12px;
  line-height: 18px;
  padding: 5px 13px;
  border: 0;
  background: var(--surface-raised);
  color: var(--text-tertiary);
  letter-spacing: 0;
  text-transform: none;
  margin: 0;
  white-space: nowrap;
}

/* 6i · the action. Her outline button: 999px, 10px 18px, 13px/700, white fill.
   min-height stays our 44px rather than her 41.5px — that is the touch-target
   floor and 2.5px is not worth losing it over. */
#preferencesMount .preference-destination-open {
  font-size: 13px;
  padding: 10px 18px;
}


/* ------------------------------------------------------ 7. CARD-HEAD PILL
   "This dashboard". Her BRAND pill, measured off "Family plan": #E8F1FA fill,
   #1B4A73 text, no border, 12px/800, 5px 13px. Distinct from the muted row
   pill above, exactly as she distinguishes the two.                         */
#preferencesMount .cardhead > .preference-scope,
#settingsMount .cardhead > .preference-scope {
  font-size: 12px;
  line-height: 18px;
  padding: 5px 13px;
  border: 0;
  background: var(--brand-tint);
  color: var(--brand-deep);
  letter-spacing: 0;
  text-transform: none;
}


/* ----------------------------------------------------------- 8. SAVE ROW
   Her card footer is a secondary on the left and the primary hard right
   (Account.dc.html: "Change password →" / "Save changes"). Ours keeps the
   live status message in that left slot, which is the same shape and must
   stay adjacent to the button for aria-live.

   `row-reverse` WITHOUT a justify-content override is deliberate: in
   row-reverse the main axis starts at the RIGHT, so the first DOM child (the
   button) lands hard right and the message fills the space to its left.
   Adding `justify-content: flex-end` would have pushed the button LEFT.
   app.css's `#preferencesLive:empty { display: none }` is left exactly as it
   is, so a pristine form shows the button alone, on the right, where her Save
   sits.
     .primary  min-width 180px -> 0 · padding 14px 24px -> 11px 20px
               font-size 16px -> 13.5px                                      */
#preferencesMount .preferences-actions {
  flex-direction: row-reverse;
  margin-top: 18px;
}
#preferencesMount .preferences-actions > .primary {
  min-width: 0;
  padding: 11px 20px;
  font-size: 13.5px;
}
#preferencesMount .preferences-actions > #preferencesLive {
  flex: 1 1 auto;
  font-size: 12.5px;
  color: var(--text-tertiary);
}


/* ================================================== 9. #settings (Account) */

/* The first Account view shows the same current plan that billing.js has
   already validated and rendered in its full workspace. This is a compact
   doorway, placed after identity and before the section chooser. No price or
   entitlement state is synthesized in CSS or in account.js. */
#settingsMount > #settingsPlanSummary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 16px 24px;
  padding: 20px 24px;
}
#settingsMount > #settingsPlanSummary[hidden] { display: none; }
#settingsPlanSummary .account-plan-content { min-width: 0; }
#settingsPlanSummary .account-plan-label {
  color: var(--text-tertiary);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
}
#settingsPlanSummary .account-plan-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-top: 4px;
}
#settingsPlanSummary .account-plan-name {
  color: var(--ink);
  font-size: 19px;
  line-height: 1.25;
  overflow-wrap: anywhere;
}
#settingsPlanSummary .account-plan-detail {
  color: var(--text-tertiary);
  font-size: 13px;
  line-height: 1.4;
  margin: 5px 0 0;
}
#settingsPlanSummary .account-plan-action {
  min-height: 44px;
  white-space: normal;
  text-align: center;
}
@media (max-width: 600px) {
  #settingsMount > #settingsPlanSummary {
    grid-template-columns: minmax(0, 1fr);
    padding: 18px;
  }
  #settingsPlanSummary .account-plan-action { width: 100%; }
}

/* 9a · THE IDENTITY TILE. Her Account identity is an avatar beside two lines,
   inside the card, with no fill of its own. Ours was a tinted, brand-bordered
   cell. The three counts beside it STAY — they are real server-side COUNT(*)s
   that her Account does not show and we must not drop — but the identity cell
   loses the tint so it reads as her identity row rather than a highlighted
   box. gap 13px -> 14px (hers). */
#settingsTiles > .ovtile.acctowner {
  background: var(--dusk);
  gap: 14px;
}

/* 9b · the strip's grid. `.ovtiles.acctsum` is
   `minmax(240px,1.55fr) repeat(3, minmax(110px,.55fr))`, tuned for a 1140px
   column. At her 820px that gives the identity cell 396px and the counts
   141px each, and "can sign in to this household" / "linked to this
   household" both wrap to two lines — MEASURED in the first render at the new
   width. Her own Account identity row is full-width inside its card, so give
   the identity cell its own row and let the three counts share the next one:
   her shape, and 3 x ~264px, which is what stops the wrap. app.css:7292
   already uses this exact `grid-column: 1 / -1` form at another breakpoint. */
@media (min-width: 900px) {
  #settingsTiles.ovtiles.acctsum { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  #settingsTiles.ovtiles.acctsum > .ovtile.acctowner { grid-column: 1 / -1; }
}

/* 9c · the avatar. Hers is 52x52 with a 20px initial. `.avatar.lg` is 46/16
   and is shared with the sidebar user row and several other screens, so
   js/account.js tags this ONE mark with `.acct-owner-avatar` and only it is
   resized. This is the single rule in this file not scoped to a mount,
   because the class exists nowhere else. */
.avatar.acct-owner-avatar { width: 52px; height: 52px; font-size: 20px; }

/* 9d · identity type. Hers: name 16.5px/800 ink, then one 13px/400 muted line.
   Ours has four lines because it also states the address and the role, both of
   which are load-bearing honesty (see personTile()'s own comment) and stay. */
#settingsTiles > .ovtile.acctowner .acct-name { font-size: 16.5px; }
#settingsTiles > .ovtile.acctowner .acct-name + .acct-mail {
  font-size: 13px;
  font-weight: 400;
  color: var(--text-tertiary);
}
#settingsTiles > .ovtile.acctowner .ovt-s {
  font-size: 13px;
  color: var(--text-tertiary);
}

/* 9e · THE GROUP RAIL. app.css:8550 gives each chip `flex: 1 1 150px`, which
   stretches five chips across the full column — at 1140px they rendered 220px
   wide and read as five buttons rather than as a filter. Her pill is
   content-width. Her horizontal padding (13px) is ALREADY ours, so padding is
   untouched; her pill is 28px tall and ours stays 44px because `.chip`'s
   min-height is our touch-target floor and app.css:8797 carries a MEASURED
   note about a previous pass that pulled it to 40. Recorded as a deliberate
   deviation, not drift.
     flex 1 1 150px -> 0 0 auto · font-size 13px -> 12px · weight 600 -> 800  */
#settingsTabs.account-tabs > .chip {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 800;
}

/* 9f · disclosure summaries, to her row type. */
#settingsMount .account-disclosure-copy > strong { font-weight: 700; }
#settingsMount .account-disclosure-copy > span {
  font-size: 12.5px;
  color: var(--text-tertiary);
}

/* 9g · NOTIFICATION PREFERENCE SWITCHES (design-fidelity finding F4.6, never
   landed by the fidelity-settings lane -- grep confirms zero prior mentions
   of "pp-switch"/"checkbox" in this file). Three `<input type="checkbox">`
   at the browser's own default size, ~13x13, MEASURED against a real
   dark-theme render as a stark black square with a thin white check --
   the one unthemed native control left on either account screen, next to
   Parent Guard's and the AI filter's full-size switches.

   THE REAL <input>, RESTYLED, NOT REPLACED. `appearance: none` strips the
   native box and this file repaints it; the element, its id, its `change`
   event and its `.checked` property are all untouched, so
   app.js's paintNotificationPreferences()/saveNotificationPreference() (the
   functions that read and write these three boxes) need no change at all.
   Swapping in a <div> would have been the actual regression here: label
   association, native keyboard toggling (Space) and :focus-visible all come
   free with a real input and would all have to be rebuilt by hand without
   one.

   SIZED AND COLOURED OFF `.toggle` (app.css:1665) ON PURPOSE, not invented:
   the same mint/track pair and white-thumb-with-shadow Parent Guard and the
   AI filter already use, so a parent sees one switch language across Account,
   not two. 46x28 rather than .toggle's 58x32 -- the brief's own number, and
   the smaller of the two footprints these three rows (already the most
   text-dense card on the screen) can spare. The ::before hit-target and
   ::after thumb split mirrors .toggle's own two-pseudo-element structure
   (app.css:1667, :2213) at the SAME 44px floor
   (`max(100%, 44px)`, app.css:2213's comment on why: an audited hit-test, not
   a guess) -- copied rather than shared, since .toggle's own
   `.toggle::before` selector is scoped to a family of BUTTON-based controls
   and this is still a real checkbox input, not a button. */
#notifPrefCard .consentRow { align-items: center; }
#notifPrefCard .consentRow input.pp-switch { margin-top: 0; }
.pp-switch {
  appearance: none; -webkit-appearance: none;
  width: 46px; height: 28px;
  margin: 0; padding: 0; border: none; flex: 0 0 auto;
  border-radius: 999px;
  /* GOODQA #32 — a real switch (a restyled checkbox input, same role as
     `.toggle`/`.sw`), so its unchecked track needs the same 3:1 fix — see
     app.css's `--track-switch` token comment. settings-account-ui.test.js's
     own "notification preferences" test measures this control's contrast in
     PARITY with a live `.toggle.off` probe, so leaving this on the shared
     `--track` while `.toggle.off` moved to `--track-switch` would fail that
     test for the right reason: a real, second instance of this ticket's
     defect. */
  background: var(--track-switch);
  position: relative;
  transition: background .2s;
  cursor: pointer;
}
.pp-switch::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: max(100%, 44px); height: max(100%, 44px);
  /* Purely a hit surface, same as .toggle::before -- nothing painted. */
  background: transparent;
}
.pp-switch::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  background: #fff;
  transition: left .2s;
  box-shadow: 0 2px 6px rgba(0,0,0,.4);
}
.pp-switch:checked { background: var(--mint); }
.pp-switch:checked::after { left: 21px; }
.pp-switch:disabled { opacity: .5; cursor: default; }
:root[data-theme="light"] .pp-switch:disabled { opacity: .72; cursor: not-allowed; }
/* Same 2px glow ring every other control on this screen gets
   (app.css:1338); a plain checkbox input is not in that shared selector
   list, so without this line the restyled switch was the one control on
   the card a keyboard user could not see focus land on. */
.pp-switch:focus-visible { outline: 2px solid var(--glow); outline-offset: 2px; }

/* 9h · THE THREE PRIVACY-CHOICES CHECKBOXES, same root cause 9g's own comment
   names and app.css:6832's "THE CHECKBOX FIX" already fixed twice elsewhere
   (.device-menu-panel; .loginpanel's #rememberMeRow/#signupTermsRow): index.html's
   unconditional `<meta name="color-scheme" content="light dark">` with no
   `color-scheme` CSS property declared for these controls, so a dark-preferring
   OS/browser paints its own near-black checkbox over this card regardless of
   this app's own `data-theme` toggle. MEASURED (Playwright, colorScheme:'dark'
   context, data-theme forced to 'light', #privacyCard) as a stark black square
   for #consentProduct/#consentMarketing/#consentAttribution --
   verification/portal-v2/settings-lane/BEFORE-privacycard-dark-os-light-theme-clip.png --
   the exact defect 9g's own header describes for the notification-preference
   boxes, left behind here because 9g's finding (F4.6) never looked at this
   card. #consentAiClassify/#consentAppClassify (aiFilterCard/parentGuardCard,
   same `.consentRow` markup) share the identical unstyled `<input
   type="checkbox">` and the identical bug, so they are fixed by the same rule
   rather than a second one.

   SCOPED TO THESE THREE CARDS, NOT A GLOBAL `.consentRow` RULE -- same
   reasoning app.css:6838-6841 gives for its own narrow scope: `.consentRow` is
   shared well beyond this screen (`.schedule-app-list .consentRow` among
   others), and repainting every native control across the app is its own
   verified pass, not a drive-by from this lane. accent-color uses `--glow`,
   the same token app.css:6845's identical fix already uses for the login
   card's own two checkboxes, so a parent sees one checked-checkbox colour
   across the product rather than two. */
#privacyCard .consentRow,
#aiFilterCard .consentRow,
#parentGuardCard .consentRow {
  color-scheme: light;
}
:root[data-theme="dark"] #privacyCard .consentRow,
:root[data-theme="dark"] #aiFilterCard .consentRow,
:root[data-theme="dark"] #parentGuardCard .consentRow {
  color-scheme: dark;
}
#privacyCard .consentRow input[type="checkbox"],
#aiFilterCard .consentRow input[type="checkbox"],
#parentGuardCard .consentRow input[type="checkbox"] {
  accent-color: var(--glow);
}

/* GoodQA follow-up, MEASURED via real Tab presses in dark theme: none of
   these four cards' plain `<input type="checkbox">` (no `.pp-switch` class,
   unlike notifPrefCard's three switches, which already carry their own
   `.pp-switch:focus-visible` rule further down this file) declares a focus
   ring anywhere, so a focused one falls back to the browser default, which
   resolves to `currentColor` -- this element's own near-black text colour
   (rgb(16,16,16)) -- painted on the dark theme card background
   (rgb(16,27,45)): 1.10:1, effectively invisible. Light theme happens to
   clear a usable ratio by the same coincidence (near-black on a light card),
   which is exactly why this went unnoticed. Same `--glow` ring and 2px
   offset as `.pp-switch:focus-visible` below and app.css:1360's product-wide
   default, for one consistent focus treatment. SCOPED to these four cards,
   not a global `input[type="checkbox"]:focus-visible` rule, same reasoning
   as the two blocks above: a plain checkbox appears elsewhere in the product
   and a global change is its own verified pass. */
#privacyCard .consentRow input[type="checkbox"]:focus-visible,
#aiFilterCard .consentRow input[type="checkbox"]:focus-visible,
#parentGuardCard .consentRow input[type="checkbox"]:focus-visible,
#reportCard .consentRow input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 2px;
}

/* GoodQA follow-up, MEASURED at 768x1024 and 1024x768 (the tablet pair this
   suite's other layout sweeps already use): these four cards' `.consentRow`
   rows -- a plain, unstyled `<input type="checkbox">` beside one line of
   text, `align-items: flex-start` from app.css's shared rule -- rendered
   22px tall, 2px under WCAG 2.2 SC 2.5.8's 24px minimum target size (the
   normative floor; this product's own stricter 44px "touch floor" language
   elsewhere, e.g. `.iconbtn`'s comment in app.css, is for isolated square
   icon buttons, not a full-width list row whose WIDTH already spans the
   whole card). notifPrefCard's own three consentRow switches are unaffected
   and stay at their existing 28px (`.pp-switch` is a taller element than a
   native checkbox) -- this floor only lifts the three rows that were
   actually short. SCOPED, NOT A GLOBAL `.consentRow` RULE, same reasoning as
   the color-scheme fix immediately above: `.consentRow` is shared well
   beyond this screen and a global height floor is its own verified pass. */
#privacyCard .consentRow,
#aiFilterCard .consentRow,
#parentGuardCard .consentRow,
#reportCard .consentRow {
  min-height: 24px;
}


/* ============================================================== 10. NARROW
   Checked at 390x844 and with a 60-character child name.                    */
@media (max-width: 899px) {
  /* The right-hand cluster cannot sit beside the copy on a phone: at 390px the
     copy column is ~200px and "Account-wide" + "Manage" is ~190px. Drop to her
     stacked form — copy, then the cluster beneath it, left aligned.
     `.preference-destination` is already one track at this width
     (app.css:8808). */
  #preferencesMount .preference-destination {
    grid-template-columns: 24px minmax(0, 1fr);
    row-gap: 10px;
  }
  #preferencesMount .preference-destination-meta {
    grid-column: 2;
    justify-content: flex-start;
  }
  #preferencesMount > .card,
  #settingsMount > .card { padding: 20px; }
  #preferencesMount .preferences-actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  #preferencesMount .preferences-actions > .primary { width: 100%; }
}


/* =================================================== 11. SHORT-VIEWPORT DOCK
   CLEARANCE FOR "Save changes" — found and hit-tested during the
   screen-state-settings lane (2026-09-23), at 320x700 (the required narrow
   "dialog floor" viewport from SCREEN-STATE-COVERAGE-2026-09-22.md), NOT
   present at 375x812 (the other required phone viewport, 112px taller).

   MEASURED (Playwright, real signed-in session, real click, no scrolling —
   the page's natural top-of-document paint): at 320x700 #preferencesSave's
   own box is y:[608,658] and the fixed bottom dock (#nav, app.css's
   `body.signed-in` phone shell) is y:[635,700] — a 23px overlap.
   `document.elementFromPoint` at the button's own bottom quarter and
   near-bottom (75%/95% of its height) resolved to `#navRow`, not the button:
   roughly the bottom half of "Save changes" is genuinely un-clickable on
   first paint at this height, not merely visually crowded. (The button's own
   center still resolves to itself and a real .click() there still succeeds —
   this is a partial, height-dependent overlap, not a total block.)

   WHY app.css's OWN EXISTING FIX (html { scroll-padding-bottom }, this same
   file's file-header quotes its comment) does not reach this case: that
   property only changes where a PROGRAMMATIC scroll (scrollIntoView/focus)
   comes to rest. Nothing scrolls on a cold load — the parent has not
   interacted with anything yet — so a control that already sits in the
   dock's fixed footprint at scroll position zero is not helped by it. This
   screen's short first card (two fields, no long device/child list above it)
   is what makes its Save row land unusually close to the fold at a 700px
   viewport height; it is a plausible, height-triggered rather than
   width-triggered, defect and does not follow the design handoff to any
   pinned value — the artboards are not drawn at 320px in the first place —
   so trimming THIS row's own top margin, THIS narrowly, contradicts no
   measured fidelity number in this file.

   SCOPED BY HEIGHT, NOT ONLY WIDTH, so an ordinary phone in portrait
   (375x812, 390x844 — every viewport this file's other sections were
   measured against) is completely unaffected; only a viewport at or below
   the 899px width breakpoint AND at or below a height where the dock can
   plausibly reach this row is touched. 760px clears 700 with headroom and
   stays under 812.

   TWO SMALL SHAVES, NOT ONE LARGE ONE, and both MEASURED: `.preferences-
   actions`'s own 18px top margin (section 8 above) to 0, plus this card's
   `.sect` title-to-body gap (section 4 above, her OWN 18px, shared with
   #settingsMount so left untouched there — this rule is scoped to
   `#preferencesMount` only) to 10px. Together: overlapPx 23 -> 0, and
   elementFromPoint at 25/50/75/95% of the button's own height all resolve
   to `#preferencesSave` afterward, at the exact 320x700 floor this was
   measured against. */
@media (max-width: 899px) and (max-height: 760px) {
  #preferencesMount .preferences-actions { margin-top: 0; }
  #preferencesMount > .card > .sect,
  #preferencesMount > .card > .cardhead > .sect { margin: 0 0 10px; }
}
