/*
 * Responsive layer - ONE owner for how every page behaves at every width.
 *
 * Loaded by every page: app pages get it as the third @import in branding.css,
 * and the four bespoke marketing pages plus the index shim link it directly, so
 * there is no page in the app that is responsive by accident.
 *
 * ---------------------------------------------------------------------------
 * WHY THIS FILE EXISTS
 * ---------------------------------------------------------------------------
 * These rules used to live at the bottom of theme.css under the heading
 * "MOBILE-FIRST RESPONSIVE LAYER". They worked, as far as they went, and the
 * problem was how far that was: the block only knew the container names the
 * pages it was written against happened to use. It styled `.wrap`, `main`,
 * `.main-content`, `.header-actions` and `.page-actions`, so:
 *
 *   - courses.html calls its action row `.head-actions`, one letter different
 *     from the `.header-actions` the shared rule wraps, so the row never
 *     wrapped. Six children on one line, a `.searchbox` with a hard
 *     `min-width: 220px` floor, and three `.btn`s that ui-kit.css gives
 *     `white-space: nowrap`, measured 1019px inside a 390px viewport.
 *   - developer.html and instructions.html call their container `.container`,
 *     which no shared rule mentions, so they kept `padding: 2rem` on a phone
 *     and developer.html carried no @media rule of its own at all.
 *   - instructions.html's `.tabs` is a flex row with neither `flex-wrap` nor
 *     `overflow-x`, so the tab strip pushed its container to 427px.
 *
 * A shared layer that has to be told each page's class names is not shared. So
 * every rule below is written against the ALIAS SET - all the names the app
 * actually uses for the same role - and new pages inherit it rather than
 * reinventing it.
 *
 * ---------------------------------------------------------------------------
 * WHY THE SELECTORS START WITH `body`
 * ---------------------------------------------------------------------------
 * Not decoration, and not a specificity arms race. A page's inline <style>
 * loads AFTER branding.css, so at equal specificity the page wins. That is
 * usually correct and deliberate, but it also means a bare `.btn { ... }` in
 * one page's <style> silently outranks a shared floor.
 *
 * It had, concretely: theme.css set `min-height: 44px` on `.btn` under
 * `@media (pointer: coarse)`, and edit-outline.html line 44 sets
 * `.btn { min-height: 42px }`. Both are (0,1,0), the page is later, so every
 * button on that page measured 42px on a touch device while the shared rule
 * that was supposed to guarantee 44 sat there looking correct. Verified in
 * Chromium with touch emulation on: `(pointer: coarse)` matched, and
 * getComputedStyle still reported 42px.
 *
 * `body .btn` is (0,1,1). It beats a page's single-class rule on specificity,
 * so source order stops mattering, and it does it WITHOUT !important, which
 * leaves a page free to override deliberately with its own two-class or
 * body-scoped rule. This is the same technique ui-kit.css uses and documents
 * for the same reason.
 *
 * ---------------------------------------------------------------------------
 * BREAKPOINTS - five, and no more
 * ---------------------------------------------------------------------------
 *   1100px  laptop: the editor's third chrome column stops fitting
 *    900px  tablet: the sidebar goes off-canvas
 *    768px  tablet portrait: single column, the workhorse breakpoint
 *    640px  large phone
 *    480px  small phone
 *
 * An audit found 21 distinct width values across the frontend, most of them
 * one page each (700, 720, 760, 800, 820, 620, 680, 560, 600, 1000, 1180...).
 * Those are not breakpoints, they are guesses, and they are why one page
 * reflowed at 700 and the page next to it at 720.
 *
 * 900 and 768 are LOAD-BEARING and must not move. components.js reads
 * `matchMedia('(max-width: 900px)')` to decide the sidebar mode, and home.html
 * reads `window.innerWidth < 768` to collapse the workflow rail, which
 * test_ui_no_page_scroll.py asserts is still there. Changing either number
 * desynchronises the CSS from the JS.
 *
 * ---------------------------------------------------------------------------
 * Colours and type come from tokens only. No hex literal belongs in this file:
 * it is a LAYOUT layer, and a layout layer that picks a colour is how the
 * brand drifts.
 */

/* =========================================================================
 * 1. FLUID TYPE
 * =========================================================================
 * Headings scale with the viewport between a phone-readable floor and the
 * token size, so a 32px page title does not eat a third of a 390px screen.
 * The token is the CEILING, so desktop is unchanged.
 */
/* THE TOKENS CLAMP THEMSELVES NOW.
 *
 * This block used to wrap each heading in its own clamp() and the 768 and 480
 * blocks below then overrode all three again - nine rules describing one ramp,
 * and three different opinions about what an h2 is. --text-lg/xl/2xl carry their
 * own phone floor and desktop ceiling in brand-tokens.css, so a heading is sized
 * once, adapts at every width rather than at five, and desktop is unchanged by
 * construction because the clamp maximum IS the old fixed value.
 *
 * What stays here is only the MAPPING of an element to a step, which is a layout
 * decision and belongs in this file.
 */
h1,
.page-header > h1,
.page-header .page-heading > h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }

/* The FAB is the one piece of chrome that is genuinely oversized on a phone.
 *
 * 56px is a comfortable desktop affordance and 14% of a 390px viewport's width,
 * sitting on top of content in the corner a thumb rests on. 48px still clears the
 * 44px AAA touch target with room, and the corner lanes derive from the token so
 * the jump buttons and toasts follow it automatically instead of needing their own
 * mobile offsets. */
@media (max-width: 640px) {
  :root { --fab-size: 44px; --fab-inset: 0.75rem; }

  /* ── ICONS STEP DOWN, WHICH IS WHY THE TOKEN EXISTS ────────────────────────
   *
   * "reduce icon fonts as look liveke native mobile app ... change icons as per
   * mobile naive style".
   *
   * A native mobile app draws its chrome tighter than a desktop app does: the
   * icons are smaller relative to the text, because the screen is held closer and
   * the controls are hit with a thumb whose target is the BUTTON, not the glyph.
   * Ours were rendering at desktop sizes, so a 20px mark inside a 44px tile read
   * as oversized and slightly cartoonish.
   *
   * One edit, because the icon scale was introduced for exactly this. Before it
   * there were eight places to find and no way to know you had found them all.
   * The touch targets do not move: those are set on the button, not the glyph. */
  :root {
    --icon-xs: 11px;
    --icon-sm: 13px;
    --icon-md: 17px;
    --icon-lg: 19px;
  }
}

/* =========================================================================
 * 2. TABLES SCROLL RATHER THAN PUSH
 * =========================================================================
 * A data table has a genuine minimum width: the locked Curriculum fields table
 * (Job Role, Content Level, Content Tags, Length) cannot usefully reflow into
 * one column. So it keeps its width and takes a scrollbar, which contains the
 * overflow instead of handing it to the page.
 *
 * `display: block` is what makes overflow-x work on a table element. The cost
 * is that the cells stop sharing a layout, which is why a page that has
 * already wrapped its table in a scrolling container opts out below.
 */
table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.scroll-container table,
.scroll-x table,
.table-wrap table { display: table; }

/* The outline document tables OPT OUT of the block/scroll treatment above.
 *
 * home.html's inline <style> gives `.preview-box-formatted table` (and the
 * org-table / kv-table variants) a real multi-column desktop layout with
 * fixed column widths, but it never restates `display` - it relies on the UA
 * default `display: table`. The bare `table { display: block }` rule above is
 * (0,0,1) and, since it is the only rule that sets `display`, it wins by
 * default and collapses those tables into a single stacked column at EVERY
 * width - including desktop, where nothing should stack.
 *
 * These tables have their own answer to overflow: home.html sets
 * `min-width: 620px` on them inside its phone media query so they SCROLL, and
 * outline-workspace.css's <=640px block stacks them into a grouped list with
 * higher specificity (`body .preview-box-formatted table`, (0,2,1)), which
 * still wins on phones. So excluding them here restores the desktop table
 * without touching the mobile list. */
.preview-box-formatted table {
  display: table;
  overflow-x: visible;
}

/* =========================================================================
 * 3. OVERFLOW HYGIENE - applies at EVERY width
 * =========================================================================
 * Horizontal overflow is not a mobile-only bug, it is just easiest to see
 * there. These rules remove the three mechanisms that caused all of it.
 */

/* 3a. An unbreakable token cannot be wider than its box.
 *
 * developer.html lists reference files as inline <code>: paths like
 * `backend/outline_invariants.py`. A monospace path has no break opportunity,
 * so its min-content width IS its full rendered width - 305px inside a 242px
 * <li>, which overflowed the <ul>, then the .card, then .container, then the
 * page. Nothing in the app set overflow-wrap on inline code.
 *
 * `break-word` rather than `anywhere`: `anywhere` also shrinks min-content
 * width, which makes flex and grid tracks collapse in ways that surprise. This
 * only breaks a word that genuinely cannot fit.
 *
 * `pre` is excluded on purpose - it already scrolls, and rewrapping a code
 * block changes what the code MEANS. */
p, li, dd, dt, td, th, figcaption, blockquote, summary, label,
h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
code, kbd, samp { overflow-wrap: break-word; word-break: break-word; }
pre { overflow-x: auto; }
pre code { overflow-wrap: normal; word-break: normal; }

/* 3b. A flex or grid child defaults to min-width:auto, which means it refuses
 * to shrink below its content. That default is the single most common cause of
 * a row pushing past the viewport, because ONE long label in ONE child is
 * enough. Every named layout row in the app opts out. */
body .page-header,
body .page-heading,
body .header-actions,
body .page-actions,
body .head-actions,
body .course-bar,
body .course-bar .field,
body .content-area,
body .content-workspace,
body .searchbox,
body .toolbar,
body .toolbar > input,
body .toolbar > select,
body .card,
body .stat,
body .stat-card { min-width: 0; }

/* 3c. Nothing is allowed to be wider than the screen.
 *
 * A blanket `* { max-width: 100% }` would break the tables above and any
 * deliberately-scrolling pane, so this names the things that are meant to be
 * fluid and have no reason to exceed the viewport. */
img, svg, video, canvas, iframe, embed, object { max-width: 100%; }
img, video { height: auto; }

/* =========================================================================
 * 4. LAPTOP: 1100px
 * =========================================================================
 * Where the editor's third chrome column (sidebar + icon rail + course panel)
 * stops fitting beside the content. icon-rail.css already turns the course
 * panel into an overlay here; this only stops content rows fighting for room.
 */
@media (max-width: 1100px) {
  body .head-actions,
  body .header-actions,
  body .page-actions { flex-wrap: wrap; }
}

/* =========================================================================
 * 5. TABLET: 900px
 * =========================================================================
 * The sidebar goes off-canvas at this width (components.js). Content gets the
 * full measure, so multi-column shells become one column.
 */
@media (max-width: 900px) {
  body .app-layout { flex-direction: column; }
  body .sidebar,
  body .side-panel,
  body .content-area,
  body .content-workspace { width: 100%; max-width: 100%; }
}

/* =========================================================================
 * 6. TABLET PORTRAIT: 768px - the workhorse
 * =========================================================================
 */
@media (max-width: 768px) {

  /* ---- Containers: one padding rule, every name the app uses ----
   * `.container` is the name developer.html, instructions.html and
   * workspace.html use, and it was missing from the old list, so those three
   * kept 2rem of padding (64px of a 390px screen) on a phone. */
  body > .wrap,
  body .wrap,
  body .container,
  body main,
  body .main-content,
  body .page-wrap { padding-inline: var(--space-4); }

  /* ---- The page header stacks, and its actions get a full row ----
   * `.head-actions` is courses.html's name for `.header-actions`. Both are
   * listed everywhere from here down; that one missing alias is what let a
   * 1019px row ship. */
  body .page-header {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }
  body .page-header .header-actions,
  body .page-header .page-actions,
  body .page-header .head-actions,
  body .head-actions {
    width: 100%;
    flex-wrap: wrap;
  }

  /* A search field stops being a fixed-width ornament and becomes the row.
   * courses.html pinned `min-width: 220px`, which on its own guaranteed
   * overflow once anything sat beside it. */
  body .searchbox {
    min-width: 0;
    width: 100%;
    flex: 1 1 100%;
  }

  /* A search-and-filter toolbar (workspace.html's asset toolbar: an input beside a
   * <select>) wraps so each control gets a usable width. Unwrapped, the select
   * took its natural width and squeezed the search input to 26px. Also enforced
   * under coarse pointer below, because a landscape phone (850px) is wider than
   * this breakpoint yet still squeezed the input to 26px. */
  body .toolbar { flex-wrap: wrap; }
  body .toolbar > input { flex: 1 1 100%; }
  body .toolbar > select { flex: 1 1 auto; }

  /* ---- Tab strips scroll instead of pushing ----
   * instructions.html's `.tabs` had no wrap and no overflow, so 4 tabs at
   * 1.5rem padding each measured 411px in a 358px box. Scrolling rather than
   * wrapping keeps the strip reading as one row of tabs. */
  body .tabs,
  body .tab-row,
  body .filter-pills,
  body .view-toggle {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  body .tabs::-webkit-scrollbar,
  body .tab-row::-webkit-scrollbar,
  body .filter-pills::-webkit-scrollbar,
  body .view-toggle::-webkit-scrollbar { display: none; }
  body .tabs > *,
  body .tab-row > *,
  body .filter-pills > *,
  body .view-toggle > * { flex: none; }
  body .tab { padding-inline: var(--space-3); }

  /* ---- Card grids collapse ----
   * !important is load-bearing here: these grids are declared per page with
   * explicit `repeat(N, ...)` track lists, and a track list is not something a
   * shared layer can partially override. */
  body .card-grid,
  body .template-grid,
  body .course-grid,
  body .features-grid,
  body .stats-grid,
  body .metrics-grid,
  body .two-col,
  body .split { grid-template-columns: 1fr !important; }

  body .card,
  body .template-card,
  body .course-card,
  body [class*="-card"] { min-width: 0; max-width: 100%; }

  body .app-layout { padding: var(--space-4); gap: var(--space-4); }

  /* Heading step-downs USED TO BE HERE, and at 480 as well. The type tokens now
   * carry their own range (see the note at the top of this file), so a heading
   * that was described in three places is described in one. */

  body input,
  body select,
  body textarea { max-width: 100%; }

  body .btn-group,
  body .button-group,
  body .actions,
  body .form-row { flex-wrap: wrap; }

  /* A dialog is inset from the viewport rather than centred on a width it
   * cannot have. */
  body .modal-content,
  body .dialog-content,
  body .popup-content {
    width: calc(100vw - var(--space-6)) !important;
    max-width: 100% !important;
    margin: var(--space-4);
  }
}

/* =========================================================================
 * 7. LARGE PHONE: 640px
 * =========================================================================
 */
@media (max-width: 640px) {
  body .course-bar { flex-direction: column; align-items: stretch; }
  body .course-bar .field { flex: 1 1 auto; width: 100%; }
}

/* =========================================================================
 * 8. SMALL PHONE: 480px
 * =========================================================================
 */
@media (max-width: 480px) {
  body > .wrap,
  body .wrap,
  body .container,
  body main,
  body .main-content,
  body .page-wrap { padding-inline: var(--space-3); }

  body .app-layout { padding: var(--space-2); gap: var(--space-2); }

  /* Buttons stop sitting beside each other and become a stack. Three 120px
   * nowrap buttons cannot share a 390px row, and shrinking them to fit is how
   * labels get truncated to nonsense. */
  body .page-header .header-actions,
  body .page-header .page-actions,
  body .page-header .head-actions,
  body .btn-group,
  body .button-group,
  body .actions {
    flex-direction: column;
    align-items: stretch;
  }
  /* Children go full width, EXCEPT groups that are already a compact unit.
   * A segmented "Cards | List" toggle or a pill filter row stretched across the
   * screen reads as a broken card rather than a control: the thing it is is a
   * small cluster, and its size is part of how you recognise it. They stay
   * their natural width and centre instead. */
  body .page-header .header-actions > *:not(.view-toggle, .filter-pills, .seg, .segmented),
  body .page-header .page-actions > *:not(.view-toggle, .filter-pills, .seg, .segmented),
  body .page-header .head-actions > *:not(.view-toggle, .filter-pills, .seg, .segmented),
  body .btn-group > *,
  body .button-group > * { width: 100%; justify-content: center; }

  body .head-actions > .view-toggle,
  body .head-actions > .filter-pills,
  body .header-actions > .view-toggle,
  body .header-actions > .filter-pills { align-self: center; }

  /* ── THE SMALLEST TEXT ON THE SMALLEST SCREEN MUST NOT GET SMALLER ────────
   *
   * This block used to set `body { font-size: var(--text-sm) }` and
   * `td, th { font-size: var(--text-xs) }`, stepping the app's body copy down
   * from 14px to 13px and table cells to 12px at exactly the width where reading
   * is hardest. Together with three heading step-downs it is the single reason
   * the app read as cramped on a phone rather than as any one size being wrong.
   *
   * The lower type steps are fixed on purpose (brand-tokens.css says why) and the
   * upper ones clamp themselves, so there is nothing left for this breakpoint to
   * say about type. What a small phone genuinely needs is SPACE, not smaller
   * glyphs, so only padding is adjusted here. */
  body .card,
  body .template-card,
  body .course-card,
  body [class*="-card"] { padding: var(--space-3); }

  body td,
  body th { padding: var(--space-2); }

  body .nav-links { gap: var(--space-1); }
}

/* =========================================================================
 * 9. TOUCH TARGETS
 * =========================================================================
 * `pointer: coarse` is the right question to ask - it is true on a phone and
 * false on a desktop browser someone has made narrow - and it was already
 * being asked. What was missing was the specificity to make the answer stick
 * (see the header note about edit-outline.html's 42px `.btn`).
 *
 * Deliberately dense chrome is EXCLUDED rather than inflated. app-topbar's
 * 32px icon buttons and the icon rail's tiles are 32px by design, inside a
 * 56px bar; forcing them to 44 would break the bar to satisfy a number they
 * already clear. WCAG 2.5.8 (AA) asks for 24x24 CSS px, which they pass. The
 * 44px floor here is the AAA-grade target applied where there is room for it:
 * content buttons and form controls.
 */
@media (pointer: coarse) {
  /* Targeted at the ELEMENT, not at a list of class names.
   *
   * A class list was tried first and it does not work, for the same reason the
   * container rules above need an alias set: the app calls a button `.btn`,
   * `.btn-sec`, `.view-btn`, `.bp-btn`, `.brand-preset`, `.dv-btn`, `.na-btn`,
   * `.sp-btn`, `.np-btn`, `.load-button`, `.instructions-button` and
   * `.sw-primary`, depending on which page and which year. Enumerating them
   * means the next new name is under the floor again, silently, until someone
   * measures it. `button` is the one name that cannot drift.
   *
   * The exclusions are deliberately dense chrome, not oversights:
   *   - `tb-*` is app-topbar, a uniform 32px control row inside a 56px bar.
   *     Six of them at 44px would not fit the bar at 390px, and they already
   *     clear WCAG 2.5.8 (AA), which asks for 24x24 CSS px. The 44px floor is
   *     the AAA-grade 2.5.5 target, applied where there is room for it.
   *   - `.irail-btn` tiles are 48px already.
   *   - the composer's inline icon and send controls sit in a bar that scrolls.
   *
   * Bare inline <a> links in prose are NOT raised: 2.5.8 explicitly exempts a
   * target that is inline in a sentence, and padding them would break the line
   * height of the paragraph they sit in. */
  body button:not(
    .tb-icon, .tb-act, .tb-avatar, .tb-upgrade, .tb-item, .tb-result,
    .tb-signin, .tb-toggle, .ntf-bell, .irail-btn,
    .composer-icon, .composer-send
  ),
  body input[type="submit"],
  body input[type="button"],
  body a.btn,
  body .btn,
  body .btn-primary,
  body .btn-secondary,
  body [role="button"]:not(.tb-icon, .tb-act, .irail-btn) {
    min-height: var(--tap-min, 44px);
  }
  /* Was `min-height: 44px`. The literal predated the --tap-min token; both
     resolve to 44px, but referencing the token keeps this shared floor and the
     topbar/ui-kit floors (already on the token) tied to one value. Fallback
     preserved so it still holds if brand-tokens.css is absent. */

  /* NATIVE PILL BUTTONS on a phone are applied in ui-kit.css, not here: that is
     the authoritative button stylesheet and it loads AFTER this one, so a radius
     rule here (even body-scoped) would lose to its `html body .btn-primary` base.
     See the `@media (pointer:coarse)` block at the end of ui-kit.css. */

  body input:not([type="checkbox"]):not([type="radio"]),
  body select,
  body textarea { min-height: var(--tap-min, 44px); }

  /* A search+filter toolbar squeezes its input on a touch device whatever the
     width (a landscape phone is 850px, past the 768 breakpoint, yet the select
     still crushed the search box to 26px). Wrap it so the input keeps a full row. */
  body .toolbar { flex-wrap: wrap; }
  body .toolbar > input { flex: 1 1 100%; min-width: 0; }
  body .toolbar > select { flex: 1 1 auto; min-width: 0; }

  /* 16px is the threshold below which iOS Safari zooms the page on focus, and
   * that zoom is what leaves a form scrolled sideways with no way back.
   *
   * Scoped to text-entry fields, NOT to every control. ui-kit.css caps
   * `.composer-select` at 85px wide with 10px text on a small phone; handing
   * that an unconditional 16px would clip the model name it exists to show. */
  body input[type="text"],
  body input[type="search"],
  body input[type="email"],
  body input[type="password"],
  body input[type="number"],
  body input[type="url"],
  body textarea { font-size: 1rem; }
}

/* Checkboxes and radios had NO shared rule, so they rendered at the browser
 * default: measured 13px on outline-builder.html's autopilot toggle and 15px
 * on usage.html's select-all. Both are below the 24px WCAG 2.5.8 (AA) target,
 * and a 13px checkbox is hard to hit with a mouse, let alone a thumb. */
body input[type="checkbox"],
body input[type="radio"] {
  width: 1.15rem;
  height: 1.15rem;
  accent-color: var(--accent-violet);
  cursor: pointer;
  flex: none;
}
@media (pointer: coarse) {
  body input[type="checkbox"],
  body input[type="radio"] { width: 1.5rem; height: 1.5rem; }
}

/* An inline text link that acts as a control needs a hit area. home.html's
 * `.link-more` ("View all", "Manage connection") measured 19px tall. */
@media (pointer: coarse) {
  body .link-more,
  body .text-link {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
  }
}

/* STANDALONE navigation / list links get the full touch floor.
 *
 * A link that is the whole content of a footer column, a link list, or a nav
 * row is a tappable ROW, not a word inside a sentence, so WCAG 2.5.8's inline
 * exemption does not apply and a 16-31px height is a real miss. These were the
 * last page-level findings: landing.html's footer columns (.fcol > a, 31px),
 * the 404 link list (.links > a), and the marketing "Back to home" nav link.
 *
 * Scoped to the CONTAINERS that hold standalone links so a link genuinely inline
 * in prose (trends.html's source citation inside a .rf-src sentence, a link in a
 * paragraph) is untouched - padding those would wreck the line height of the text
 * they sit in. `display:block` gives the height somewhere to land on an <a> that
 * is otherwise inline. */
@media (pointer: coarse) {
  body .foot-top .fcol > a,
  body .footer .fcol > a,
  body nav.foot a,
  body .links > a,
  body .nav-row > a,
  body .drawer-nav a,
  body .foot-bottom > div > a,
  body .brand-links > a {
    display: block;
    min-height: var(--tap-min, 44px);
    line-height: var(--tap-min, 44px);
  }

  /* A HORIZONTAL header nav link gets a taller hit area, but this must NOT set
     `display` on it. The marketing pages (how-it-works, security) hide their nav
     text links below 768px with `.nav-links a:not(.btn){display:none}` and show
     only the CTA button; a `body`-scoped `display:inline-flex` here beat that
     page rule, un-hid all three links, and pushed the header 61px wider - a
     regression this rule itself caused. Sizing only, with alignment applied ONLY
     when the link is already an inline-flex/flex box, so a hidden link stays
     hidden and a visible one still centres its label. */
  body nav.nav-links > a,
  body header.nav .nav-links > a { min-height: var(--tap-min, 44px); }
  body nav.nav-links > a:not([style*="none"]),
  body header.nav .nav-links > a:not([style*="none"]) { align-items: center; }

  /* Social icon links (landing.html's footer .social row: X, LinkedIn, etc.) are
     standalone square targets, not inline text, so they get a full square target.
     They already display:grid to centre their glyph, so only the size is added. */
  body .social > a,
  body .social-links > a {
    min-width: var(--tap-min, 44px);
    min-height: var(--tap-min, 44px);
  }
}

/* The sidebar is the most-tapped surface in the app and every row in it was
 * under the floor: destinations at 36-40px, the theme button and the workflow
 * steps at 36 and 34, the collapse chevron at 23x29. They are off-canvas on a
 * phone, which is why a viewport-overflow check never saw them, but the drawer
 * is exactly where a thumb goes first.
 *
 * Safe in rail mode too: the rail is a desktop layout (mobile keeps the sidebar
 * fully off-canvas at its full width), and its tiles are already 40px+. */
@media (pointer: coarse) {
  body app-nav .nav-links a,
  body app-nav .nav-more,
  body app-nav .nav-customize,
  body app-nav .nav-newcourse,
  body app-nav .appnav-theme,
  body app-nav .nav-logo,
  body app-nav .wf-step { min-height: var(--tap-min, 44px); }

  body app-nav .nav-collapse,
  body app-nav .nav-reopen { min-width: var(--tap-min, 44px); min-height: var(--tap-min, 44px); }

  /* The claim above that rail-mode tiles "are already 40px+" held for HEIGHT and
     not for width: measured on a 1024x1366 iPad Pro, where the sidebar rests in
     rail mode, a workflow step came out 30x44. Only half the target was fixed.
     The rail is 68px wide, so a 44px floor fits inside it with room to spare. */
  body app-nav .wf-step { min-width: var(--tap-min, 44px); }

  /* The bare icon button in the marketing pages' own nav (the theme toggle on
     how-it-works and security). It sizes itself from padding alone, which came out
     at 35x44 - tall enough, 9px too narrow. Both pages declare their own
     `.icon-btn` in a page <style> that the browser reads after this file, so this
     is scoped with `body` to outrank it on specificity rather than order. */
  body .icon-btn { min-width: var(--tap-min, 44px); min-height: var(--tap-min, 44px); }

  /* Popup CLOSE / header controls, which are width-INDEPENDENT: a review sheet or
     side drawer can be open on a wide-but-short landscape phone (850x407) where the
     max-width:768px sheet rules do not apply, yet a finger is still what closes it.
     The om-close X was 32px and the drawer's "Open full page" / close link 32px
     tall; both need the coarse-pointer floor here, not tied to a width. Icon-only
     targets get width too; a text link only needs the height. */
  body .om-close,
  body .fs-close,
  body .sheet-close { min-width: var(--tap-min, 44px); min-height: var(--tap-min, 44px); }
  body .side-drawer-openpage,
  body .side-drawer .view-toggle-btn { min-height: var(--tap-min, 44px); }
}

/* =========================================================================
 * 10. NOTCHED PHONES
 * =========================================================================
 * Horizontal insets only. `padding-bottom` here would add to the page's
 * scrollHeight, and test_responsive_ui_playwright.py asserts the page itself
 * does not scroll vertically - a shell that ends exactly at the viewport is
 * the whole layout contract. Fixed-position furniture handles its own bottom
 * inset against its own inset token.
 */
@supports (padding: env(safe-area-inset-left)) {
  body {
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
  }
}

/* =========================================================================
 * 11. REDUCED MOTION
 * =========================================================================
 * A global answer, so a new animated component is quiet by default instead of
 * having to remember. Not `animation: none`: an animation that is a spinner
 * still needs to convey "working", so it is slowed and de-transformed rather
 * than removed.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================================
 * 12. OVERLAYS — ONE SHARED SHEET PRIMITIVE FOR EVERY POPUP
 * =========================================================================
 * WHY THIS SECTION EXISTS
 *
 * Every popup in the app was built as its own island, and it showed. There are
 * seven of them and they agree on nothing:
 *
 *   .confirm-modal / .confirm-box   versions.html, instructions.html  centred, 400px, NO mobile rule
 *   .history-modal / .history-box   instructions.html                 centred, 700px, NO mobile rule
 *   .polish-modal  / .polish-box    instructions.html                 centred, 900px, NO mobile rule
 *   .sheet / .sheet-backdrop        courses.html                      centred translate, min(860px,...), NO mobile rule
 *   .fullscreen-overlay / .fs-modal home.html                         centred, 1100px, NO mobile rule
 *   .side-drawer                    home.html                         right drawer, ONE 640px width:100vw rule
 *   .om-drawer                      outline-mentors.js (injected)      the review sheet, hand-rolled mobile
 *
 * On a phone a "centred 860px dialog" is a box wider than the screen with its
 * left and right sliced off; a "right drawer at 94vw" is a full-screen takeover
 * whose dismiss gesture is the browser's back-swipe; and a centred box with a
 * `max-height` but no inner scroll region traps its own footer off the bottom of
 * the screen. Each was reported separately ("buttons cutting off", "gaps", "why
 * do i have to tell you every one") because each was a separate island with the
 * same missing half.
 *
 * A sheet is the native answer to all of them: on a phone a temporary surface
 * comes up from the BOTTOM where the thumb is, stops short of the top so the page
 * stays visible behind it (which is what makes it read as temporary rather than a
 * new screen), scrolls its body, and pins its actions to the bottom. iOS and
 * Material both draw exactly this. So this section makes every popper above behave
 * as one, by NAME — the same alias-set technique the rest of this file uses so a
 * new popup inherits the behaviour instead of reinventing its broken half.
 *
 * SCOPING AND SAFETY
 *   - Keyed on `max-width: var(--bp-tablet-portrait)` (768) so desktop and tablet
 *     landscape are untouched: the islands' own desktop rules still stand.
 *   - `body`-scoped so it outranks a page's single-class inline rule on
 *     specificity without !important, the way the rest of this file does. Where an
 *     island already sets a value inline via JS (the injected .om-drawer), that is
 *     an inline style and beats any stylesheet, so .om-drawer keeps its own
 *     hand-tuned mobile CSS and is deliberately NOT reshaped here — it is listed
 *     only for the pieces that are safe to share (footer, tap targets).
 *   - Nothing here renames a class or changes desktop geometry, so the computed
 *     -style and class-name assertions in the test suite are unaffected.
 *
 * THE ALIAS SETS
 *   backdrop/overlay : the fixed, inset, scrim layer
 *   panel            : the surface that holds the content
 *   head / body / foot : the three bands inside the panel
 */
@media (max-width: 768px) {

  /* ---- The scrim: one dim, from the token, behind every popup ----------- */
  body .confirm-modal,
  body .history-modal,
  body .polish-modal,
  body .sheet-backdrop,
  body .fullscreen-overlay,
  body .side-drawer-overlay {
    /* These already position:fixed + inset:0 themselves; only the dim is unified
       so three different darks behind three popups become one. */
    background: var(--scrim);
  }

  /* ---- The panel becomes a bottom sheet -------------------------------------
   * Full width, pinned to the bottom, rounded only on the top edge, and capped
   * so the page shows behind it. A centred box's translate is overridden; a
   * right drawer's slide-in axis is flipped to vertical.
   *
   * `.om-drawer` is intentionally absent: it sets these same properties inline
   * from JS (which beats any stylesheet), and it already implements this exact
   * shape. Duplicating it here would be dead CSS.
   */
  body .confirm-box,
  body .history-box,
  body .polish-box,
  body .sheet,
  body .fs-modal,
  body .side-drawer {
    position: fixed !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    top: auto !important;
    transform: none !important;
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    /* Stop short of the top so the page reads behind the sheet. Use the DYNAMIC
       viewport height (dvh) so a docked sheet reaches the real visible bottom on
       mobile browsers whose toolbar collapses - `92vh` alone pins to the LAYOUT
       viewport, leaving a strip of the page showing through beneath the sheet's
       last row on iOS Safari (the reported gap). vh line is the fallback. */
    max-height: 92vh !important;
    max-height: 92dvh !important;
    display: flex !important;
    flex-direction: column;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important;
    border-left: 0 !important;
    border-right: 0 !important;
    border-bottom: 0 !important;
    box-shadow: 0 -12px 40px rgba(16, 24, 40, 0.20) !important;
    overflow: hidden !important;
  }

  /* ---- `.sheet` alone toggles its OWN display; the rest ride a parent overlay -
   * The bottom-sheet rule above forces `display: flex !important` on every panel
   * so they dock. For .confirm-box / .history-box / .polish-box / .fs-modal /
   * .side-drawer that is harmless: each is the INNER box of an overlay
   * (.confirm-modal / .fullscreen-overlay / .side-drawer-overlay) whose own
   * `display:none` (toggled by `.open`) hides the whole thing, so the inner box's
   * forced display never shows through.
   *
   * `.sheet` (courses.html Idea Lab) is the exception: it toggles visibility on
   * ITSELF via `.sheet.open { display:flex }` / `.sheet { display:none }`, with a
   * SEPARATE `#sheetBackdrop` scrim. The `!important` above beat BOTH of those, so
   * on a phone the sheet was pinned `display:flex` whether or not `.open` was set -
   * removing `.open` (what every close path does: the X, the scrim tap, Escape,
   * swipe) did nothing, and the fixed, bottom-docked 92vh panel kept intercepting
   * every tap. The page read as hung with a popup that "won't close". The scrim
   * itself DID hide (it is gated on `.open` and nothing forces its display), which
   * is why only the panel stayed and there was no dim - just a dead sheet.
   *
   * Gate the forced display on `.open`, and hide it explicitly when not open, so
   * the mobile rule follows the same open/closed state the desktop rule does. */
  body .sheet:not(.open) { display: none !important; }

  /* A drawn grab handle, so the top edge says "this pulls down". Added on the
     panels that do not already draw one (the .om-drawer draws its own). */
  body .confirm-box::before,
  body .history-box::before,
  body .polish-box::before,
  body .sheet::before,
  body .fs-modal::before,
  body .side-drawer::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--border-subtle);
    pointer-events: none;
    z-index: 2;
  }

  /* The centred boxes (confirm/history/polish) have no head/body/foot bands, so
     give the panel top room for the handle and let its own content scroll. */
  body .confirm-box,
  body .history-box,
  body .polish-box {
    padding-top: var(--space-5);
    overflow-y: auto !important;
    overscroll-behavior: contain;
    /* Room past the home bar on a notched phone. */
    padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)) !important;
  }

  /* ---- The body band scrolls; the head and foot do not --------------------- */
  body .sheet-body,
  body .side-drawer-body,
  body .fs-body,
  body .history-box .history-list,
  body .polish-box .polish-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }

  /* The head sits above the scroll and carries the handle's clearance. */
  body .sheet-head,
  body .side-drawer-head,
  body .fs-header {
    flex: none;
    padding-top: var(--space-5);
  }

  /* ---- The action bar: pinned, compact, thumb-reachable --------------------
   * The single biggest complaint ("why are these buttons so big", "wasting the
   * bottom area"). A footer of full-width stacked slabs ate ~40% of the sheet.
   * The native answer is a pinned bar of NORMAL-height buttons that sit in a row
   * and wrap only if they must — a secondary action does not deserve its own
   * full-width slab. Sticky to the bottom so it is always reachable however far
   * the body has scrolled, and clear of the home bar.
   */
  body .sheet-foot,
  body .side-drawer-foot,
  body .polish-actions,
  body .confirm-box .btn-row,
  body .fs-header .fs-actions {
    flex: none;
    position: sticky;
    bottom: 0;
    z-index: 3;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
    margin: 0;
    padding: var(--space-3);
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
    background: var(--bg-card, #fff);
    border-top: 1px solid var(--border-subtle);
  }
  /* Buttons in the bar take a fair share of the row rather than a slab each. The
     primary earns a little more pull so it reads as primary. They stay a
     44px-tall target (the touch floor in section 9), just not full-bleed. */
  body .sheet-foot > .btn,
  body .side-drawer-foot > .btn,
  body .polish-actions > .btn,
  body .confirm-box .btn-row > .btn {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
  }
  body .sheet-foot > .btn-primary,
  body .polish-actions > .btn-primary,
  body .confirm-box .btn-row > .btn-primary { flex-grow: 2; }

  /* A close affordance stays a real target even when it is a small glyph. It also
     sits ABOVE the drawn grab-handle pseudo-element (::before, z-index:2) and takes
     its own taps: the handle is pointer-events:none, but giving the close button an
     explicit stacking context + pointer-events guarantees a tap near the top edge
     lands on the X and not in dead space - the reported "X click not closing it". */
  body .sheet-close,
  body .side-drawer .fs-close,
  body .om-close {
    min-width: var(--tap-min, 44px);
    min-height: var(--tap-min, 44px);
    position: relative;
    z-index: 4;
    pointer-events: auto;
  }
}

/* Landscape phones are wide but very short (a 850x407 handset). A bottom sheet
 * capped at 92vh there leaves almost nothing, so let the sheet use the height it
 * needs and scroll — the same panels, taller. Keyed primarily on SHORT HEIGHT,
 * which is what actually distinguishes a landscape phone; the width bound uses the
 * sanctioned 900 breakpoint (a tablet in landscape is 1024+ and taller than 460,
 * so it is excluded by the height anyway). Using 900 rather than a bespoke 926
 * keeps this inside the five-breakpoint contract the tests enforce. */
@media (max-width: 900px) and (max-height: 460px) {
  body .confirm-box,
  body .history-box,
  body .polish-box,
  body .sheet,
  body .fs-modal,
  body .side-drawer { max-height: 100vh !important; max-height: 100dvh !important; border-radius: 0 !important; }
}
