/*
 * Awdur brand foundation — the shared design system for every surface
 * (the authenticated app AND the public author landing pages). Slice-specific
 * layout lives with each slice; this file owns the tokens, typography, base
 * elements, and the curated palette presets (D13) so nothing drifts.
 *
 * Fonts (Newsreader serif + Hanken Grotesk sans) are loaded in the layout head.
 * Propshaft serves this as `application`.
 */

:root {
  --paper:      #F2EDE3;   /* warm ground */
  --paper-hi:   #FBF8F2;   /* raised card */
  --paper-in:   #EFE8DB;   /* inset well */
  --line:       #d8d2c6;   /* hairline */
  --line-soft:  #e4ddd0;
  --ink:        #3b352c;   /* primary text */
  --ink-soft:   #736B5C;   /* secondary text */
  --ink-faint:  #a99f8d;   /* placeholders */

  /* App default accent is PLUM. Components reference --accent/-hi/-deep, never a
     hard-coded hue, so a `.palette-*` preset class (author pages) recolors
     everything beneath it; unclassed app chrome (dashboard, editor, auth) uses
     this default. */
  --accent:      #6B4A6B;
  --accent-hi:   #8a638a;
  --accent-deep: #573957;

  --ok:    #3A5A43;
  --warn:  #9a6a2f;
  --error: #8f3a2f;

  --serif: 'Newsreader', Georgia, 'Times New Roman', serif;
  --sans:  'Hanken Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  --radius: 11px;
  --measure: 64ch;
}

/* ---- curated palette presets (D13: constrained choices, not raw hex) ----
   Each preset redefines the accent family ONLY. NO accent may be lighter than
   .palette-terracotta: --accent is used as link-hover TEXT and is already only
   4.53:1 on the default ground, so terracotta is the lightness ceiling for the
   whole set (design spec §3.4). Check any future accent against that floor
   first; the 8 x 4 accent/ground matrix passes AA because of it, not by luck. */
.palette-terracotta { --accent:#9C5B3B; --accent-hi:#c08560; --accent-deep:#8a4f33; }
.palette-slate      { --accent:#4A5A6B; --accent-hi:#6E7E8F; --accent-deep:#3a4857; }
.palette-plum       { --accent:#6B4A6B; --accent-hi:#8a638a; --accent-deep:#573957; }
.palette-forest     { --accent:#3A5A43; --accent-hi:#4f7a5c; --accent-deep:#2d4834; }
.palette-ink        { --accent:#4a4438; --accent-hi:#6a6152; --accent-deep:#2a251e; }
/* appended by the theming slice — the presentation ORDER authors see is the
   swatch order in PagesHelper::THEME_ACCENT_LABELS, not this file's order. */
.palette-claret     { --accent:#8E3A45; --accent-hi:#B0616B; --accent-deep:#752F38; }
.palette-indigo     { --accent:#3F4C7A; --accent-hi:#6673A0; --accent-deep:#323C63; }
.palette-sea        { --accent:#35605F; --accent-hi:#52807E; --accent-deep:#294D4C; }

/* ---- curated GROUND presets (D13 theming slice, design spec §3.2). Same
   mechanism, second axis: a ground redefines the paper family ONLY. --ink* is
   never touched, which is exactly what keeps every accent x ground pair at AA —
   the safety argument decomposes because ink is invariant. NO ground may be
   darker than .ground-parchment: --ink-soft on --paper is already only 4.51:1
   there, so a darker ground would break secondary text everywhere at once.
   .ground-parchment reproduces today's :root paper values byte-for-byte, which
   is what makes the default pair a genuine no-op. ---- */
.ground-parchment { --paper:#F2EDE3; --paper-hi:#FBF8F2; --paper-in:#EFE8DB; --line:#d8d2c6; --line-soft:#e4ddd0; }
.ground-linen     { --paper:#F1F0EB; --paper-hi:#FAFAF7; --paper-in:#EAE9E3; --line:#d9d7cf; --line-soft:#e5e3dc; }
.ground-ivory     { --paper:#FAF6EE; --paper-hi:#FFFFFF; --paper-in:#F4EFE5; --line:#e3ddd0; --line-soft:#ece7dc; }
.ground-mist      { --paper:#EEF1F0; --paper-hi:#F8FAF9; --paper-in:#E7EBEA; --line:#d3d8d6; --line-soft:#e0e4e3; }

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.01em;
  text-wrap: balance;
  margin: 0 0 0.4em;
}
h1 { font-size: clamp(30px, 5vw, 46px); }
h2 { font-size: clamp(24px, 3.4vw, 32px); }
h3 { font-size: 22px; }

p { margin: 0 0 1rem; max-width: var(--measure); }

a { color: var(--accent-deep); text-underline-offset: 3px; text-decoration-thickness: 1px; }
a:hover { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* eyebrow / uppercase label */
.eyebrow {
  font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---- forms ---- */
label { display: block; font-size: 14px; color: var(--ink-soft); margin: 0 0 6px; }

input[type=text], input[type=email], input[type=password],
input[type=url], input[type=number], textarea, select {
  width: 100%;
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-hi);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 13px 14px;
  transition: border-color .18s, box-shadow .18s, background .18s;
}
input::placeholder, textarea::placeholder { color: var(--ink-faint); }
input:focus, textarea:focus, select:focus {
  outline: none; background: var(--paper-hi);
  border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

button, .button, input[type=submit] {
  font-family: var(--sans); font-size: 16px; font-weight: 600;
  color: var(--paper-hi); background: var(--accent);
  border: 1px solid var(--accent); border-radius: var(--radius);
  padding: 13px 22px; cursor: pointer; text-decoration: none; display: inline-block;
  transition: background .18s, transform .06s;
}
button:hover, .button:hover, input[type=submit]:hover { background: var(--accent-deep); }
button:active, .button:active { transform: translateY(1px); }

/* THE SPECIFICITY TRAP, and the over-correction that follows it. Read both.
 *
 * The base rule above matches a submit button as `input[type=submit]` — an
 * attribute selector, so (0,1,1). A bare `.button-quiet` is (0,1,0) and LOSES
 * every declaration to it, which meant every `f.submit` asking to be quiet
 * (admin Reject/Suspend, the panel's Grant and Update expiry, the book editor's
 * Add link / Save link) rendered accent-FILLED — the "wall of identical
 * recommended actions" the 2026-07-25 audit kept finding. The compound selector
 * (0,2,1) is what makes the class do anything at all on those elements, and it
 * must restate colour and background, not just the border.
 *
 * But the two rules CANNOT be merged, and the border must not be hoisted into
 * the bare one. ~22 `link_to` sites wear this class — the app header's
 * Settings/Help/Admin on every authenticated page, the public-plane preview
 * bar's back link — and an <a> matches NO base button rule, so it has no
 * radius and no button chrome. A `border` shorthand there paints a square 1px
 * box around each of them. `border-color` alone is inert on an anchor (no
 * border-style to go with it) and is exactly enough on a <button>, where the
 * class already outranks the bare element selector. So: longhand in the bare
 * rule, shorthand ONLY in the submit compound. */
.button-quiet {
  color: var(--accent-deep); background: transparent; border-color: var(--line);
}
input[type=submit].button-quiet {
  color: var(--accent-deep); background: transparent; border: 1px solid var(--line);
}
.button-quiet:hover,
input[type=submit].button-quiet:hover { background: var(--paper-hi); color: var(--accent-deep); }

/* Destructive variant (polish backlog P2-8, first needed by P1-10.4). Built from
   --error exactly as .badge-blocked mixes it — no new token. An irreversible
   action must not wear the accent fill, which is the app's ONE signal for
   "recommended", nor the same quiet button as "Add another link". Reversible
   actions ("Take offline") stay .button-quiet: this class means "can't be
   undone", and it keeps its confirm/reason requirement either way.
   Same two-rule shape as .button-quiet above — no anchor wears this class today,
   but seeding the merged shape is how the anchor bug comes back. */
.button-danger {
  color: var(--error); background: transparent;
  border-color: color-mix(in srgb, var(--error) 40%, var(--line));
}
input[type=submit].button-danger {
  color: var(--error); background: transparent;
  border: 1px solid color-mix(in srgb, var(--error) 40%, var(--line));
}
.button-danger:hover,
input[type=submit].button-danger:hover {
  background: color-mix(in srgb, var(--error) 8%, var(--paper-hi));
  color: var(--error);
}

/* ---- flash ---- */
.flash {
  max-width: 640px; margin: 16px auto; padding: 12px 16px;
  border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--paper-hi); font-size: 15px;
}
.flash-notice { border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); color: var(--ok); }
.flash-alert  { border-color: color-mix(in srgb, var(--error) 40%, var(--line)); color: var(--error); }

.form-errors { color: var(--error); font-size: 14px; margin: 0 0 16px; }
.form-errors ul { margin: 0; padding-left: 18px; }

/* ---- the `hidden` attribute actually hides ----------------------------------
 * `[hidden] { display: none }` ships in the USER AGENT stylesheet, and ANY
 * author-origin declaration beats user-agent origin regardless of specificity.
 * So the moment ANY author rule sets `display` on an element, every `hidden` on
 * it silently stops working — the attribute is still there, the element is still
 * painted. Three live sites when this was found (2026-07-27), all shipped:
 *   1. `.condition-operand` (display: flex) — the segment builder painted all
 *      five operand groups at once, so "Match on" appeared to do nothing;
 *   2. `.featured-book-controls` (display: flex) — the landing-page editor's
 *      up/down reorder arrows showed on un-featured rows too;
 *   3. the clipboard Copy button (`button { display: inline-block }`, the BASE
 *      element rule — this one needs no component class at all) — a dead
 *      "Copy" button was painted for visitors whose JS never revealed it.
 * Site 3 is the instructive one: the collision does not need a fancy selector,
 * only a `display` anywhere in the author stylesheet.
 *
 * `!important` is load-bearing, not a shortcut: `[hidden]` and a single class
 * selector have IDENTICAL specificity (0,1,0), so without it the winner is
 * whichever rule comes later in the file — an invariant that silently breaks the
 * next time someone adds a `display` rule below this line. This is exactly what
 * normalize.css and every modern reset do, and it is how the server-rendered
 * `hidden` attribute and Stimulus's `el.hidden = true` are meant to behave.
 *
 * Audited before adding (2026-07-27): nothing in the app relies on `hidden`
 * failing to hide. Pinned by test/system/segments_builder_test.rb (computed
 * style — no server-rendered assertion can see this class of bug). */
[hidden] { display: none !important; }

/* ---- layout helpers ---- */
.wrap { width: 100%; max-width: 720px; margin: 0 auto; padding: clamp(24px, 5vw, 56px) 22px; }
.stack > * + * { margin-top: 18px; }

.brand { font-family: var(--serif); font-size: 20px; color: var(--ink); text-decoration: none; }
.brand .dot { color: var(--accent); }
.ink-soft { color: var(--ink-soft); }
.ink-faint { color: var(--ink-faint); }
.hint { color: var(--ink-soft); font-size: 14px; }

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

/* =====================================================================
 * Author app shell — the signed-in author's own workspace (dashboard,
 * page editor, holding page) plus the staff admin console. Utilitarian
 * tool chrome, not a marketing surface; reuses the tokens/elements above.
 * ===================================================================== */

.app-shell { min-height: 100vh; }

.app-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 16px clamp(20px, 4vw, 40px);
  border-bottom: 1px solid var(--line);
  background: var(--paper-hi);
}
.app-nav { display: flex; align-items: center; gap: 16px; font-size: 14px; color: var(--ink-soft); }
.app-nav .button-quiet { padding: 8px 14px; font-size: 14px; }

.app-main { max-width: 760px; margin: 0 auto; padding: clamp(24px, 5vw, 48px) 22px; }
.app-main-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 24px;
}
.app-main-head h1 { margin: 0; }

.card {
  background: var(--paper-hi); border: 1px solid var(--line); border-radius: var(--radius);
  padding: clamp(18px, 3vw, 28px); margin-bottom: 20px;
}
.card h2 { font-size: 18px; margin-bottom: 12px; }
.card > div + div { margin-top: 14px; }

.badge {
  display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; padding: 3px 10px; border-radius: 999px;
}
.badge-live { background: color-mix(in srgb, var(--ok) 16%, transparent); color: var(--ok); }
.badge-draft { background: var(--paper-in); color: var(--ink-soft); }
.badge-admin { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent-deep); }
/* D14 secondary badges — ADDITIONS beside .badge-live, never replacements.
   Staged keys off --warn (the .badge-live formula); scheduled off --accent
   (the .badge-admin formula). No new token. */
.badge-staged { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); }
.badge-scheduled { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent-deep); }

/* ---- admin console: wider main + plain roster tables ---- */
.app-main-wide { max-width: 1040px; }
.table-scroll { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: 15px; }
.data-table th {
  text-align: left; font-weight: 600; font-size: 12px; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-soft);
  padding: 0 14px 10px; border-bottom: 1px solid var(--line);
}
.data-table td { padding: 12px 14px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.data-table tr:last-child td { border-bottom: none; }
.data-table td:first-child { font-weight: 500; color: var(--ink); }
.data-table form { display: inline-block; }
.data-table td button { padding: 7px 14px; font-size: 14px; }
.data-table details summary { cursor: pointer; color: var(--accent-deep); font-size: 14px; }
/* P1-10.2 — dates and timestamps never wrap. An ISO date that broke into
   "2026-" / "07-25" read as a damaged value and made every row two lines tall;
   tabular figures then let a column of dates align on their digits. The nowrap is
   scoped to the TOKEN, not the cell: an Expires cell also carries relative-time
   prose ("· 3 days ago"), and holding that on one line would push this 7-column
   table into horizontal scroll even at 1040px. */
.data-table td.date-cell { font-variant-numeric: tabular-nums; }
.data-table .date-token { white-space: nowrap; }
/* a plain-language note inside an uppercase column header (e.g. what an em dash
   means in that column) — same ink, sentence case, no tooltip */
.data-table th .th-note { text-transform: none; letter-spacing: 0; font-weight: 400; }
/* P1-4 — the audit log's DETAILS cell. The payload wraps (it used to clip
   mid-token, so a row read as corrupted) and the JSON lives in a --paper-in well
   inside the existing details/summary disclosure. Monospace is a SYSTEM stack —
   the only non-brand family in the app, and no webfont. */
.data-table td.audit-details { overflow-wrap: anywhere; }
.data-table details pre {
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 10px 12px; margin: 8px 0 0;
  max-height: 220px; overflow: auto; white-space: pre-wrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px; color: var(--ink-soft);
}

/* P1-5 — the roster's first cell is the row's link to the dossier. It inherits
   the cell's ink/500 emphasis so the table still reads as a table; the accent
   only appears on hover/focus. (A clickable <tr> was rejected: it swallows the
   Suspend button's hit target and breaks keyboard order.) */
.data-table td:first-child a.row-link { color: inherit; text-decoration: none; }
.data-table td:first-child a.row-link:hover,
.data-table td:first-child a.row-link:focus-visible {
  text-decoration: underline; text-decoration-color: var(--accent);
}

/* admin entitlements panel (admin-entitlements-panel-spec §4.6). Layout + tone
   ONLY: no colour beyond existing tokens, no new token, no .palette-*.
   Precedent: .condition-row, .schedule-controls, .link-row-btn. */
.ent-row-form { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 10px; margin-top: 10px; }
/* the global input rule is width:100% — a reason input needs a floor, not a fill,
   or it blows out its table cell */
.ent-reason { width: auto; min-width: 16rem; flex: 1 1 16rem; }
/* a not-granted flag reads quieter than a granted one (overrides the
   .data-table td:first-child ink/500 emphasis) */
.data-table td.flag-off { color: var(--ink-soft); font-weight: 400; }

/* moderation queue status filters (admin/authors) */
.admin-filters {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  font-size: 14px; color: var(--ink-soft); margin-bottom: 24px;
}
.admin-filters a { text-decoration: none; color: var(--ink-soft); }
.admin-filters a:hover { color: var(--accent-deep); }
.admin-filters a.current {
  color: var(--accent-deep); font-weight: 600;
  border-bottom: 2px solid var(--accent);
}

/* admin author dossier: stat tiles (counts only, never reader lists) */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
.stat {
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px;
}
.stat-figure { display: block; font-family: var(--serif); font-size: 28px; line-height: 1.15; color: var(--ink); }
.stat-label {
  display: block; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-soft); margin-top: 2px;
}
/* suspended/rejected status badge — mirrors the .badge-live formula on --error */
.badge-blocked { background: color-mix(in srgb, var(--error) 16%, transparent); color: var(--error); }

.actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px; }

.editor-form .actions { margin-top: 24px; }
.editor-form .actions .hint { align-self: center; }

/* THE RULE (polish backlog P1-7): A CARD OWNS ITS OWN SAVE. Every action row we
   ship now lives inside a card, so this rule has to WIN over `.editor-form
   .actions` above — the two selectors have identical specificity, which means
   source order decides and this one must come last. Do not move it back up. */
.card .actions { margin-top: 12px; }

/* ---- page status block (dashboard card + editor) ---- */
/* card heading + badge on one scannable line (same pattern as .app-main-head) */
.card-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.card-head h2 { margin-bottom: 0; }

.page-status { margin-bottom: 20px; }
.card .page-status { margin-bottom: 0; }
.page-status .actions { margin-top: 10px; }

/* the live address: plain selectable text + copy + view link, on an inset well */
.url-row {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 10px 14px;
}
.url-row-address { font-weight: 500; }
.url-row-copy { padding: 6px 12px; font-size: 14px; }

/* info panel inside/below a card (e.g. the editor's pending-review note) */
.callout {
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 16px; margin-top: 16px;
  font-size: 14px; color: var(--ink-soft);
}
.callout strong { color: var(--ink); }

/* D14 staged-changes summary inside the status block (the V1 diff affordance) */
.staged-summary { margin-top: 12px; }
.staged-summary p { margin: 0; }
.staged-summary .hint { margin-top: 2px; }

/* D14 pending-schedule panel — the "goes live {time}" promise + its controls.
   Mirrors .callout's inset well (--paper-in). */
.schedule-panel {
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 16px; margin-top: 12px;
  font-size: 14px; color: var(--ink-soft);
}
.schedule-panel p { margin: 0; }
.schedule-panel .badge { margin-right: 4px; }

/* ---- authenticated preview chrome over the public template ---- */
/* carries .palette-plum in markup so the bar reads as APP chrome against the
   terracotta public page below it */
.preview-bar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; min-height: 44px; padding: 6px 16px;
  background: var(--paper-hi); border-bottom: 1px solid var(--line);
}
.preview-bar-label {
  display: flex; align-items: center; gap: 10px;
  font-size: 14px; color: var(--ink-soft);
}
.preview-bar .button-quiet { padding: 6px 12px; font-size: 14px; }

/* inert signup form on preview renders: no visual chrome of its own */
.preview-inert { border: 0; padding: 0; margin: 0; min-width: 0; }

/* The ADMIN record-panel idiom: uppercase eyebrow over a read-only fact, for
   dossier panels where nothing is editable and the density earns it. Author-facing
   surfaces use .field-list below instead (P2-13). The spacing is a GROUPING
   signal — the gap above a term must clearly exceed the gap below it, or a stack
   of term/definition pairs reads as one undifferentiated column (P1-11). */
.detail-list { margin: 0; }
.detail-list dt {
  font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-soft); margin-top: 18px;
}
.detail-list dt:first-child { margin-top: 0; }
.detail-list dd { margin: 4px 0 0; }
/* P1-11 — `p` has no top margin, so a paragraph after a definition list ran
   straight into the last definition and the two read as one block. */
.detail-list + p { margin-top: 16px; }

/* The AUTHOR-facing read-only fact (P1-9 / P2-13): the form-label idiom — 14px
   sentence case, --ink-soft — over a body-scale value, so an editable field and
   a read-only one speak the same language inside one card. Any explanation is a
   BLOCK under the value, never a trailing span sharing its wrapping line (which
   made a stated email address look like the start of a sentence). */
.field-list { margin: 0; }
.field-list dt { font-size: 14px; color: var(--ink-soft); margin: 0 0 6px; }
.field-list dd + dt { margin-top: 18px; }
.field-list dd { margin: 0; color: var(--ink); }
.field-list dd p.hint { margin: 4px 0 0; }

/* ---- auth screens (registration / sign-in) ----
 * Split layout: a brand hero beside the form, so the logged-out entry lands
 * you in the brand, not a bare box. Stacks on narrow screens. */
.auth-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  /* faint paper grain, tinted by the active palette's accent (so plum stays
     plum, terracotta stays terracotta) */
  background-image:
    radial-gradient(1200px 600px at 78% -8%, color-mix(in srgb, var(--accent-hi) 12%, transparent), transparent 60%),
    radial-gradient(900px 500px at 8% 108%, color-mix(in srgb, var(--accent-deep) 9%, transparent), transparent 60%);
}

.auth-hero { display: flex; align-items: center; padding: clamp(32px, 6vw, 76px); border-right: 1px solid var(--line); }
.auth-hero-inner { max-width: 30rem; }
.eyebrow-row { display: flex; align-items: center; gap: 12px; margin-bottom: 30px; }
.eyebrow-row .mark { font-family: var(--serif); font-size: 22px; font-weight: 500; color: var(--ink); }
.eyebrow-row .mark .dot { color: var(--accent); }
.eyebrow-row .pill {
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft);
  border: 1px solid var(--line); border-radius: 999px; padding: 5px 11px;
}
.auth-hero-h {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(34px, 4.4vw, 52px); line-height: 1.04; letter-spacing: -0.02em; margin: 0;
}
.auth-hero-h em { font-style: italic; color: var(--accent); }
.auth-hero-lede { font-size: clamp(16px, 2vw, 18px); line-height: 1.55; color: var(--ink-soft); margin: 22px 0 0; max-width: 44ch; }
.auth-hero-lede .pron { font-style: italic; }
.auth-hero-points { list-style: none; margin: 30px 0 0; padding: 0; display: flex; flex-direction: column; gap: 13px; }
.auth-hero-points li { position: relative; padding-left: 24px; color: var(--ink); font-size: 15px; }
.auth-hero-points li::before { content: "\2726"; position: absolute; left: 0; color: var(--accent); }

.auth-form-col { display: flex; align-items: center; justify-content: center; padding: clamp(28px, 5vw, 56px); }
.auth-card {
  width: 100%; max-width: 400px; background: var(--paper-hi);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: clamp(24px, 5vw, 36px);
}
.auth-card h1 { font-size: clamp(26px, 3vw, 32px); }
.auth-sub { color: var(--ink-soft); font-size: 15px; margin: 0 0 22px; }
.auth-alt { font-size: 14px; color: var(--ink-soft); margin: 18px 0 0; }

@media (max-width: 820px) {
  .auth-shell { grid-template-columns: 1fr; }
  .auth-hero { border-right: none; border-bottom: 1px solid var(--line); padding: clamp(28px, 7vw, 44px); }
  .auth-hero-h { font-size: clamp(30px, 8vw, 40px); }
  .auth-hero-lede { margin-top: 16px; }
  .auth-hero-points { display: none; }
}

/* ---- P2 S4: broadcast composer (Trix-constrained, token-based) ---- */
/* trix.css (vendored) ships its own greys; re-skin the chrome with the app
   tokens so the editor reads as plum app chrome, and hide the file-attachment
   group entirely (attachments are refused in JS; no upload endpoint exists). */
trix-toolbar .trix-button-group--file-tools { display: none; }
trix-toolbar .trix-button-group {
  border-color: var(--line); border-radius: var(--radius);
  margin-bottom: 8px;
}
trix-toolbar .trix-button { border-bottom: none; }
trix-toolbar .trix-button.trix-active {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* The RESPONSIVE trix-toolbar overrides (findings E-3/E-4 — wrap-not-scroll,
   44px buttons, the link dialog) are NOT here. They live in the phone tier of
   the responsive block at the end of this file, per supervisor ruling W3-D1:
   §5.4's "in the trix re-skin block's neighbourhood" is a statement about which
   FILE, not which line, and every responsive rule in the pass sits in one
   appended block so C-11 stays answerable by reading one place. */
.composer-editor {
  min-height: 260px; width: 100%;
  font-family: var(--sans); font-size: 16px; color: var(--ink);
  background: var(--paper-hi);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 13px 14px;
  transition: border-color .18s, box-shadow .18s;
}
.composer-editor:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.composer-editor:empty:not(:focus)::before { color: var(--ink-faint); }

/* the saved-draft preview iframe: inert document in a paper well */
.composer-preview {
  width: 100%; height: 380px; border: 1px solid var(--line);
  border-radius: var(--radius); background: #fff;
}

/* "or schedule for later" row under the primary Send button */
.schedule-row { margin-top: 18px; }
.schedule-controls { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.schedule-controls input[type=datetime-local] {
  width: auto; flex: 0 1 260px;
  font-family: var(--sans); font-size: 15px; color: var(--ink);
  background: var(--paper-hi); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 10px 12px;
}
.schedule-controls .button-quiet { padding: 10px 18px; font-size: 15px; }

/* P1 links editor — stack the rows inside the links card */
.link-row + .link-row { margin-top: 12px; }
/* row reorder/remove buttons: compact, dedicated class (NOT .url-row-copy, which
   is the live-URL clipboard affordance the draft editor must not show) */
.link-row-btn { padding: 6px 12px; font-size: 14px; }

/* ---- SIX FLEX VALUES LIFTED OUT OF INLINE `style` ATTRIBUTES ---------------
 * Mobile pass Wave 3, 2026-08-02. Spec §5.4's markup deltas, finding E-5,
 * conformance C-10 and C-11 ITEM 3 — the LAST of the five sanctioned
 * unconditional changes, and the only one Wave 3 ships. Supervisor ruling
 * W3-D2 fixes this position (beside .link-row, NOT in the responsive block at
 * the end of the file) and requires the responsive block's C-11 preamble to
 * point here; see :906-923 there.
 *
 * WHY THIS BLOCK EXISTS AT ALL. An inline `style` attribute outranks every
 * stylesheet declaration short of `!important`, and C-9 forbids adding one — so
 * six form controls carrying `style="flex: …"` were literally UN-STACKABLE in
 * CSS. That is the one place in the whole mobile pass that genuinely needed a
 * markup edit (E-5). The class is inert on its own; the payoff is the
 * `max-width: 560px` override at the end of this file.
 *
 * EVERY VALUE BELOW IS LIFTED BYTE-FOR-BYTE FROM THE ATTRIBUTE IT REPLACED, and
 * that fidelity is the ONLY thing making this change desktop-neutral. Each rule
 * therefore names the partial and the line it came from, so a reviewer can check
 * the claim from here without opening three ERB files. If you change a value,
 * you are changing desktop rendering — which C-11 does not permit for this item.
 * Pinned by test/system/responsive_editors_test.rb, whose DESKTOP half compares
 * all six against the numbers the inline styles produced before the lift.
 *
 * .book-link-address carries three MORE declarations than the flex shorthand
 * because its inline attribute did: it is the read-only retailer address, and
 * an ellipsised single line IS the design (the full URL lives in the "Edit this
 * link" disclosure right below it). It is deliberately the one class here with
 * NO phone override — see the note at the phone block.
 *
 * NOT IN SCOPE, AND BYTE-IDENTICAL, SO DO NOT "FINISH THE SWEEP" BY REFLEX:
 * pages/books/_links_frame.html.erb:39,41 and pages/books/_fields.html.erb:66
 * carry three more inline `flex:` styles, one of them (`:41`) the SAME
 * `flex:1 1 260px;` string as the book edit URL below. §5.4's delta table
 * ratifies exactly six; the other three are logged as a follow-up
 * (docs/development/handoffs/mobile-wave3.md §5b #1). The `<details>` ancestor
 * in _link.html.erb:43 is what tells the in-scope site from the out-of-scope
 * one, and responsive_editors_test.rb selects on it for that reason. */
.link-row-label    { flex: 1 1 160px; }   /* landing_pages/_link_row.html.erb:19 */
.link-row-url      { flex: 2 1 240px; }   /* landing_pages/_link_row.html.erb:26 */
.book-link-url     { flex: 1 1 260px; }   /* books/_link.html.erb:43 (inside <details>) */
.book-link-address {                      /* books/_link.html.erb:8 (keeps .ink-faint) */
  flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.step-delay-value  { flex: 0 0 90px; }    /* sequence_steps/_step.html.erb:41 */
.step-delay-unit   { flex: 0 1 120px; }   /* sequence_steps/_step.html.erb:42 */

/* ---- P1 featured-books editor (spec §4.7) — a checklist of the author's
   published books, checked = leads the shelf, up/down to order the featured.
   Layout only; every value is an existing token (no raw hex, no .palette-*).
   Reuses .check-inline for the pick control and .link-row-btn for reorder. ---- */
.featured-books-list { display: flex; flex-direction: column; }
.featured-book-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--line-soft);
}
.featured-book-row:last-child { border-bottom: none; }
/* .check-inline override: own the row's left, no top margin inside the row */
.featured-book-pick { margin: 0; gap: 12px; min-width: 0; }
.featured-thumb {
  flex: 0 0 auto; width: 34px; height: 51px; object-fit: cover;
  border-radius: 4px; border: 1px solid var(--line);
}
.featured-book-title {
  font-size: 15px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.featured-book-controls { flex: 0 0 auto; display: flex; gap: 6px; }

/* ---- P4 S3: author-shell cover/headshot upload bits (spec §3.9) ---- */
.book-thumb {
  flex: 0 0 auto; width: 44px; height: 66px; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--line);
}
.cover-preview { margin: 0 0 10px; }
.cover-preview img {
  max-height: 120px; width: auto; border-radius: 8px; border: 1px solid var(--line);
}
.check-inline { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--ink-soft); margin-top: 10px; }
.check-inline input { width: auto; }
.qr-row { margin-top: 12px; }

/* ---- P3 S1: reader segments — preset grid + flat-AND builder rows ----
   Composition/layout only; every value is an existing token (no raw hex, no
   .palette-*). The condition row keeps native controls styled by the global
   form CSS — the only new markup in the feature. */
.preset-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px;
  margin-bottom: 8px;
}
.preset-grid .card { margin-bottom: 0; }

.condition-row {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px 14px;
  padding: 14px 0; border-bottom: 1px solid var(--line-soft);
}
.condition-row:first-child { padding-top: 4px; }
.condition-row:last-child { border-bottom: none; }
.condition-kind label { margin-bottom: 4px; }
.condition-operand { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
.condition-operand select,
.condition-operand input[type=number] { margin: 0; }
.condition-days { width: 6rem; }
.condition-fixed-op { font-size: 14px; }
.condition-remove { margin-left: auto; padding: 6px 12px; font-size: 14px; }

/* Scrollable tag well (Open Q2 interim): existing tokens only, no new component. */
.tag-picker {
  width: 100%; max-height: 168px; overflow-y: auto;
  background: var(--paper-in); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 8px 12px;
}
.tag-picker .check-inline { margin-top: 6px; }
.tag-picker .check-inline:first-child { margin-top: 0; }

/* ---- P3 S2: broadcast composer audience picker ----
   Layout only; every value is an existing token (no raw hex, no .palette-*).
   The picker reuses .card/.check-inline/.stat/.hint; this block just resets the
   fieldset chrome and spaces the segment field + count frame. */
.audience-picker { margin-bottom: 18px; }
.audience-picker fieldset { border: none; margin: 0; padding: 0; }
.audience-picker .label {
  font-family: var(--serif); font-size: 15px; color: var(--ink); padding: 0; margin-bottom: 4px;
}
.audience-picker .hint { margin: 4px 0 0 26px; }
.audience-picker [data-broadcast-audience-target="segmentField"] { margin: 10px 0 0 26px; }
.audience-picker select { margin-bottom: 10px; }
.audience-count { display: block; }

/* P7 S2 analytics — layout + the two new primitives. Plum chrome only; every
   value is a token or a color-mix() of tokens (no raw hex). */
.chart { margin-top: 18px; }
.chart figcaption { margin-top: 8px; }

/* chart canvas Observable Plot draws into; sized so the layout is stable */
.chart svg { max-width: 100%; height: auto; }

/* placeholder in the chart footprint (mirrors .stat's inset well) */
.chart-empty {
  background: var(--paper-in); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px 18px; margin-top: 18px;
  min-height: 120px; display: flex; align-items: center;
}
.chart-empty .hint { margin: 0; }

/* distribution bars — tiers + retailers. Single-hue fill, label+length carry data. */
.tier-bars { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
.tier-bar { display: grid; grid-template-columns: 9rem 1fr 4rem; align-items: center; gap: 12px; }
.tier-bar-label { font-size: 14px; color: var(--ink-soft); }
.tier-bar-track {
  height: 14px; border-radius: 999px; background: var(--paper-in);
  border: 1px solid var(--line); overflow: hidden;
}
.tier-bar-fill {
  display: block; height: 100%;
  /* token-only ramp — palette-stable under plum chrome */
  background: color-mix(in srgb, var(--accent) 65%, var(--paper-hi));
}
.tier-bar-value { font-size: 14px; color: var(--ink); text-align: right; }

@media (max-width: 560px) { .tier-bar { grid-template-columns: 7rem 1fr 3.5rem; } }

/* ---- P0a progress meter — the shared primitive, used by the public book page,
   the public shelf, and (structurally) nothing else. Lives here, not in
   public.css, because this file is the cross-surface design system and is
   loaded by BOTH layouts; the fill keys off --accent, so it is terracotta on a
   public author page and plum in app chrome with no per-surface code. Geometry
   adapted from .tier-bar-track/.tier-bar-fill; every value is an existing token
   (no raw hex, no new token, no .palette-* dependency). The AUTHOR card renders
   no bar — the public partial consumes this block, so do not move it, rename its
   classes, or fold it into public.css. ---- */
.meter { margin: 0; }

.meter-track {
  height: 10px;                       /* .book-meter/.shelf-meter re-scale this */
  border-radius: 999px;
  background: var(--paper-in);
  border: 1px solid var(--line);
  overflow: hidden;
}
.meter-fill {
  display: block; height: 100%;
  background: var(--accent);
  /* NO transition/animation: the bar states a fact, it does not perform. */
}

.meter-caption {
  margin: 7px 0 0;
  max-width: none;                    /* overrides the global p { max-width: 64ch } */
  font-family: var(--sans); font-size: 13px; line-height: 1.4;
}
.meter-value { color: var(--ink); font-weight: 500; }
.meter-sep   { color: var(--ink-faint); margin: 0 2px; }
/* the D19 stamp: quiet and INVARIANT — no age-based variant exists by design. */
.meter-stamp { color: var(--ink-soft); font-weight: 400; }

/* 100% state ("Draft complete"): replaces the bar. Accent-tinted pill (the
   .author-monogram tint formula at .badge scale) — NOT .badge-live, whose --ok
   green would be the first non-paper/ink/accent hue on a public author page. */
.meter-complete {
  display: inline-block;
  font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 3px 10px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 14%, var(--paper-hi));
  color: var(--accent-deep);
}

/* ---- P0a author slider. The global input rule enumerates types and omits
   `range`, so a range renders fully native today. accent-color is the whole
   treatment: it tints thumb + filled track from the palette in every modern
   engine, with a graceful fall back to native chrome where unsupported — and it
   avoids the ::-webkit-slider-thumb / ::-moz-range-track fork entirely. ---- */
input[type=range].progress-slider {
  width: 100%; max-width: 420px;
  height: 28px;                 /* comfortable pointer/touch target */
  accent-color: var(--accent);  /* plum in app chrome, palette-reactive */
  margin: 4px 0 0;
}

/* the honest-value readout beside the slider: bigger than a .hint (it's the
   number the author is setting), quieter than a heading. */
.meter-readout {
  margin: 8px 0 0; max-width: none;
  font-size: 15px; color: var(--ink-soft);
}
.meter-readout output { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }

/* hide/show button pushed to the far end of .card-head (already a flex row with
   gap + wrap). button_to renders a <form>, so the auto-margin goes on the form
   element, not the button. Layout only — no colour. */
.card-head .card-head-action { padding: 6px 12px; font-size: 14px; }
.card-head form:has(.card-head-action) { margin-left: auto; }

/* Turbo's own busy flag is the only loading affordance this card needs. */
turbo-frame#book-progress[aria-busy="true"] { opacity: .65; }

/* ---- app-plane error pages (P1-1) ----
   The .not-found centring idiom from public.css, expressed in the app shell
   (public.css is not loaded on this plane). Layout only — no colour beyond the
   tokens the shell already uses, and no new token. */
.app-error { text-align: center; padding-top: clamp(24px, 8vw, 72px); }
.app-error p { margin: 0 auto; color: var(--ink-soft); }
.app-error .actions { justify-content: center; margin-top: 24px; }

/* ---- theming picker (D13, design spec §4.2/§4.3). Layout only — every value
   below is an existing token. This is the ONLY place .palette-* / .ground-*
   classes are allowed inside APP chrome, and only ever on a chip or the
   specimen: never on .app-shell, .app-main, or a .card, which must stay plum.
   The chip carries the preset class so the swatch is rendered by the SAME
   declaration that renders the author's page — if a preset is ever tuned, the
   picker follows for free and no hue is duplicated into a view. ---- */
.theme-axis { border: 0; margin: 0 0 18px; padding: 0; }
.theme-axis legend { padding: 0; margin-bottom: 8px; }

.swatch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 10px; }
.swatch { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 0; cursor: pointer; }
/* focusable but invisible — the ring goes on the LABEL so it stays plum app
   chrome rather than picking up the chip's own accent. Never `disabled`. */
.swatch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.swatch:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

.swatch-chip {
  position: relative; width: 100%; height: 46px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--radius);
}
.swatch-chip-accent { background: var(--accent); }
.swatch-chip-ground { background: var(--paper); }
.swatch-chip-ground i { width: 56%; height: 52%; border-radius: 6px; background: var(--paper-hi); border: 1px solid var(--line); }

.swatch-name { font-size: 13px; color: var(--ink-soft); }
.swatch-check { display: none; font-size: 15px; line-height: 1; }
.swatch-chip-accent .swatch-check { color: var(--paper-hi); }
.swatch-chip-ground .swatch-check { color: var(--ink); position: absolute; }
/* selection = ring + glyph + weight (NEVER colour alone) */
.swatch input:checked ~ .swatch-chip { box-shadow: 0 0 0 2px var(--ink); }
.swatch input:checked ~ .swatch-chip .swatch-check { display: block; }
.swatch input:checked ~ .swatch-name { color: var(--ink); font-weight: 600; }

/* the specimen: a mini page in the .callout / .chart-empty inset-well family. It
   carries the two chosen preset classes, so it shows the real thing rather than
   an approximation of it. */
.theme-specimen {
  display: flex; flex-direction: column; gap: 10px;
  padding: 16px; margin-top: 4px;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
}
.specimen-name { font-family: var(--serif); font-size: 20px; color: var(--ink); }
.specimen-card { background: var(--paper-hi); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; }
.specimen-button {
  display: inline-block; padding: 8px 16px; border-radius: var(--radius);
  background: var(--accent); color: var(--paper-hi); font-size: 14px; font-weight: 600;
}
.specimen-meter { display: block; height: 8px; border-radius: 999px; background: var(--paper-in); border: 1px solid var(--line); overflow: hidden; }
/* the fill is a fixed 62% here — a picture of a meter, not a meter. Kept in CSS
   rather than an inline style so the view carries no presentation at all. */
.specimen-meter i { display: block; height: 100%; width: 62%; background: var(--accent); }

/* ===========================================================================
 * RESPONSIVE — the app plane (mobile pass, Wave 2, 2026-08-02).
 * Design spec: docs/design/mobile-responsive-spec.md §2.3 (findings A-1..A-13),
 * §3 (breakpoints), §5.3 (these deltas), §9 (conformance C-1..C-11).
 * Supervisor rulings: docs/development/handoffs/mobile-wave2.md (W2-D1/D2/D5)
 * and mobile-wave1.md (D-5).
 *
 * ONE section, appended at the end of the file, exactly as public.css's Wave 1
 * block — a reviewer needs to read the whole responsive story in one place, and
 * `git blame` on a single block then tells the truth about the slice.
 *
 * THREE TIERS, and all three values already existed in this codebase before the
 * pass: 820px (the auth split-shell at :513), 560px (.tier-bar at :684), 480px
 * (marketing.css's hero CTA at :53). No new breakpoint is invented, and the one
 * 768px query in the tree stays where it is — inside vendored trix.css (C-4).
 *
 * DESKTOP-FIRST, `max-width` OVERRIDES, ON PURPOSE. This file is 800+ lines of
 * desktop-first cascade with load-bearing scars: the .button-quiet specificity
 * trap (:139-166), the `[hidden] !important` rule (:202-228), the .card .actions
 * source-order rule (:377-381). A mobile-first rewrite would re-open every one
 * of them in a pass whose entire value proposition is "nothing changed except
 * the shape at narrow widths". Additive max-width blocks make desktop-neutrality
 * checkable BY INSPECTION, which is worth more than idiom purity (§3, §11).
 *
 * SO: EXACTLY FIVE THINGS BELOW SIT OUTSIDE A `max-width` BLOCK — the two
 * `flex-wrap: wrap` declarations (W2-D5), the two fallback-preserving
 * `min-height` pairs, and one datetime `font-size` (W2-D2). They are enumerated
 * and argued at the rules themselves. Everything else in this section is inside
 * a max-width query, so at >=821px this whole block is inert. Conformance C-11
 * is answered by counting the declarations between here and the first `@media`.
 *
 * AMENDED 2026-08-02 (WAVE 3) — C-11'S AUDIT IS NOW A TWO-PLACE READ, AND THIS
 * IS THE OTHER PLACE. The sentence above was true when Wave 2 wrote it and is
 * no longer sufficient on its own: Wave 3 adds C-11's item 3, the SIX LIFTED
 * FLEX BASE CLASSES, and by supervisor ruling W3-D2 they live beside `.link-row`
 * at :577-622 rather than in this block — because a reader of `.link-row` needs
 * those values in front of them, and because they are byte-identical restatements
 * of inline `style` attributes rather than responsive rules.
 *
 *   .link-row-label (:614), .link-row-url (:615), .book-link-url (:616),
 *   .book-link-address (:617-620), .step-delay-value (:621),
 *   .step-delay-unit (:622)
 *
 * So counting from here to the first `@media` returns FIVE and the sanctioned
 * total is SIX; the sixth is at the line numbers above, with its provenance
 * commented per class. Nothing else in Waves 1-3 sits outside a max-width query.
 * (C-11's own list, as recounted by W2-D5 and spec §8.3 item 4, is five ITEMS —
 * item 3 being all six classes at once. Five items, six declaration sites, and
 * the two numbers are not the same number; that is why this note names both.)
 *
 * NOT ONE DECLARATION BELOW SETS A CUSTOM PROPERTY, inside a media query or
 * out of one. The .palette-* / .ground-* presets on <body> stay the only source
 * of colour at every width (D13) — a breakpoint that redefines --accent is a
 * theme that silently changes when you rotate your phone. Geometry, type scale
 * and overflow-wrap only (C-2, C-3; system test 2 asserts it mechanically).
 *
 * NOTHING INFORMATION-BEARING IS HIDDEN (C-8). No `display: none` is added at
 * any width; the one sanctioned instance in the file is .auth-hero-points at
 * :518, a decorative bullet list, and it predates this pass. Reflow, don't
 * amputate — an author on a phone must not have a smaller product than an
 * author on a laptop.
 *
 * AND NO `display` DECLARATION AT ALL lands on .condition-operand,
 * .featured-book-controls or .url-row-copy (C-7). Those three carry a
 * server-rendered / Stimulus `hidden` attribute, and ANY author-origin `display`
 * on them defeats it — see the archaeology at :202-228. .url-row-copy is raised
 * to a 44px tap target below via `min-height` + `padding`, never `display`.
 *
 * Zero `!important` (C-9), zero raw colour (C-1), zero view edits in the wave.
 *
 * "THE APP PLANE" IS A CLAIM ABOUT SELECTORS, NOT ABOUT THIS FILE. Two other
 * layouts load application.css — layouts/public.html.erb (then public.css) and
 * layouts/marketing.html.erb (then public.css, then marketing.css) — so an
 * unscoped selector here reaches whatever those planes happen to reuse. They
 * reuse a lot, on purpose: it is why the front door looks like the product.
 *   * `.app-header` / `.app-nav` are borrowed by marketing/home.html.erb:42,
 *     OUTSIDE any `.app-shell`. Every header rule below is therefore scoped
 *     `.app-shell …`. Wave 2 shipped them unscoped and regressed Awdur's own
 *     masthead; test/system/responsive_marketing_home_test.rb now guards it.
 *   * `.preview-bar` lives in layouts/public.html.erb and has NO `.app-shell`
 *     ancestor, so its tap-target rule must stay unscoped. Prefixing it would
 *     switch off a rule §5.3 asks for.
 * Every other selector below was audited against both non-app planes and matches
 * nothing there — but read the reason, because one of them is a near miss:
 * marketing DOES reuse `.card` (four of them, marketing/home.html.erb:140,146,
 * 153,159; layouts/marketing.html.erb:27 names the reuse), so `.card > p.ink-soft`
 * is the selector to watch. It matches nothing today only because none of those
 * four cards holds a `p.ink-soft` — the single `ink-soft` hit in that file is
 * `--ink-soft` inside an ERB comment at :215. Add one and this rule reaches the
 * front door. Neither non-app layout renders a `.flash` element at all (that grep
 * hit is Ruby `flash[:signup_error]`, which renders `class="error"`); the rest are
 * app-only components. Scope by plane where the plane is really shared —
 * do not `.app-shell`-prefix by reflex, and do not leave a shared selector bare.
 * ========================================================================= */

/* --- unconditional: the five sanctioned desktop-neutral changes (C-11) ---- */

/* design: THE APP HEADER WRAPS AT EVERY WIDTH. Supervisor ruling W2-D5, and the
   one place Wave 2 goes past the literal text of §5.3 — which puts these two
   declarations inside `@media (max-width: 820px)`.

   The measurement that moved them out: at the 844x390 LANDSCAPE PHONE the spec
   names by number in §8.3, the header is 877px wide in an 844px viewport and NO
   Wave 2 rule reaches it — every §5.3 rule lives at <= 820px, and 844 is above
   all three tiers. So an author who merely rotates their phone still gets the
   horizontal scroll that A-1 calls the single worst finding in the product, on a
   device the spec explicitly lists. (For the record: §8.3's claim that a 844x390
   landscape phone "hits the <= 820px NARROW tier" is wrong — 844 > 820, it hits
   none of the three. test/system/responsive_app_shell_test.rb asserts that
   negative explicitly rather than trusting the sentence.)

   ------------------------------------------------------------------------
   AMENDED 2026-08-02, after the Wave 2 gate BLOCKED on this comment. Read the
   correction before the claim it corrects; the original is left visible because
   the trap in it is worth more to the next reader than a tidy paragraph.

   THIS COMMENT USED TO ARGUE: "`flex-wrap: wrap` is the most rendering-neutral
   declaration in CSS. It changes nothing until a line would otherwise overflow
   — i.e. it can only ever fire where the rendering is ALREADY broken and
   scrolling sideways."

   THE SECOND HALF OF THAT IS FALSE, and the way it is false is the whole reason
   these two rules are now scoped to `.app-shell`. `flex-wrap: wrap` fires when a
   flex line would overflow ITS CONTAINER'S CONTENT BOX. That is not the same
   event as the DOCUMENT scrolling sideways, and on this header the two are ~148px
   apart. Without `wrap`, flex items do not overflow — they SHRINK, because
   `flex-shrink` defaults to 1. `.app-nav` is a shrinkable item of `.app-header`,
   so a no-wrap header squeezed silently, long before anything reached
   `documentElement.scrollWidth`.

   MEASURED 2026-08-02, headless Chrome, the dashboard signed in as the 76-char
   address responsive_app_shell_test.rb uses, sweeping the viewport 1px at a time:

     >= 1026px   identical with and without the rule. Header 71.4px, nav 870.3px,
                 #current-account 606.8px on one 22.4px line, document 1026/1026.
     == 1025px   THE RULES DIVERGE HERE.
                   with wrap:   header 119.4px — two rows, every control full size.
                   without:     header 87px — still ONE row, but #current-account
                                has been shrunk to 606.6px and its text pushed onto
                                a second line (22.4px -> 44.8px). SQUEEZED.
                 Document 1025/1025 in BOTH cases — nothing is scrolling sideways.
     ~ 877px     only NOW does the no-wrap document start to overflow (at a 870px
                 viewport: scrollWidth 878). Below this the nav has bottomed out at
                 768.2px and the address at its 529.3px min-content floor — the
                 shrink has run out of room and the page gives way instead.

   So the rule changes rendering across a ~148px band (1025px down to ~877px)
   where the page was NOT broken by the old comment's definition. Desktop
   neutrality is a real property of this rule but it is bounded, and the bound is
   1026px, not "wherever the page overflows".

   WHY IT IS STILL THE RIGHT DECLARATION — and this, not neutrality, is the
   argument: WRAPPING BEATS SQUEEZING. Both branches at 1025px change the header;
   only one of them keeps the author's content. The squeezed branch compresses the
   identity line and will go on compressing every control until it hits a
   min-content floor and then breaks the page anyway. The wrapped branch spends a
   row of vertical space and keeps every control at full size. And it is not
   speculative: it fires only when the content genuinely cannot fit on one line.

   AND WHY THE RULE NEEDED A PLANE BOUNDARY RATHER THAN A GLOBAL BLESSING. The
   marketing masthead is the counterexample that proves it. `marketing/home.html.erb:42`
   BORROWS `.app-header`/`.app-nav` and is NOT inside a `.app-shell`;
   `layouts/marketing.html.erb` loads this file. Unscoped, these two declarations
   plus four more below reached Awdur's own front door, where wrapping was exactly
   what nobody wanted — that masthead is a wordmark and one "Sign in" link, it
   FITS, and Wave 2 shipped it at 113px tall with the link on its own row against
   main's 71.4px single row. A rule that is right for a header carrying an email
   address and four controls is not automatically right for every element that
   happens to share its class name. `.app-shell` is the boundary; the app layout
   and errors/_shell.html.erb both provide it, the marketing home does not, and
   test/system/responsive_marketing_home_test.rb is the guard that keeps it that
   way. See docs/development/handoffs/mobile-wave2.md ruling W2-D5 and spec §8.3.
   ------------------------------------------------------------------------

   Not fixed by widening a breakpoint: C-4 fixes the three values, and an 844px
   tier would be a fourth. `row-gap` stays at 820 (below) because a row gap is
   chrome trim, not the fix — and .app-header already carries `gap: 16px` (:252),
   which supplies a sane row gap above the tier the moment a line breaks. */
.app-shell .app-header { flex-wrap: wrap; }
.app-shell .app-nav { flex-wrap: wrap; }

/* design: 100vh on a mobile browser measures the EXPANDED viewport, so the last
   ~60px of a full-height shell sits under the collapsing URL bar (A-8). dvh
   tracks it. The vh line stays FIRST as the fallback — any engine that does not
   know dvh keeps today's behaviour, any engine that does overrides it, with no
   @supports and no feature query. Unconditional because the pair is a desktop
   no-op by inspection: on a desktop browser dvh == vh (C-11).
   These restate min-height only; .app-shell's and .auth-shell's own rules at
   :248 and :472-481 keep everything else, and this block's position at the end
   of the file is what makes the restatement win. */
.app-shell  { min-height: 100vh; min-height: 100dvh; }
.auth-shell { min-height: 100vh; min-height: 100dvh; }

/* design: ANY input under 16px makes iOS Safari zoom the page on focus and never
   zoom back. The global form rule is correctly 16px (:113-122); this datetime
   field at 15px (:557-562) is the one leak in the app plane (A-7). Fixed
   unconditionally rather than at a breakpoint, per §5.3's own comment and
   conformance C-11: a 1px difference is not worth a media query, and the
   schedule row's other controls are already 15-16px.
   The SECOND iOS auto-zoom leak — Trix's 12.75px link-dialog input (E-4) — is
   deliberately NOT here: ruling W2-D1 keeps both Trix findings in Wave 3, where
   they ship beside the toolbar rules they belong to. Do not fold it forward. */
.schedule-controls input[type=datetime-local] { font-size: 16px; }

/* --- NARROW (<= 820px) --------------------------------------------------- */
@media (max-width: 820px) {
  /* design: the header holds an email address, three or four quiet links and a
     sign-out form — roughly 520px of content in a space-between flex row. The
     wrap that fixes it is unconditional above; what belongs at this tier is only
     the chrome trim, because a wrapped header's two rows sit 16px apart under
     the base `gap` and that reads as a gap, not a stack. 10px tightens it once
     wrapping is actually plausible. */
  .app-shell .app-header { row-gap: 10px; }
}

/* --- PHONE (<= 560px) ---------------------------------------------------- */
@media (max-width: 560px) {
  /* --- the app header, stacked ------------------------------------------ */
  /* design: the nav takes its own full-width row under the wordmark, and the
     identity line takes its own row inside the nav. Reading order stays DOM
     order — wordmark, who you are, what you can do — because reordering with
     `order:` would put the visual sequence out of step with the accessible one
     (WCAG 1.3.2) to save 20px.

     #current-account is asserted by test/integration/authentication_test.rb and
     sessions_test.rb. It is NOT hidden and its text is NOT changed: it goes to
     13px, takes the full row, and gains overflow-wrap so a long address wraps
     instead of pushing the page sideways. Every assertion on that element keeps
     passing — assert_select cannot see CSS at all (handoff.md gotcha 12), which
     is exactly why this is the safe move rather than the tempting one.

     BOTH HALVES OF THE #current-account RULE ARE LOAD-BEARING, measured at a
     true 390x844 viewport against a 76-character address:
       width: 100% + overflow-wrap  -> document 390px (correct)
       width: 100%, no overflow-wrap -> document 511px (the token cannot break)
       overflow-wrap, no width       -> document 511px (the D-5 flex floor: the
                                        span's box IS the whole address, 491px)
     `anywhere` rather than `break-word` because an email address has no word
     boundary to prefer, and because `anywhere` is the half that survives if the
     declared width is ever refactored away. Do not simplify either away.

     EVERY SELECTOR IN THIS SUB-BLOCK IS SCOPED TO `.app-shell`, and so are the
     two unconditional `flex-wrap` rules above and the `row-gap` at 820. That is
     not defensive prefixing: `.app-header`/`.app-nav` are BORROWED by
     `marketing/home.html.erb:42`, which sits outside any `.app-shell`, and
     `layouts/marketing.html.erb` loads this file. Unscoped, these three rules
     land on Awdur's own masthead — `width: 100%` is the one that drops its lone
     "Sign in" link onto its own row. `#current-account` matches nothing over
     there (the marketing header has no such id) and is scoped anyway, so the
     block reads as one plane-scoped unit and a later reader does not have to
     work out which omissions were deliberate.
     See the amended note on the `flex-wrap` rules above for the full argument,
     and test/system/responsive_marketing_home_test.rb for the guard. */
  .app-shell .app-header { padding: 12px 20px; }
  .app-shell .app-nav { width: 100%; gap: 8px 10px; }
  .app-shell .app-nav #current-account {
    width: 100%; font-size: 13px; overflow-wrap: anywhere;
  }

  /* --- tap targets ------------------------------------------------------ */
  /* design: ONE rule, every compact control the app ships (A-9). The base button
     (13px 22px, ~48px tall, :129-135) already clears the floor and is the
     reference; these seven selectors were tuned for a mouse and land between
     31px and 41px — MEASURED at 390px, the two header anchors are 38.4px, so
     A-9's "~35px" estimate was if anything generous. The horizontal padding
     rises with the vertical so they still read as buttons rather than stretched
     pills.

     .url-row-copy is the important one in this list: "copy my page address" is
     the single most likely thing an author does from a phone, and it is a 31px
     target today. It reaches 44px by min-height + padding and NEVER by
     `display` — it is site #3 of gotcha 12 (C-7).

     HOW EACH SELECTOR WINS — AMENDED 2026-08-02 after the Wave 2 gate. This
     comment used to say, of all seven at once, that the rule "wins on SOURCE
     ORDER from the end of the file rather than on weight ... and this block must
     stay last in the file". Both halves needed correcting, in opposite
     directions, so the honest version is per-selector:

       .app-shell .app-nav .button-quiet  — WINS ON WEIGHT NOW. 0,3,0 against the
         base `.app-nav .button-quiet` at :257 (0,2,0). The `.app-shell` scope was
         added to keep this rule off the marketing masthead (see the amended note
         on the flex-wrap rules above), and raising the specificity is a side
         effect that STRENGTHENS the no-`!important` position (C-9): this one no
         longer depends on where in the file it sits at all.
       .url-row-copy (:399), .link-row-btn (:569), .condition-remove (:628),
       .card-head .card-head-action (:754), .data-table td button (:299)
         — these five still restate their base selector exactly, so they win on
         SOURCE ORDER ONLY. They are the reason this block must stay last. Do not
         read the line above as covering them.
       .preview-bar .button-quiet — 0,2,0, restating :437, so within THIS file it
         wins on source order like the five above. See the plane note below; on
         the plane it actually renders on, that is not the end of the story.

     "LAST IN THE FILE" IS NOT "LAST IN THE CASCADE", and the previous version of
     this comment overstated it. Being last in application.css only orders these
     rules against other rules in application.css. That is the whole truth on the
     APP plane, where layouts/application.html.erb loads this stylesheet and
     nothing else. It is NOT the truth on the other two planes:
     layouts/public.html.erb loads public.css AFTER this file, and
     layouts/marketing.html.erb loads public.css and then marketing.css after it —
     so on those planes a same-specificity rule in either of them beats anything
     here regardless of position.

     .preview-bar .button-quiet is the live instance of exactly that, and it is
     the one selector in this list that renders on a non-app plane at all
     (.preview-bar appears only in layouts/public.html.erb). public.css:350 states
     the same selector at the same tier — `min-height: 44px; padding: 10px 16px` —
     and being in the later stylesheet it WINS on min-height and padding. This
     rule's font-size: 15px is uncontested there and does apply. The outcome is
     the same either way (the control is >= 44px, which is all §5.3 asks), so
     this is a redundancy and not a conflict. It is deliberately NOT scoped to
     `.app-shell`: .preview-bar has no such ancestor, and prefixing it would
     silently switch off a rule the spec enumerates. Kept in this list because
     .preview-bar itself is declared in THIS file (:427-437) and a reader of the
     app plane's tap-target inventory should not have to know the public plane
     also states it.

     The sign-out control is a <button> and so already picks up the base button
     metrics at 54px; `min-height: 44px` is a knowing no-op for it. That is
     expected, not a miss. */
  .app-shell .app-nav .button-quiet,
  .preview-bar .button-quiet,
  .url-row-copy,
  .link-row-btn,
  .condition-remove,
  .card-head .card-head-action,
  .data-table td button {
    min-height: 44px; padding: 11px 16px; font-size: 15px;
  }

  /* design: a checkbox ROW is the documented exception to the 44px floor
     (A-10). 44px per row turns the tag picker into a three-item viewport and
     the featured-books card into a scroll. 36px clears WCAG 2.5.8's 24px AA
     floor with room, keeps dense lists legible, and is the standard concession
     for list items in a scrolling well. Standalone controls stay at 44.
     min-height only: .check-inline is already `display: flex` (:602) and this
     rule must not restate it — .featured-book-pick borrows this class inside a
     row whose sibling .featured-book-controls is gotcha-12 site #2. */
  .check-inline { min-height: 36px; }

  /* --- the flash -------------------------------------------------------- */
  /* design: <body> in the application layout has no padding of its own, so a
     640px max-width flash sits FLUSH against both screen edges on any phone —
     alone among every element in the app (A-2). min() keeps the desktop
     centring byte-identical (640px still wins wherever the viewport can afford
     it) and buys the app's own 22px gutter below 684px, matching .app-main's
     gutters at :259 so a flash lines up with the card under it. */
  .flash { max-width: min(640px, calc(100% - 44px)); }

  /* --- unbreakable addresses -------------------------------------------- */
  /* design: `verylongpenname.awdur.com/b/the-book-slug` contains no break
     opportunity — UAX #14 gives the browser nothing at `.` or `/`. .url-row
     already wraps as a flex container (:394), but the TOKEN cannot, so it
     overflows the card at ~240px of inner width (A-3). Three live sites.

     `anywhere` rather than `break-word` because a hostname has no word
     boundaries to prefer — the §5.1/D-5 argument for keeping `break-word` on
     public-plane prose labels does not transfer to an opaque token.

     `min-width: 0` IS NOT OPTIONAL AND IS NOT REDUNDANT (Wave 1 ruling D-5).
     CSS Text 3 deliberately excludes the permitted break from a box's intrinsic
     min-content contribution, so a flex item with the default `min-width: auto`
     stays floored at the width of the whole unbreakable word and the break can
     never fire — Wave 1 measured exactly this on .author-link-label (501px of
     content in a 501px box AFTER the overflow-wrap rule landed). .url-row-address
     is a flex item of .url-row, so it needs the release valve. This is the one
     pair in the file most likely to be "simplified" back into a bug. */
  .url-row-address { overflow-wrap: anywhere; min-width: 0; }

  /* design: the same token printed as PROSE — the books index prints
     `subdomain.host/b/slug` inside the card's ink-soft line
     (pages/books/index.html.erb:56, finding A-4). Scoped to a direct-child
     ink-soft paragraph of a card so it cannot leak into author bio copy or any
     other prose on the plane.
     VERIFIED against spec §7.1's flag, which asks whether the books-EMPTY
     selling card's example address (an <em> inside a <p>) is reached by this
     rule: it is. §7.1 says that paragraph is "a plain <p>", but the shipped
     markup at pages/books/index.html.erb:18 is `<p class="ink-soft">` and a
     direct child of the `.card`, so the selector matches and `overflow-wrap`
     inherits into the <em>. No widening to `.card p em` was needed — the spec's
     note is stale, not the rule. */
  .card > p.ink-soft { overflow-wrap: anywhere; }

  /* --- headings --------------------------------------------------------- */
  /* design: an h1 on this plane can be a broadcast subject or a book title —
     author text, arbitrary length, no guaranteed spaces. `break-word` (not
     `anywhere`) because these are prose headings: break between words first,
     split a word only when a single word genuinely cannot fit. Neither element
     is a flex item — .app-main-head is the flex container and its h1 is the
     item, but the item is a block-level heading with `margin: 0` and no
     competing sibling on its line, so the D-5 min-width pair is not needed
     here. If a future rule makes either a shrinking flex item, revisit it. */
  .app-main-head h1, .app-error h1 { overflow-wrap: break-word; }

  /* --- charts ----------------------------------------------------------- */
  /* design: Observable Plot emits an SVG with width/height attributes and NO
     viewBox, so the `max-width: 100%` at :659 CLIPS rather than scales it after
     a rotate (A-6). Scroll is the honest CSS fallback: a clipped axis is a lie
     about the data, a scrollable one is not. The real fix is the
     chart_controller.js resize delta shipping alongside this in Wave 2.
     Consequence to expect, not to fix: .chart now legitimately reports
     scrollWidth > clientWidth, so it appears in the system harness's OFFENDER
     DIAGNOSTICS. Those are message-only and nothing asserts on them (W2-D3). */
  .chart { overflow-x: auto; }

  /* --- table scroll affordance ------------------------------------------ */
  /* design: .table-scroll (:288) works, but a touch device paints no scrollbar,
     so the scrollability is invisible (A-11) — the admin roster, the audit log,
     the import problem sample and the analytics per-book table all rely on it.
     A hairline inset well says "this box has its own edges, and they are not the
     page's" without inventing a component or an icon. --line-soft rather than
     --line because the affordance must read quieter than the .card border it
     sits inside. A shadow-based "there's more this way" overlay is the richer
     answer and was judged not worth the complexity for four tables (§11).
     Same caveat as .chart: this box is a legitimate offender in the harness's
     diagnostics by design. */
  .table-scroll {
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    -webkit-overflow-scrolling: touch;
  }

  /* =========================================================================
   * WAVE 3 — THE HARD EDITORS (2026-08-02). Spec §2.4 (E-1..E-6), §5.4.
   * Supervisor rulings: docs/development/handoffs/mobile-wave3.md W3-D1..W3-D6.
   *
   * Appended INSIDE the existing phone tier rather than opened as a second
   * `@media (max-width: 560px)` block, and the Trix rules are here rather than
   * beside the re-skin block at :521-533 (whose pointer comment at :534-539
   * sends a reader here), both per W3-D1: one responsive section per stylesheet
   * is what keeps C-11 answerable by reading one place.
   *
   * THREE DIVERGENCES FROM THE BRIEF, each measured, each argued at its rule,
   * none of them a build convenience. Read them before tidying:
   *   1. `.condition-fixed-op { color: var(--ink-soft) }` is OMITTED (W3-D6) —
   *      the markup already carries `.ink-soft` and the colour was measured
   *      identical. See :1351-1362.
   *   2. `.condition-operand .condition-days { width: 6rem }` SHIPS, although
   *      W3-D6 asked for it to be dropped as redundant — measurement shows the
   *      value it was said to duplicate has never applied. See :1368-1381.
   *   3. `trix-toolbar .trix-button-group { flex-wrap: wrap }` is ADDED; §5.4
   *      does not write it, and without it the toolbar is still a 2px
   *      horizontal scroller. See :1460-1475.
   * ====================================================================== */

  /* ---- SEGMENTS CONDITION ROW (E-1, E-2) --------------------------------- */
  /* design: the row already wraps, but wrapping is not enough. The global
     `select { width: 100% }` (:113-122) makes every select in a flex row demand
     100% as its flex basis; stacking removes the competition entirely — one
     control per line at its natural full width, which is what the global input
     rule wanted all along.

     `display` MUST NOT APPEAR ON `.condition-operand`. It is site #1 of
     handoff.md gotcha 12 and the reason `[hidden] { display: none !important }`
     exists at :202-228: the builder pre-renders all five operand groups and
     hides four with the `hidden` attribute, and ANY author-origin `display`
     here defeats that. `flex-direction` and `align-items` only (C-7).

     `align-items: stretch` IS LOAD-BEARING, NOT DECORATION. Without it,
     `flex-direction: column` collapses each `<select>` to its min-content width
     and produces exactly the "opened an…" truncation E-1 describes — which is
     the defect, arrived at from the other direction. Issue A measured that and
     test/system/segments_builder_test.rb pins it.

     A MEASURED CORRECTION TO E-1'S MECHANISM, recorded because it changes what
     these two rules are FOR. E-1 predicts the activity row's three controls
     shrink toward min-content today. MEASURED at 390px on main: they do not.
     `.condition-operand` is ALREADY `flex-wrap: wrap` (:676), so each select
     takes the full 308px on its own line. The row E-1 describes is one that
     WRAPS, not one that SHRINKS. So `flex-direction: column` formalises what
     wrapping already delivers, and the pair's real job is to make that
     stacking explicit and non-accidental. */
  .condition-row { flex-direction: column; align-items: stretch; gap: 12px; }
  .condition-operand { flex-direction: column; align-items: stretch; }

  /* §5.4 ALSO ASKS FOR `.condition-fixed-op { color: var(--ink-soft) }` HERE.
     IT IS DELIBERATELY OMITTED — supervisor ruling W3-D6, and the omission was
     measured, not assumed. The shipped markup writes
     `class="condition-fixed-op ink-soft"` on all four of those spans
     (audience/segments/_condition_row.html.erb:67,80,93,115), so the colour is
     already stated on purpose in our own source. MEASURED at 390px, all four
     spans ("is one of", "signed up", "has", "came from the import"): the
     computed `color` is the sRGB triple 115/107/92, which is channel-for-channel
     the value `--ink-soft` declares at :18 — the declaration §5.4 asks for would
     set the colour to what it already is. (Written as a triple rather than as a
     literal so conformance C-1's grep for raw colour in this diff stays at
     zero; the token, at :18, is still the only place that value is spelled.)
     The sketch's author did not have the `ink-soft` class in front of
     them. Rendering is identical either way, and shipping a provably inert
     declaration into a block whose whole value proposition is "every rule here
     was measured" is worse than shipping nothing. */

  /* design: a number + its unit is the ONE pair that wants to stay on a line.
     `align-self: flex-start` keeps the days input from stretching to the
     column's full width once `align-items: stretch` lands above.

     `width: 6rem` SHIPS HERE, AGAINST W3-D6's INSTRUCTION TO OMIT IT, BECAUSE
     THAT INSTRUCTION'S PREMISE IS FALSE BY MEASUREMENT — flagged to the
     supervisor rather than absorbed. W3-D6 omits it on the grounds that
     `.condition-days { width: 6rem }` at :679 "already states it, same file,
     same value". It states it and it DOES NOTHING: `.condition-days` is (0,1,0)
     and the global `input[type=number]` rule at :113-122 is (0,1,1), so the
     global `width: 100%` wins on weight at every viewport. MEASURED on main —
     the signup row's days input is 308px at 390px and 335.6px at 1400px, never
     the 96px that 6rem would give. The compound selector below is (0,2,0), the
     first weight in the file that actually beats the global rule, so this is
     the only place 6rem has ever been true.
     W3-D6's own dividing question is "what is holding the value up today"; the
     answer here is nothing, which puts it on the KEEP side of that ruling, not
     the OMIT side. Ship the spec as written; see the Wave 3 report. */
  .condition-operand .condition-days { width: 6rem; align-self: flex-start; }

  /* design: `margin-left: auto` (:681) strands Remove alone at the far right of
     a wrapped line, where it reads as belonging to the row BELOW it — MEASURED
     at 390px on main: Remove starts at 261.3px against its row's operand group
     at 41.0px, a computed margin-left of 220.3px. Stacked, Remove is simply the
     row's last item and needs no push. Restates :681's selector exactly, so it
     wins on SOURCE ORDER — another reason this block stays last in the file. */
  .condition-remove { margin-left: 0; align-self: flex-start; }

  /* design: `.check-inline` is 36px at this tier (:1214), so a 168px well shows
     three tags instead of the eight it was drawn for (E-2). 50vh is a well, not
     a page, and it still scrolls. MEASURED at 390x844: the well goes 168px ->
     422px against 445px of tag content, i.e. 10 tags visible instead of 4, and
     it is still a scroller rather than an unbounded list. */
  .tag-picker { max-height: 50vh; }

  /* ---- FEATURED BOOKS (E-6) ---------------------------------------------- */
  /* design: the title is `white-space: nowrap; text-overflow: ellipsis` beside
     a 34px thumb and two reorder arrows, so it gets ~110px at 292px and every
     title reads "The Bone Orch…" — MEASURED at 390px on main: 497px of title in
     a 170px box. Two lines is honest: pick + thumb + title on row 1, the arrows
     on row 2, right-aligned by `margin-left: auto` so they stay a pair.

     `display` MUST NOT APPEAR ON `.featured-book-controls` — site #2 of gotcha
     12 (C-7). The featured-books Stimulus controller sets `controls.hidden =
     true` on un-featured rows, and an author-origin `display` here beats the
     user agent's `[hidden]`. `margin-left` only; the base rule at :644 keeps
     the `display: flex`. featured_books_reorder_test.rb asserts the un-featured
     row's arrows stay unpainted at 390px.

     `overflow-wrap` AND `min-width` BELOW ARE ONE INSEPARABLE PAIR. DELETING
     EITHER RE-OPENS A HORIZONTAL SCROLLBAR — this is the single most
     "simplifiable"-looking thing in this block and simplifying it is a bug.
     Relaxing `white-space`/`overflow` makes `.featured-book-title` a WRAPPING
     flex item of `.featured-book-pick`, and CSS Text 3 deliberately EXCLUDES the
     break `break-word` permits from a box's intrinsic min-content contribution —
     so a flex item at the default `min-width: auto` stays floored at the width
     of the whole unbreakable token and the break can never fire. That is Wave 1's
     ruling D-5 and spec §8.2's amended item 3, which names this exact selector.
     MEASURED at 390px with §5.4's two declarations live and the pair absent, on a
     61-character title containing no spaces: the title box is 446px inside a
     308px row and `documentElement.scrollWidth` is 515 against a 390px viewport —
     125px of horizontal scroll, i.e. finding A-1's shape on a surface Wave 2 left
     healthy. On main the same fixture did NOT overflow, because `nowrap` +
     `overflow: hidden` clipped it, so this is a regression Wave 3 INTRODUCES.
     MEASURED with the pair live: the title box is 280px inside the 308px row, two
     24px lines tall, right edge 349px, and the document is 390/390.
     Ruling W3-D9.

     `break-word`, NOT `anywhere`, AND IT WAS MEASURED RATHER THAN ASSUMED: with
     `break-word` the 61-character token wraps onto two lines inside 280px and the
     document is clean, so `anywhere` buys nothing here and would cost
     line-breaking quality on ordinary titles.
     A book title is prose, and §5.3's own precedent is
     `.app-main-head h1 { overflow-wrap: break-word }` — break between words
     first, split a word only when a single word genuinely cannot fit.
     `.url-row-address` gets `anywhere` because a hostname is one opaque token
     with no word boundary to prefer; a title is not that.
     featured_books_reorder_test.rb pins BOTH shapes with two deliberately
     different fixtures (ordinary words for the ellipsis, one unbreakable token
     for this floor); they are not interchangeable and must not be merged.

     DESKTOP IS UNTOUCHED, AND THAT WAS MEASURED TOO, not just argued from the
     media query: at 1400px the same title still computes `white-space: nowrap`,
     `overflow: hidden`, `text-overflow: ellipsis`, `overflow-wrap: normal` and
     `min-width: auto` — the base rule at :640 verbatim — in a 445.5px box on a
     1400/1400 document. */
  .featured-book-row { flex-wrap: wrap; }
  .featured-book-pick { flex: 1 1 100%; }
  .featured-book-title {
    white-space: normal; overflow: visible;
    overflow-wrap: break-word; min-width: 0;
  }
  .featured-book-controls { margin-left: auto; }

  /* ---- TRIX TOOLBAR (E-3) ------------------------------------------------ */
  /* design: WRAP, DON'T SCROLL. Vendored trix.css:14-19 ships `flex-wrap:
     nowrap; overflow-x: auto` on the button row, which on a touch device is a
     scrolling strip with NO VISIBLE SCROLLBAR — half the formatting controls do
     not exist as far as the author knows. MEASURED at 390px on main:
     scrollWidth 360 against clientWidth 308, i.e. 52px (about two buttons)
     parked past an invisible edge. And `.trix-button--icon` is capped at
     `max-width: calc(0.8em + 3.5vw)` inside trix.css's own
     `@media (max-width: 768px)` — MEASURED: all 13 visible buttons are
     27.3 x 34.0, against a 44px floor.

     REJECTED: hiding the block-tools group at phone width. That costs an author
     on a phone the ability to make a bullet list, which is a product decision
     disguised as a layout one. Capability must not depend on viewport.

     WHY THERE IS NO `!important` HERE (C-9), AND WHY IT IS SAFE. trix.css is
     VENDORED and regenerated on upgrade, so it is never edited (C-5); every
     override lands in this file. layouts/application.html.erb emits
     `yield :head` — where broadcasts/new.html.erb:5 and
     email_marketing/sequences/edit.html.erb:5 link trix.css — at :16, and
     `stylesheet_link_tag "application"` at :26. So application.css is the LATER
     stylesheet. The two rules being overridden, `trix-toolbar .trix-button-row`
     (trix.css:14) and `trix-toolbar .trix-button--icon` (trix.css:85-90), are
     both (0,1,1) — identical weight to ours — and A MEDIA QUERY ADDS NO
     SPECIFICITY, so ours win on source order alone. That is exactly why this
     block must remain the last thing in the file. Verified by measurement, not
     by argument: test/system/responsive_composer_test.rb reads the computed
     `flex-wrap` and every button's box.

     With the file-attachment group already hidden (:525), the remaining 13
     buttons at 44px reflow inside a 308px column. §5.4 predicted "two rows";
     MEASURED, it is FOUR button rows and a 194px toolbar — the three visible
     groups stack (text 46px, block 90px = two rows of its own, history 46px)
     plus two 6px gaps. That is a lot of chrome, and it is still the right
     trade: the alternative is 52px of controls behind an invisible scroll.
     If 194px reads as oppressive in the hand it is a design call, not a bug —
     logged for the same real-phone read Q1 is waiting on. */
  trix-toolbar .trix-button-row {
    flex-wrap: wrap;
    justify-content: flex-start;
    overflow-x: visible;
    gap: 6px;
  }
  /* `flex-wrap: wrap` HERE IS §5.4's SKETCH CORRECTED BY MEASUREMENT — it is
     the one declaration Wave 3 adds that §5.4 does not write, and it is the
     difference between test 5 passing and failing. §5.4 wraps the button ROW
     and expects "the remaining ~11 buttons at 44px [to] wrap to two rows",
     but the buttons are not the row's flex items — the four
     `.trix-button-group`s are, and each group is its own `display: flex`
     container at trix.css's default `nowrap`. MEASURED at 390px with only
     §5.4's rules applied: the row DID wrap (three group rows), but the
     block-tools group is 7 buttons x 44px + 2px of border = 310px of
     UNSHRINKABLE min-content in a 308px row, so the toolbar was still a
     2px scroller (scrollWidth 310 / clientWidth 308) — the same undiscoverable
     strip E-3 describes, just narrower. Letting the group wrap drops its
     min-content to one button and it reflows inside its own border.
     Same selector §5.4 already writes for `margin-bottom`; one declaration;
     inside the phone tier, so desktop is untouched. Raised in the Wave 3
     report. */
  trix-toolbar .trix-button-group { margin-bottom: 0; flex-wrap: wrap; }
  trix-toolbar .trix-button-group:not(:first-child) { margin-left: 0; }
  trix-toolbar .trix-button { min-height: 44px; }
  trix-toolbar .trix-button--icon {
    width: 44px; max-width: none; height: 44px;
  }

  /* design: THE LINK DIALOG (E-4). Read the correction before the rule.

     E-4'S PREMISE IS HALF WRONG, AND SAYING SO IS THE POINT OF THIS COMMENT.
     E-4 states that `.trix-dialog { font-size: 0.75em }` (trix.css:163) plus
     `.trix-input--dialog { font-size: inherit }` (trix.css:176) yields a
     12.75px URL field that makes iOS zoom on focus. MEASURED at 390px on main:
     the dialog BOX is 12.75px, but the INPUT is already 16px and 49.0px tall.
     Reason: the element is `<input type="url">`, so this file's global form rule
     at :113-122 matches it at (0,1,1), EXACTLY TIES `trix-toolbar
     .trix-input--dialog` at (0,1,1), and wins on the same source order argued
     above. Nothing states that intent and no test named it until Wave 3.

     `font-size: 16px` and `min-height: 44px` SHIP ANYWAY (ruling W3-D6). They
     are not a restatement of an in-repo intent — they are a floor held up by an
     undocumented cross-file tie. Reorder those two `stylesheet_link_tag`s, or
     scope the global input rule, or take a trix upgrade that raises that
     selector's weight, and the iOS auto-zoom leak silently reopens with nothing
     to notice. These two declarations convert an accident into an intention.
     DO NOT delete them as dead weight, and do not read this comment as a claim
     that Wave 3 fixed a 12.75px input; it did not.

     THE GENUINELY RED HALF IS THE DIALOG'S OWN BUTTONS. `trix-toolbar
     .trix-button--dialog { font-size: inherit }` (trix.css:192-196) is (0,1,1)
     and beats this file's bare `button` rule at (0,0,1), so Link and Unlink
     inherit the dialog's 12.75px — MEASURED at 390px on main: 38.5 x 27.8 and
     52.0 x 27.8, against a 44px floor. That is what `min-height` below is for.

     WHY THE SELECTOR BELOW CARRIES A SECOND CLASS IT DOES NOT SEEM TO NEED —
     READ THIS BEFORE SHORTENING IT (ruling W3-D8). `flex: 1 1 100%` at §5.4's
     own `trix-toolbar .trix-input--dialog` (0,1,1) IS OUTRANKED AND INERT:
     trix.css:204-206 states
     `trix-toolbar .trix-dialog__link-fields .trix-input { flex: 1 }` at (0,2,1),
     two classes beat one, and SOURCE ORDER IS NEVER CONSULTED. So this is the one
     trix-toolbar override in this block that does NOT win the way the source-order
     argument above wins the others. MEASURED at 390px with §5.4's selector: the
     field still computed `1 1 0%`, the `flex-wrap: wrap` beside it never fired,
     and the URL input (195.6px) sat on ONE 288px line with the Link/Unlink pair —
     buttons top 580.1 against an input running 576.6 to 625.6, i.e. 45.5px of
     overlap. MEASURED with the selector at (0,2,1): the field computes
     `1 1 100%`, takes the full 288px, and the buttons drop to their own line
     (input bottom 625.6, buttons top 634.6 — the 8px `row-gap`).
     `flex` IS THE ONLY DECLARATION THAT NEEDED THE WEIGHT.

     THE WHOLE RULE MOVED, NOT JUST `flex`, AND THAT WAS CHECKED RATHER THAN
     ASSUMED. Narrowing a selector normally risks dropping declarations off
     elements they used to reach — here it cannot: `trix-input--dialog` appears
     exactly ONCE in the vendored toolbar template
     (vendor/javascript/trix.js:37), always inside `.trix-dialog__link-fields`,
     so the two selectors match the SAME single element in this app. One rule
     reads better than two, and a reader who splits them later would have to
     re-derive the specificity fact the hard way. ON A TRIX UPGRADE, re-grep that
     template: a second `.trix-input--dialog` outside the link fields would fall
     out of this rule's reach and lose its 16px iOS floor.

     A BONUS, AND IT IS THE POINT OF W3-D6: at (0,2,1) `font-size: 16px` now beats
     this file's global `input[type=url]` rule (:113-122, (0,1,1)) ON WEIGHT rather
     than tying it and winning on stylesheet order. The accident W3-D6 wanted
     converted into an intention is now genuinely converted at the phone tier.
     `margin-right: 0` already applied at the lighter weight (it ties trix.css:180
     and wins on source order) and buys the field 10px: MEASURED 185.6 -> 195.6.

     DESKTOP IS UNTOUCHED, MEASURED AND NOT MERELY ARGUED FROM THE MEDIA QUERY: at
     1400px the fields row still computes `flex-wrap: nowrap`, the field still
     computes `flex: 1 1 0%` (trix.css:204 unopposed) at 476.3px of a 580px row,
     on the SAME line as the buttons, 16px and 49px tall from the global
     `input[type=url]` rule — identical to main. */
  trix-toolbar .trix-dialog__link-fields { flex-wrap: wrap; row-gap: 8px; }
  trix-toolbar .trix-dialog__link-fields .trix-input--dialog {
    font-size: 16px; min-height: 44px; flex: 1 1 100%; margin-right: 0;
  }
  trix-toolbar .trix-button--dialog { min-height: 44px; }

  /* ---- ROWS FREED FROM INLINE FLEX (E-5) --------------------------------- */
  /* design: THIS IS THE PAYOFF FOR THE MARKUP EDIT. Each class below exists
     only to hold a flex value that used to be an inline `style` attribute — an
     attribute that outranks every media query, which made these rows literally
     un-stackable in CSS (E-5). The base declarations at :614-622 reproduce the
     old inline values byte-for-byte, so desktop is unchanged; these four lines
     are what the markup edit bought.

     `.step-delay-value` goes to `5.5rem`, which is a DELIBERATE 2px NARROWING
     from the 90px it restates: the root font size is 16px on every one of these
     pages (MEASURED), so 5.5rem is 88px. Not a rounding error — stated in rem
     because it is a phone-tier size beside a unit select that now grows, and
     responsive_editors_test.rb expects exactly `0 0 88px`.

     `.book-link-address` GETS NO OVERRIDE HERE, AND THE OMISSION IS
     DELIBERATE — do not lump it in with the three above. It is the read-only
     retailer address, and an ellipsised single line IS the design: the full URL
     lives in the "Edit this link" disclosure directly below it, which is what
     `.book-link-url` stacks. responsive_editors_test.rb asserts it still
     computes `1 1 auto` at 390px, so adding it here goes red. */
  .link-row-label,
  .link-row-url,
  .book-link-url { flex: 1 1 100%; }
  .step-delay-value { flex: 0 0 5.5rem; }
  .step-delay-unit  { flex: 1 1 auto; }
}

/* --- SMALL (<= 480px) ---------------------------------------------------- */
@media (max-width: 480px) {
  /* design: .tier-bar is `9rem 1fr 4rem`, trimmed to `7rem 1fr 3.5rem` at 560
     (:684) — a tablet trim, not a phone one. At 390px that leaves the track
     ~118px and forces 14px labels like "New reader" or "Kindle Store" to wrap
     inside a 112px cell, breaking the row's baseline (A-5). Below 480 the row
     becomes two lines: label and value share row 1 (the value stays
     right-aligned, as it already is at :682), and the track spans row 2 at full
     width. Same three elements, same DOM, same tokens — the bar gets the whole
     column and the label gets to be a whole word.
     The 560px rule at :684 STAYS and is not merged into this one: it is the
     correct layout for the 480-560 band, and this block only overrides the
     properties it needs on top of it. Do not move or fold them together.

     BOTH GRID PLACEMENTS ARE EXPLICIT, AND THAT IS A CORRECTION TO §5.3'S
     SKETCH — read this before "simplifying" it back. The sketch writes only
     `grid-template-columns: 1fr auto` + `.tier-bar-track { grid-column: 1/-1 }`
     and leaves the rest to auto-placement. But the shipped DOM order is
     label -> TRACK -> value (analytics/_tiers.html.erb:12-14 and
     _books.html.erb:33-38), not label -> value -> track, so auto-placement puts
     the label at row 1 col 1, discovers the full-width track cannot fit beside
     it and pushes the track to row 2, then drops the VALUE onto a row 3 of its
     own — leaving column 2 empty at 0px. MEASURED at 390px: three rows, 70.8px
     tall, against the 22.4px single row it replaces.
     That is not the layout §5.3's own prose asks for ("label and value share
     row 1, the value right-aligned as it already is; the track spans row 2 full
     width"), so the placements are stated rather than inferred. Same three
     elements, same DOM, same tokens, same intent — two lines, as specified. */
  .tier-bar {
    grid-template-columns: 1fr auto;
    row-gap: 6px;
  }
  .tier-bar-track { grid-column: 1 / -1; grid-row: 2; }
  .tier-bar-value { grid-column: 2; grid-row: 1; }
}
