/* Household Apps — sheet for js/household-apps.js.
 *
 * Everything reusable (.card, .pill, .chip, .catchip*, .ovtiles/.ovtile,
 * .banner, .muted, .an/.asub, .sr-only) is already defined in app.css. This
 * file only adds what is genuinely new here: the tab bar, the search/sort/
 * child-filter row, and the two real <table>s.
 *
 * WHY A REAL <table> HERE, WHEN app.css'S OWN .devprov COMMENT SAYS NOT TO:
 * that comment is about a fixed 3-column fact grid, where a table would add a
 * sideways scroll for no reason. The Allowed/Locked view is a genuine matrix —
 * one column per child, and a household can have several — so column count is
 * unbounded by this file's control. The `.happs-table-wrap` below is the
 * contained horizontal scroller that keeps the scroll INSIDE this card
 * instead of the page ever scrolling sideways at 320–400px, which is the same
 * outcome that comment is protecting, reached the way that fits a real
 * matrix instead of a fixed few columns.
 *
 * ---------------------------------------------------------------------------
 * 2026-09-20 DESIGN FIDELITY (owner ticket L21, his own screenshot of the
 * "<child> — unknown-app default & AI category filters" card). His words:
 * "large, uneven vertical gaps ... it reads as unfinished rather than airy".
 *
 * NOT spot fixes. Every number below was MEASURED on the binding 2026-09-16
 * handoff with the same getComputedStyle routine that measured ours, on
 * KidApps.dc.html and RequestsEmpty.dc.html. Her system, as measured:
 *
 *   card            #FFF · 1px rgba(22,35,63,.08) · r22 · pad 26 · --shadow-card
 *   card title      16.5px/800 --ink                     margin-bottom  6px
 *   group label     12.5px/700 UPPERCASE ls .5 #8A93A3   margin-bottom 10px
 *   description     14px/400 lh1.5 #5B6472               margin-bottom 20px
 *   control         r12 · 1px rgba(22,35,63,.16) · pad 13/14 · 15px
 *   filter tab ON   #2B6CB0 fill, white, r999, pad 9/18, 13.5px/700
 *   filter tab OFF  transparent, #5B6472, 1px rgba(22,35,63,.14), same metrics
 *   empty state     IN a card: r50% 56px #F1F3F6 disc, 16.5/800 title,
 *                   13.5/400 #8A93A3 body, all centred, card pad 56/32
 *
 * Her greys are already tokens here and are reused rather than re-typed:
 *   #8A93A3 = --text-tertiary-ui   #5B6472 = --text-secondary
 *   #F1F3F6 = --surface-raised     #F5F7F9 = --field-fill
 *   #2B6CB0 = --brand              rgba(22,35,63,.08) = --border-subtle
 *
 * CSS-THAT-IS-REALLY-BEHAVIOUR AUDIT (commit ed3eaf68's lesson), done before
 * writing a line: NOTHING in this pass sets display, visibility or opacity on
 * anything. Every rule here moves text-align / padding / margin / gap / colour
 * / radius / font. The module's user-facing feedback nodes — .happs-ai-state,
 * .happs-reach, .happs-error, and the empty-state text — are all still
 * rendered and still readable; two of them are FIXED here, because
 * app.css's `.muted { text-align:center }` was centring a status line inside
 * an otherwise left-aligned card. `.muted:empty { display:none }` in app.css
 * is untouched and still collapses a genuinely empty node.
 * ---------------------------------------------------------------------------
 */

#householdAppsMount { grid-column: 1 / -1; min-width: 0; }

.happs-add-task {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin: 0 0 16px;
  padding: 16px 18px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--glow) 9%, var(--dusk));
}
.happs-add-copy { min-width: 0; }
.happs-add-copy .sect { margin-bottom: 4px; }
/* `.sect::after` trails a hairline gradient off the end of the title. Nothing
   in her design does that, and inside a tinted band with a control sitting on
   the right of the same line it reads as a rule that failed to reach the edge
   rather than as a divider. */
.happs-add-copy .sect::after { content: none; }
.happs-add-copy .help { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-secondary); }
.happs-add-controls { display: flex; align-items: center; gap: 8px; flex: none; }
.happs-add-controls .inp { min-width: 150px; min-height: 44px; }
/* "Choose an app" was wrapping onto two lines inside the pill, which made the
   one primary action on this band look like a mistake. */
.happs-add-controls .primary { width: auto; min-height: 44px; margin: 0; padding: 10px 18px; font-size: 14px; white-space: nowrap; }

/* THREE TILES WERE SITTING IN A FOUR-COLUMN GRID. `.ovtiles` is
   `repeat(auto-fit, minmax(180px, 1fr))`, which at this screen's width
   resolves to four 274.5px tracks — MEASURED, not read — so Waiting/Allowed/
   Locked occupied three of them and a 274px gutter sat empty on the right,
   which is what made the row look like something had failed to load. Above
   720px the count is fixed at the three tiles that exist; below it the
   inherited auto-fit still wraps them. */
.happs-summary { margin-bottom: 14px; }
@media (min-width: 720px) {
  .happs-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.happs-controls { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }

/* HER FILTER-PILL ROW (RequestsEmpty.dc.html: Pending/Approved/Denied/All).
   She has two tab idioms and this is the household one — separate pills with a
   SOLID BRAND FILL on the selected pill — not KidApps' segmented grey track,
   which is her per-child sub-nav. Requests/Allowed/Locked is the same kind of
   control as her Pending/Approved/Denied, so it gets the same shape.

   THE SELECTED PILL HAD NO STYLE AT ALL BEFORE THIS. app.css styles
   `.chip[aria-pressed="true"]`, and renderControls() sets `aria-selected`
   (correct for role="tab" — aria-pressed is for buttons, and changing the
   markup would be wrong twice over). So the attribute the stylesheet was
   waiting for was never set, and all three tabs rendered identically: nothing
   on this screen told a parent which view they were looking at. Found by
   measuring all three pills, not by reading the sheet. */
.happs-tabs { margin: 0; gap: 8px; }
.happs-tabs .chip {
  border-color: var(--line);
  background: transparent;
  color: var(--text-secondary);
  border-radius: var(--radius-pill);
  padding: 9px 18px;
  font-size: 13.5px;
  font-weight: 700;
}
.happs-tabs .chip[aria-selected="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: #FFFFFF;
}

.happs-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}
.happs-search { flex: 1 1 220px; min-width: 0; }
/* Her field, measured on KidApps: r12, 1px rgba(22,35,63,.16), 13px/14px of
   padding, 15px text — a 50px control. 44px is kept as the floor because it is
   this codebase's touch target and her 50 clears it anyway. */
.happs-search-input,
.happs-sort select,
.happs-child-filter select {
  width: 100%;
  min-height: 44px;
  padding: 11px 14px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--dusk2);
  color: var(--ink);
  font-family: inherit;
  font-size: 15px;
}
.happs-search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
.happs-sort,
.happs-child-filter { flex: 0 1 200px; min-width: 140px; }

.happs-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: 14px;
  margin-bottom: 16px;
  -webkit-overflow-scrolling: touch;
}
.happs-table {
  width: 100%;
  min-width: 480px;
  border-collapse: collapse;
  font-size: 13.5px;
}
.happs-matrix-table { min-width: 480px; }
.happs-table th,
.happs-table td {
  padding: 10px 12px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
  min-width: 96px;
}
.happs-table thead th {
  position: sticky;
  top: 0;
  background: var(--dusk2);
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--haze);
  white-space: nowrap;
}
.happs-table tbody tr:last-child td { border-bottom: none; }
.happs-table td:first-child,
.happs-table th:first-child { min-width: 160px; }

.happs-rating { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.happs-rating .pill { margin: 0; }
.happs-rating-desc { max-width: 260px; }
.happs-rating-source { font-style: italic; }

.happs-row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.happs-row-actions > button { min-height: 36px; }

.happs-cell-check {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 24px;
}
.happs-cell-check input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin: 0;
}
.happs-not-seen { opacity: .7; white-space: nowrap; }

.happs-error { color: var(--coral); }

.happs-truncated { text-align: left; }

.happs-defaults-section { display: flex; flex-direction: column; gap: 10px; }

/* L21, THE CARD IN THE OWNER'S SCREENSHOT. It was 14px/16px against her 26px,
   so the content sat tight to an edge while the rows inside it sat far apart —
   the exact inversion of her card, and what "unfinished rather than airy"
   describes. `.card`'s own 22px is used rather than a fourth number invented
   here, so this card matches the tiles and banners stacked above it on the
   same screen; her 26 is 4px further out and is in the app.css patch. */
.happs-defaults-card { padding: 22px; }
.happs-defaults-card > summary {
  cursor: pointer;
  list-style: none;
  /* Her card title, measured: 16.5px/800 on a 1.5 line box, body face. This
     was 16px/700 in the inherited face — a hair lighter and smaller than
     every other card title on the screen, which all come from `.sect`. */
  font-family: var(--font-body);
  font-size: 16.5px;
  font-weight: 800;
  line-height: 1.5;
  color: var(--ink);
}
.happs-defaults-card > summary::-webkit-details-marker { display: none; }
.happs-defaults-card > summary::before { content: '▸ '; }
.happs-defaults-card[open] > summary::before { content: '▾ '; }

/* THE RHYTHM. Her card breaks between GROUPS at 20px and inside a group's
   header pair at 6px; she never leaves 28px between two sentences, which is
   what our stack of `.muted` paragraphs was doing (each one carried
   app.css's `.muted { padding: 10px 0 }` on top of an 8px flex gap, so two
   consecutive lines sat 28px apart while the heading above them sat 22px from
   its own description — three different gaps in four elements).

   Flex `gap` is set to 0 and every step is declared explicitly below, so the
   rhythm is readable in one place instead of being the sum of a gap and
   whatever margin each shared class happens to carry. */
.happs-defaults-body { margin-top: 20px; display: flex; flex-direction: column; gap: 0; }

.happs-unknown-default { display: flex; flex-direction: column; gap: 8px; max-width: 460px; margin-bottom: 0; }
/* Her field label sits tight above its control. */
.happs-unknown-default label { font-size: 13px; font-weight: 600; color: var(--ink); }
.happs-unknown-default select {
  min-height: 44px;
  padding: 11px 14px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--dusk2);
  color: var(--ink);
  font-family: inherit;
  font-size: 15px;
}

/* `.happs-ai-filters` and the `<h3 class="sect">` inside it are GONE, and with
   them the rules that styled them. L21's group-label treatment existed because
   the AI-filter block was a SECOND heading inside a card whose `<summary>` was
   already the title, and her cards carry one title-weight line each. The two
   policies are two cards now, each with its own `<summary>` as its one title,
   so the group label has nothing left to label. Her measured description
   metrics (14px/400 lh1.5 --text-secondary, 16px to the next block) did not go
   anywhere — they are `.happs-section-help` in the new block below, which is
   the element that replaced `.happs-ai-filters > .help`. */
/* HER MEASURE. Her prose column is 706px; ours was running to 1094px, because
   this card is full-bleed on the household screen while hers is a 760px card
   inside a wider main. A 1094px line of 14px text is over 150 characters and
   is the other half of why this card reads as unfinished — it is the only
   place on the screen where a sentence runs the entire width. The cap is on
   the PROSE only; the chip grid still flows edge to edge, as hers does. */
.happs-section-help,
.happs-ai-state,
.happs-reach,
.happs-add-copy .help { max-width: 720px; }
/* The two status lines are one pair: 8px between them, 16px to the next block.
   Both also drop `.muted`'s centring and its 10px band of vertical padding —
   a centred sentence inside a left-aligned card is most of what reads as
   unfinished here, and the padding is what made the gaps uneven. */
.happs-ai-state,
.happs-reach {
  text-align: left;
  padding: 0;
  margin: 0 0 8px;
  font-size: 13.5px;
  line-height: 1.5;
}
.happs-ai-state { color: var(--text-secondary); }
.happs-reach { color: var(--text-tertiary-ui); margin-bottom: 16px; }
/* The "Open Account settings" escape hatch stretched to the full 1104px of the
   card, because a flex column stretches its children by default. Her
   secondary buttons are compact pills that sit at the start of the line. */
.happs-defaults-body > .ghost {
  align-self: flex-start;
  width: auto;
  margin: 0 0 16px;
  padding: 9px 18px;
  font-size: 13.5px;
}
/* =========================================================================
   APP-CATEGORY POLICY CARDS — owner brief, 2026-09-21.

   REPLACES THE CHIP SET ENTIRELY. What was here was `.happs-category-grid`
   as a wrapping flex row of `.happs-catchip` pills borrowed from app.css's
   `.catchip*` family. Measured at 1440px on a real seeded household before
   this change: eighteen variable-width pills wrapping into a ragged second
   row, state carried ONLY by a coral fill on the two blocked ones, no count,
   no word, no save feedback. The owner's words: "a randomly wrapped tag
   cloud" and "the user cannot tell what state they represent".

   `.catchip*` IN app.css IS NOT TOUCHED. It is shared with the WEBSITE
   category screen (categoryRow() in js/app.js, pinned by
   test/categories-ui.test.js, which clicks the hidden input directly).
   Restyling it here to fix the APP panel would have silently restyled a
   different control on a different screen — so this panel gets its own
   component and stops using `.catchip` at all.

   CSS-THAT-IS-REALLY-BEHAVIOUR AUDIT (commit ed3eaf68's lesson), done before
   writing a line and re-run after: NOTHING below sets `display`,
   `visibility`, `opacity` or `content` on `.happs-savestate`,
   `.happs-save-pill`, `.happs-save-why`, `.happs-error`, `.happs-cat-state`,
   `.happs-cat-stateword`, `.happs-cat-summary` or `.happs-precedence` — every
   node the module writes user-facing text into. The only `display` rules here
   are layout containers (`grid`, `flex`) and the `list-style`/marker
   suppression the `<summary>` needs; the only `opacity` is on a DISABLED
   control, which is the state it is meant to signal. The phone media query at
   the bottom re-flows the grid to one column and hides NOTHING.
   ========================================================================= */

/* A GRID, NOT A WRAPPING ROW. Equal-width tracks in a stable alphabetical
   order (orderedCategories() in the module) is the whole of the owner's
   defect #5: a parent can predict where "Messaging" is, and the second row is
   never ragged because every cell is the same width. `auto-fill` rather than
   a fixed column count so the same rule serves 1440 down to 390 without a
   breakpoint per width; 260px is the narrowest track that still fits the
   longest label ("Video streaming"), its count line and its state chip
   without wrapping the chip onto its own line. */
.happs-category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
  margin: 0;
}

/* ONE CATEGORY. A real card with an edge, not a pill: the owner's first
   defect is that these read as "navigation filters or selectable tags" when
   they are policy controls, and a bordered row with a labelled switch and a
   stated consequence is what a policy control looks like everywhere else in
   this product. */
.happs-cat-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--field-fill);
  min-width: 0;
}
/* RESTRAINED RED, AND NEVER ON ITS OWN. The brief allows a blocked category a
   red treatment but requires text and a control alongside it — the word
   "Blocked", the ⛔ glyph and the ticked checkbox are all present in the DOM
   independently of this rule, so removing every colour from this sheet would
   lose no information. Keyed off `data-state`, which the module writes from
   categoryState(), so the tint cannot disagree with the word next to it the
   way a `:checked` selector could (an `ask` category is `:checked` too). */
.happs-cat-item[data-state="blocked"] {
  border-color: rgba(192,52,44,.38);
  background: rgba(192,52,44,.06);
}
.happs-cat-item[data-state="ask"] {
  border-color: rgba(194,118,10,.38);
  background: rgba(194,118,10,.06);
}

/* NAME AND STATE ON ONE LINE, the state pushed to the end so a column of
   cards reads down the right-hand edge as a list of states. `min-width: 0` on
   the name so a long label wraps instead of shoving the chip out of the card. */
.happs-cat-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  min-width: 0;
}
.happs-cat-facts { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* The switch and its own words, plus the optional secondary action. */
.happs-cat-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  min-width: 0;
}
/* The <label> is the hit target for the SWITCH ONLY — see renderCategoryItem's
   own comment. 44px of touch target comes from the padding it carries here
   plus the card's, not from stretching it over the whole card. */
.happs-cat-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  min-width: 0;
  padding: 4px 0;
}
/* A REAL, VISIBLE CHECKBOX. The old control hid its input under an opaque
   pill (app.css's `.catchip-input { opacity: 0 }`), which is what let the
   whole thing read as a tag. Here the checkbox is the thing you look at. */
.happs-cat-switch {
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
  flex: none;
  cursor: pointer;
  accent-color: var(--brand);
}
.happs-cat-switch:disabled { cursor: wait; }
/* THE DIM IS ON THE CONTROL ROW ONLY, NOT THE WHOLE CELL. The `.happs-savestate`
   node lives inside this item and is the ONLY place the panel says 'Saving…'
   — dimming the item would dim the very message the disabled state exists to
   explain. That is ed3eaf68's shape one notch softer (fading feedback rather
   than hiding it) and it is avoided for the same reason. */
.happs-cat-item:has(.happs-cat-switch:disabled) .happs-cat-row { opacity: .62; }

.happs-cat-name {
  flex: 1 1 auto;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.happs-cat-count { font-size: 12.5px; line-height: 1.4; color: var(--text-secondary); }
.happs-cat-yours { font-size: 12.5px; line-height: 1.4; color: var(--text-tertiary-ui); }
/* The switch's own label — "Block this category". Small and secondary,
   because the category name is the heading of the row, but present in text so
   the row states its effect before it is clicked. */
.happs-cat-action {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: .2px;
  color: var(--text-secondary);
}

.happs-cat-state { display: inline-flex; align-items: center; gap: 5px; flex: none; }
.happs-cat-icon { font-size: 12px; line-height: 1; }
/* A compact secondary action; `.ghost` stretches in a flex column by default
   the same way the "Open Account settings" button did. */
.happs-cat-view {
  align-self: center;
  width: auto;
  margin: 0;
  padding: 5px 11px;
  font-size: 12px;
  min-height: 0;
}

/* SAVING… / SAVED · N APPS CHANGED / NOT SAVED. Laid out, never hidden —
   see the audit note at the top of this block. */
.happs-savestate {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}
.happs-save-why { font-size: 12.5px; line-height: 1.4; }
.happs-undo {
  width: auto;
  margin: 0;
  padding: 5px 11px;
  font-size: 12px;
  min-height: 0;
}

/* The summary above the list — "2 of 18 categories blocked · 20 apps
   affected". Same measure cap as the prose around it. */
.happs-cat-summary {
  max-width: 720px;
  margin: 0 0 8px;
  padding: 0;
  text-align: left;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.5;
  color: var(--ink);
}
.happs-precedence {
  max-width: 720px;
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.happs-precedence-unclassified { margin-bottom: 16px; }
.happs-section-help {
  max-width: 720px;
  margin: 0 0 16px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* THE CARD HEADINGS. Two cards now, one per policy, each naming its own —
   and each carrying its current state on the CLOSED summary, so a collapsed
   section never hides which way the policy is set. */
/* INLINE, NOT BLOCK. `.happs-defaults-card > summary::before` draws the
   ▸/▾ disclosure marker as generated inline content at the start of the
   summary; a block-level title would push the title onto the line under
   the marker and leave the arrow floating on a line of its own. */
.happs-card-title { display: inline; }
.happs-card-sub {
  display: block;
  margin-top: 3px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* ONE COLUMN ON A PHONE, and the meta/status rows come back to the left edge:
   a 30px indent on a 360px card costs more than the alignment buys. */
@media (max-width: 560px) {
  .happs-category-grid { grid-template-columns: 1fr; }
}

/* HER EMPTY STATE (RequestsEmpty.dc.html), built by emptyState() in the module.
   Ours was a bare centred grey line floating on the page background with no
   card under it. The text is unchanged and nothing was added to it: her shape
   has a title and a body sentence, and inventing a second sentence here would
   be writing a claim the data has not earned, so the truthful line we already
   had becomes the title and there is no body. */
.happs-empty {
  text-align: center;
  padding: 44px 32px;
  margin: 0 0 16px;
}
.happs-empty-disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 18px;
  border-radius: 50%;
  background: var(--surface-raised);
}
/* Her stroke colour for this glyph is #8A93A3 — --text-tertiary-ui exactly. */
.happs-empty-glyph { width: 26px; height: 26px; stroke: var(--text-tertiary-ui); fill: none; }
.happs-empty-title {
  margin: 0;
  padding: 0;
  font-size: 16.5px;
  font-weight: 800;
  line-height: 1.5;
  color: var(--ink);
  text-align: center;
}

@media (max-width: 480px) {
  .happs-add-task { align-items: stretch; flex-direction: column; }
  .happs-add-controls { align-items: stretch; flex-direction: column; }
  .happs-add-controls .primary { width: 100%; }
  .happs-filter-row { flex-direction: column; align-items: stretch; }
  .happs-sort,
  .happs-child-filter { flex-basis: auto; }
}

/* ---------------------------------------------------------------------------
 * DEF-002 (owner's 2026-09-21 visual audit, CRITICAL) — THE ALLOW SWITCH WAS
 * OFF THE SIDE OF THE PHONE.
 *
 * MEASURED, audit: matrix table 480px inside a 375px client, the Max Allow
 * column starting at x=433. MEASURED, reproduced here with 120 seeded apps and
 * two children (.scratch/def001-002-repro.mjs): table 493px, 96 Allow
 * checkboxes at left=446 right=466 — every one of them outside a 375px
 * viewport. The document itself does NOT overflow (scrollWidth == clientWidth
 * == 375), because `.happs-table-wrap` above is `overflow-x:auto` and swallows
 * it, which is exactly why a page-overflow check passes while a parent cannot
 * see the control. The only route to the switch was a sideways drag of a
 * container that shows no affordance for it. A parent could not turn an app on
 * or off: a functional failure, not a cosmetic one.
 *
 * WHY THE TABLE STILL EXISTS. The sheet header above argues for a real <table>
 * because the Allowed/Locked view is a genuine matrix with one column per
 * child, and that argument is still right — ABOVE 700px. It was never an
 * argument for a 480px floor on a 375px phone. So the table stays, and below
 * 700px each row becomes a stacked card with the child's own control on it,
 * which is the audit's own suggested fix. Same DOM at every width: no control,
 * no row and no destination is rendered on one and not the other, so nothing
 * can be lost by being on a phone.
 *
 * `min-width: 0` here does the other half of the audit's note: the 480px floor
 * and the per-cell 96px/160px floors are what made the table wider than the
 * phone in the first place, and all three are dropped in this layout.
 *
 * CSS-THAT-IS-REALLY-BEHAVIOUR AUDIT (commit ed3eaf68's lesson) — this block
 * DOES set `display`, so it was done before the block was written, not after:
 *   - Every text write in js/household-apps.js was listed
 *     (`grep -n '\.textContent *=' public/js/household-apps.js`, 17 sites) and
 *     every one of them runs while the DOM is being BUILT. `render()` calls
 *     `ctx.mount.replaceChildren()` and rebuilds; there is no post-render
 *     `textContent`/`hidden`/`style.display` write anywhere in the module, so
 *     no rule here can silence a message that arrives later.
 *   - The ONE element this block takes out of the visual flow is `thead`, and
 *     it carries real user-facing text: `th.textContent = childName(...)`.
 *     It is NOT `display:none` — it is the sr-only clip, so it stays in the
 *     accessibility tree — and every word it holds is REPRINTED beside the
 *     control it labels, by `td::before { content: attr(data-label) }` from
 *     the `data-label` renderMatrixTable/renderMatrixCell now set. A child's
 *     name is on screen next to that child's switch, which is more than the
 *     column header managed at this width.
 *   - Nothing here sets `opacity`, and nothing fades or washes a live control
 *     (a fade over working controls shipped and was reverted today; it reads
 *     as disabled).
 *   - `.happs-cell-app[data-label=""]::before { content: none }` suppresses a
 *     PSEUDO-ELEMENT only. No node the JS writes into is affected.
 * ------------------------------------------------------------------------ */
@media (max-width: 700px) {
  /* The contained sideways scroller has nothing left to contain. */
  .happs-table-wrap {
    overflow-x: visible;
    border: none;
    border-radius: 0;
    margin-bottom: 4px;
  }
  .happs-table,
  .happs-table thead,
  .happs-table tbody,
  .happs-table tr,
  .happs-table th,
  .happs-table td { display: block; }

  .happs-table,
  .happs-matrix-table,
  .happs-requests-table { width: 100%; min-width: 0; }

  /* sr-only, NOT display:none — see the audit note above. */
  .happs-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .happs-table tbody tr {
    border: 1px solid var(--line);
    border-radius: 14px;
    margin-bottom: 12px;
    padding: 6px 0;
    background: var(--dusk2);
  }
  .happs-table tbody tr:last-child { margin-bottom: 0; }

  .happs-table td {
    min-width: 0;
    max-width: 100%;
    border-bottom: none;
    padding: 8px 14px;
    overflow-wrap: anywhere;
  }
  .happs-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 4px;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--haze);
  }
  .happs-table td[data-label=""]::before { content: none; }

  /* The app name is the card's title, so it carries no repeated header. */
  .happs-cell-app { padding-top: 12px; font-size: 15px; }
  .happs-cell-app .an { font-weight: 700; }

  /* THE CONTROL ROW — the whole point of DEF-002. The child's name (or
     "Allowed for Max") sits on the left, the switch on the right, both inside
     the viewport, on a 44px tap row. */
  .happs-table td.happs-cell-child {
    display: flex;
    /* `flex-wrap: wrap` IS LOAD-BEARING, and it was missing in the first cut of
       this block. renderMatrixCell appends a refused write's error INTO THIS
       SAME CELL (`td.appendChild(ctx.elem('div','asub happs-error', …))`), so
       it is a third flex item. Without wrap, `flex-basis:100%` below does
       NOTHING: MEASURED at 320/375/414 with the exact node renderMatrixCell
       builds (.scratch/def002-error-probe.mjs) — the message rendered on the
       same line and to the RIGHT of the switch, 166px wide and 3 lines tall at
       320px, while every overflow check and every textContent assertion stayed
       green. Exactly the ed3eaf68 shape: the words were all there and unreadable. */
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 44px;
    border-top: 1px solid var(--line);
    margin-top: 4px;
  }
  .happs-table td.happs-cell-child[data-label]::before {
    margin-bottom: 0;
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    color: var(--ink);
  }
  .happs-cell-child .happs-cell-check { justify-content: flex-end; flex: none; }
  /* The error a refused write leaves under the switch keeps the full row. */
  .happs-cell-child .happs-error { flex-basis: 100%; }

  /* Requests rows: Allow / Block are full-width buttons, not a clipped column. */
  .happs-cell-actions { padding-top: 4px; padding-bottom: 12px; }
  .happs-cell-actions .happs-row-actions > button { flex: 1 1 140px; min-height: 44px; }
}

/* ---------------------------------------------------------------------------
 * DEF-009 + DEF-010 (owner's 2026-09-21 visual audit) — the pager, the clamped
 * row summary, and the app's device provenance.
 *
 * MEASURED, audit: 24,061px at 375x812 with 114 rows and no chunking, and six
 * app names (Calculator, Camera, Clock, Contacts, Phone, ParentProof Agent)
 * repeated with no device/source column. MEASURED, reproduced here with the
 * household's own 114 apps across three devices (.scratch/def009-repro.mjs):
 * 34,802px mobile / 19,801px tablet / 20,101px desktop, rows ~293px each.
 *
 * CSS-THAT-IS-REALLY-BEHAVIOUR AUDIT (commit ed3eaf68's lesson), done BEFORE
 * this block was written, because it DOES set `display` and DOES clamp text:
 *   - All 18 `.textContent =` sites in js/household-apps.js were listed
 *     (`grep -n '\.textContent *=' public/js/household-apps.js`). Every one of
 *     them runs while render() is REBUILDING the mount
 *     (`ctx.mount.replaceChildren()`); the module makes no post-render text,
 *     `hidden` or `style.display` write at all. So no rule here can silence a
 *     message that arrives later — the failure mode ed3eaf68 actually was.
 *   - NO RULE HERE SHORTENS, CLIPS OR HIDES USER-FACING PROSE AT ALL. A
 *     three-line `-webkit-line-clamp` on `.happs-rating-desc` WAS built and
 *     WAS removed: measured over one 20-row page at 375x812, it clipped 6 of
 *     20 descriptions and saved 114px while the per-row "Show more" buttons it
 *     needed cost 760px — a net 646px WORSE on the exact page DEF-009 is
 *     about. See renderClassificationCell's note in js/household-apps.js for
 *     why that is a property of `CLASS_DESCRIPTION_MAX_WORDS = 20` and not a
 *     tuning problem. Pagination is the whole fix.
 *   - Nothing here sets `opacity` on a live control. A `:disabled` pager
 *     button uses app.css's existing `.ghost:disabled` treatment, which is the
 *     same one every other disabled button on this screen already has — it is
 *     genuinely disabled, not a working control wearing a fade.
 *   - `.happs-app-source` and `.happs-tab-caption` are NEW nodes. They hide
 *     nothing and replace nothing.
 * ------------------------------------------------------------------------ */

/* The sentence under the tabs that says what the list actually is. `.muted` is
   centred by app.css; this is a caption for a left-aligned control row. */
.happs-tab-caption {
  margin: -4px 0 0;
  text-align: left;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-secondary);
}

/* DEF-010 — the device line under the app name. Secondary by construction:
   smaller, tertiary ink, never bolder than the name above it. */
.happs-app-source {
  margin-top: 3px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-tertiary-ui);
  overflow-wrap: anywhere;
}

/* DEF-009 — the pager. One copy above the table and one below. */
.happs-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 14px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--dusk2);
}
.happs-table-wrap + .happs-pager { margin: 4px 0 16px; }
.happs-pager-status {
  flex: 1 1 auto;
  margin: 0;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}
.happs-pager-btn {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 8px 14px;
  font-size: 13px;
}

@media (max-width: 480px) {
  /* The count goes on its own row rather than being squeezed between two
     buttons at 375px, and both buttons keep a full 44px tap target. */
  .happs-pager { justify-content: space-between; }
  .happs-pager-status { order: 3; flex-basis: 100%; }
  .happs-pager-btn { min-height: 44px; flex: 1 1 auto; }
}

/* ---------------------------------------------------------------------------
 * 176 PX OF DEAD SPACE UNDER THE SEARCH BOX ON EVERY PHONE. Found while
 * measuring DEF-009, not reported by the audit, fixed here because it is a
 * direct contributor to the height DEF-009 is about.
 *
 * MEASURED at 375x812: `.happs-search` renders 220px tall around a 44px input.
 *
 * WHY. The `@media (max-width: 480px)` block above turns `.happs-filter-row`
 * into `flex-direction: column`. `flex-basis` then sizes the MAIN axis, which
 * in a column is HEIGHT — so `.happs-search { flex: 1 1 220px }` (written for
 * the row layout, where 220px is a sensible minimum WIDTH) asks for a 220px
 * tall box, and `flex-grow: 1` keeps it there. That block already corrects
 * exactly this for its two siblings — `.happs-sort, .happs-child-filter
 * { flex-basis: auto }` — and `.happs-search` was simply missed.
 *
 * Appended rather than edited into that block on purpose: another lane's
 * uncommitted DEF-002 work is live in this file and this lane does not write
 * into it. Same selector, later in the sheet, so it wins on order.
 * ------------------------------------------------------------------------ */
@media (max-width: 480px) {
  .happs-search { flex-basis: auto; }
}
