/* FrameBright Secure — parent settings, served from platform.framebright.ai.
 *
 * The palette, the card/pill/toggle vocabulary and the light-theme contrast fixes
 * are carried over from dashboard/index.html so the two parent surfaces do not
 * look like two products. What is NOT carried over is that file's 290 KB of
 * base64 woff2: it embeds Inter and Fraunces inline, and this service is a
 * container whose whole design argument is that it stays small. System fonts
 * instead. MEASURED: this stylesheet is ~9 KB, the dashboard's <style> block is
 * 303 KB, essentially all of it font bytes.
 *
 * PHONE FIRST, and it is not a slogan here — the layout is a single 440px column
 * because that is a phone held in one hand, and the desktop case is that column
 * centred. e2e/mobile.spec.cjs measures the result at 390x844 and 360x800.
 */

:root {
  --ink:#16181D; --dusk:#1E2230; --dusk2:#262B3B; --glow:#FFC24B; --coral:#FF6B5E;
  --mint:#4FD1A1; --sky:#6BA8FF; --cloud:#F7F5EF; --haze:#C9CCD6; --line:#313647;
  --bg:#0E0F13; --statustop:#2C3346; --statusbot:#222838; --arowline:#2d3343; --track:#3a4052;
  --ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

/* Light is opt-in through the toggle (data-theme="light"), and it is ALSO what a
 * visitor whose OS says light gets before app.js has run — the media query below
 * is scoped to :root:not([data-theme]) so it applies only until the toggle has
 * expressed a preference, and never fights it afterwards. Without it a light-mode
 * phone flashes a black page on every load. */
:root[data-theme="light"] {
  --dusk:#FFFFFF; --dusk2:#F0EEE7; --cloud:#1C1E26; --haze:#565C6B; --line:#E0DCD2;
  --bg:#EDEAE1; --statustop:#FFFFFF; --statusbot:#F5F2EB; --arowline:#E4E0D6; --track:#CBCED8;
  --coral:#BC3328; --mint:#0E7C52; --sky:#2C6BD1; --glow:#F2A93B;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --dusk:#FFFFFF; --dusk2:#F0EEE7; --cloud:#1C1E26; --haze:#565C6B; --line:#E0DCD2;
    --bg:#EDEAE1; --statustop:#FFFFFF; --statusbot:#F5F2EB; --arowline:#E4E0D6; --track:#CBCED8;
    --coral:#BC3328; --mint:#0E7C52; --sky:#2C6BD1; --glow:#F2A93B;
  }
  :root:not([data-theme]) .wm b { color: #9C6700; }
  :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; }
  :root:not([data-theme]) .card { box-shadow: 0 1px 3px rgba(0,0,0,.05); }
}

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

body {
  background: var(--bg);
  color: var(--cloud);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.45;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  padding: max(20px, 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%;
}

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

/* 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. */
@media (min-width: 900px) {
  .wrap { max-width: 940px; }
  #devices > .card, #policy > .card, #inv > .card { break-inside: avoid; }
  #devices, #policy, #inv { columns: 2; column-gap: 18px; }
  /* The heading row, the sign-out note and the first-run card span both
     columns: they are about the page, not about one card in it. */
  #devices > .toprow, #devices > .fineprint, #devices > #startCard,
  #policy > .toprow, #policy > .fineprint,
  #inv > .toprow, #inv > .fineprint { column-span: all; }
  /* A card at the top of a column must not inherit the margin that separates it
     from the card above, or the two columns start at different heights. */
  #devices > .card, #policy > .card, #inv > .card { margin-top: 0; margin-bottom: 18px; }
}
.hide { display: none !important; }

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

.brand { display: flex; align-items: center; gap: 12px; margin: 4px 2px 16px; }
.ap {
  position: relative; width: 38px; height: 38px; border-radius: 12px; flex: none;
  border: 3px solid var(--glow); background: var(--dusk);
  box-shadow: 0 0 22px -4px var(--glow);
  transition: border-color .4s ease, box-shadow .4s ease;
}
.ap::after {
  content: ""; position: absolute; inset: 30%; border-radius: 50%;
  background: radial-gradient(circle at 50% 42%, #FFE7B0, var(--glow) 62%);
  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)); }

.wm { font-weight: 700; font-size: 23px; letter-spacing: -.4px; margin: 0; }
.wm b { color: var(--glow); font-weight: 800; }
:root[data-theme="light"] .wm b { color: #9C6700; } /* AA on the cloud page */
.wm small {
  display: block; font-size: 10px; font-weight: 600; letter-spacing: 3px;
  text-transform: uppercase; color: var(--haze); margin-top: -2px;
}

.pagehead { display: block; }
.controls { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin: 0 2px 14px; }
.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: 12px; padding: 12px 14px; margin-bottom: 14px; 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; }
: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; }
.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 */

.card { background: var(--dusk); border: 1px solid var(--line); border-radius: 20px; padding: 18px; margin-bottom: 14px; }
:root[data-theme="light"] .card { box-shadow: 0 1px 3px rgba(0,0,0,.05); }

.sect {
  font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--haze); margin: 0 2px 12px; display: flex; align-items: center; gap: 8px;
}
.sect::after { content: ""; flex: 1; height: 1px; background: var(--line); }

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; }
.muted { color: var(--haze); font-size: 13px; text-align: center; padding: 10px 0; }
.muted:empty { display: none; padding: 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; }

/* 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. */
.inp, .limitin, .timein {
  background: var(--dusk2); 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:focus, .limitin:focus, .timein:focus, .langsel:focus, button:focus-visible {
  outline: 2px solid var(--glow); outline-offset: 1px;
}
select.inp { cursor: pointer; }

/* 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; }

button { font-family: var(--ui); font-weight: 600; cursor: pointer; border: none; border-radius: 12px; }
.primary { width: 100%; background: var(--glow); color: var(--ink); padding: 14px; font-size: 16px; margin-top: 12px; min-height: 48px; }
.primary:disabled { opacity: .55; cursor: default; }
.ghost {
  background: transparent; 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); }

/* An <a> that has to look and behave like a .ghost button. 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. */
.btnlink {
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; box-sizing: border-box;
}

/* 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 */

.devbtn {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: left;
  background: var(--dusk2); border: 1px solid var(--line); border-radius: 14px;
  padding: 13px 14px; margin-bottom: 10px; color: var(--cloud); min-height: 60px;
}
.devbtn .devnm { font-weight: 600; font-size: 15px; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.devbtn .devsub { font-size: 11px; color: var(--haze); margin-top: 2px; font-weight: 400; }
.pill { font-size: 11px; font-weight: 700; padding: 4px 9px; border-radius: 20px; flex: none; }
.pill.on { background: rgba(79,209,161,.18); color: var(--mint); }
.pill.off { background: rgba(255,107,94,.18); color: var(--coral); }

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

.statuscard {
  display: flex; align-items: center; gap: 14px;
  background: linear-gradient(135deg, var(--statustop), var(--statusbot));
  border: 1px solid var(--line); border-radius: 18px; padding: 16px; margin-bottom: 14px;
}
.statustext { flex: 1; min-width: 0; }
.st { font-weight: 700; font-size: 18px; }
.ss { font-size: 12px; font-weight: 600; color: var(--mint); margin-top: 2px; }
.ss.off { color: var(--coral); }
.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); }
.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 */

.arow {
  display: flex; align-items: center; gap: 12px; background: var(--dusk2);
  border: 1px solid var(--arowline); border-radius: 13px; padding: 11px 13px; margin-bottom: 8px;
}
.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; }
.iconbtn { background: transparent; border: 1px solid var(--line); color: var(--haze); border-radius: 10px; padding: 8px 11px; font-size: 12px; flex: none; }
.iconbtn.danger { color: var(--coral); }

.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); }
.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; }

/* 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 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); }
/* 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; }
.chip {
  border: 1px solid var(--line); background: var(--dusk2); color: var(--haze);
  border-radius: 20px; padding: 9px 13px; font-size: 13px; font-weight: 600;
  cursor: pointer; font-family: var(--ui); min-height: 44px; min-width: 48px;
}
.chip[aria-pressed="true"] { background: var(--glow); color: var(--ink); border-color: var(--glow); }

.sched-days-label { margin-top: 12px; }
.dns-label { margin-top: 16px; }
/* 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; }
.timerow { display: flex; gap: 10px; margin: 10px 0 4px; }
.timerow label { flex: 1; font-size: 12px; font-weight: 600; color: var(--haze); }
.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; }
}

/* 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; }
