/*
 * One stylesheet, hand-written, no build step (PLAN.md "UI"). Everything that
 * differs between the two themes is a custom property on :root — nothing below
 * the theme blocks should contain a raw colour, because scattered hex is what
 * made this file impossible to theme the first time.
 *
 * Theme resolution order, and all three matter:
 *   1. :root                                  → light, the default
 *   2. @media (prefers-color-scheme: dark)    → follows the OS when unset
 *   3. :root[data-theme="light"|"dark"]       → an explicit choice, and it must
 *      win in BOTH directions, which is why (3) repeats the full palette rather
 *      than only the dark one.
 * The inline script in <head> stamps data-theme before first paint.
 */

:root {
  color-scheme: light;

  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-sunken: #f7f8fa;
  --ink: #1b1d21;
  --ink-soft: #3f444c;
  --muted: #6b7280;
  --line: #e3e5ea;
  --line-strong: #cfd3da;

  --accent: #2f5fd0;
  --accent-ink: #ffffff;
  --accent-soft: #eef2fd;

  --ok: #17663d;
  --ok-line: #b7e0c6;
  --ok-bg: #eefaf2;
  --warn: #8a5a08;
  --warn-line: #f0d79a;
  --warn-bg: #fdf7e9;
  --bad: #a92626;
  --bad-line: #f2c1c1;
  --bad-bg: #fdf1f1;

  --focus: #2f5fd0;
  --shadow: 0 1px 2px rgba(16, 20, 28, 0.05), 0 1px 3px rgba(16, 20, 28, 0.04);

  --radius: 10px;
  --radius-sm: 6px;

  /*
   * Page width. ⚠ These three are the only numbers that decide how much of a
   * wide screen the app uses — change them here, not in a media query.
   *
   * The owner, 2026-08-21: *"on desktop we can utilize more of the horizontal
   * space… but dont make it use 100% of the screen either"*. Until then `main`
   * was 780px at every width, so a 2560px display showed 780px of app between
   * 890px of empty page on each side (measured, not guessed).
   *
   * `--measure` is what keeps that from being a downgrade: the SHELL widens,
   * but prose and form controls stay at a width you can still read and aim at.
   * A 1400px-wide text input is worse than a 640px one, not better. Tables opt
   * out of the measure and use the whole shell — they are the reason for it.
   */
  --page: 1400px;
  --page-wide: 1600px;
  --measure: 640px;

  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;

  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --bg: #101216;
    --surface: #191c21;
    --surface-sunken: #14171b;
    --ink: #e4e7ec;
    --ink-soft: #c2c7d0;
    --muted: #8f96a3;
    --line: #2a2f37;
    --line-strong: #3a414b;

    --accent: #7ea6ff;
    --accent-ink: #0f1319;
    --accent-soft: #1b2740;

    --ok: #6dd39b;
    --ok-line: #2a4d3a;
    --ok-bg: #14251c;
    --warn: #e5be6a;
    --warn-line: #4d4023;
    --warn-bg: #26200f;
    --bad: #f08b8b;
    --bad-line: #552b2b;
    --bad-bg: #2a1717;

    --focus: #7ea6ff;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
  }
}

/* An explicit choice beats the media query both ways. */
:root[data-theme='light'] {
  color-scheme: light;

  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-sunken: #f7f8fa;
  --ink: #1b1d21;
  --ink-soft: #3f444c;
  --muted: #6b7280;
  --line: #e3e5ea;
  --line-strong: #cfd3da;

  --accent: #2f5fd0;
  --accent-ink: #ffffff;
  --accent-soft: #eef2fd;

  --ok: #17663d;
  --ok-line: #b7e0c6;
  --ok-bg: #eefaf2;
  --warn: #8a5a08;
  --warn-line: #f0d79a;
  --warn-bg: #fdf7e9;
  --bad: #a92626;
  --bad-line: #f2c1c1;
  --bad-bg: #fdf1f1;

  --focus: #2f5fd0;
  --shadow: 0 1px 2px rgba(16, 20, 28, 0.05), 0 1px 3px rgba(16, 20, 28, 0.04);
}

:root[data-theme='dark'] {
  color-scheme: dark;

  --bg: #101216;
  --surface: #191c21;
  --surface-sunken: #14171b;
  --ink: #e4e7ec;
  --ink-soft: #c2c7d0;
  --muted: #8f96a3;
  --line: #2a2f37;
  --line-strong: #3a414b;

  --accent: #7ea6ff;
  --accent-ink: #0f1319;
  --accent-soft: #1b2740;

  --ok: #6dd39b;
  --ok-line: #2a4d3a;
  --ok-bg: #14251c;
  --warn: #e5be6a;
  --warn-line: #4d4023;
  --warn-bg: #26200f;
  --bad: #f08b8b;
  --bad-line: #552b2b;
  --bad-bg: #2a1717;

  --focus: #7ea6ff;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
}

* {
  box-sizing: border-box;
}

/* Any author `display` rule beats the UA stylesheet's `[hidden] { display: none }`,
   so a hidden flex row (the CSV mapping's attribute-name box) stays visible
   without this. Found in Chrome, 2026-08-05. DO NOT REMOVE. */
[hidden] {
  display: none !important;
}

html {
  background: var(--bg);
  /*
   * ⚠ The cost of a sticky bar: an anchor jump lands its target UNDER the bar.
   * /docs/api is rendered markdown with in-page links ("see Permissions"), so
   * this is a real path rather than a precaution. Generous enough for the bar
   * at its two-row height on a narrow phone.
   */
  scroll-padding-top: 5.5rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* Present to a screen reader, absent to everything else. Not `display: none`
   and not `hidden`, either of which removes it from the accessibility tree too
   — which would defeat the whole point. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * The setup checklist. Its state comes from the data, so a tick is a fact
 * rather than a stored flag — see src/routes/dashboard.tsx.
 *
 * The done/todo marks are the same 24px box so the two rows line up; only the
 * glyph inside and the colour differ. A ragged left edge would make a
 * half-finished list look broken rather than half-finished.
 */
.steps {
  list-style: none;
  margin: var(--s3) 0;
  padding: 0;
}

.step {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s2) 0;
}

.step-mark {
  width: 20px;
  height: 20px;
  flex: none;
  /* Optical alignment with the first line of text, not the box. */
  margin-top: 2px;
  color: var(--ok);
}

.step-mark-todo {
  color: var(--line-strong);
}

/* The text stays full strength when done — this is a map of the parts, not a
   crossed-out shopping list, and a completed step is still a link people use. */
.step-done > span:last-child {
  color: var(--ink-soft);
}

/* ----------------------------------------------------------------- focus */

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

/* Only suppress the ring for pointer users; keyboard focus always shows one. */
:focus:not(:focus-visible) {
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------- topbar */

/*
 * ⚠ STICKY, AT EVERY WIDTH. Asked for on mobile (owner, 2026-09-19), where
 * scrolling a long campaign or contact list left no way to navigate without
 * scrolling all the way back up — and in the installed PWA there is no browser
 * chrome above it to fall back on. It is the same argument as the back bar on
 * the preview shell: the app's one navigation surface should not be something
 * you have to go and find.
 *
 * Applied at all widths rather than behind a mobile media query. A top bar that
 * sticks on a phone and scrolls away on a laptop is two behaviours to hold in
 * your head, and the bar costs ~56px of a viewport that has plenty.
 *
 * ⚠ `position: sticky` DIES SILENTLY IF ANY ANCESTOR SCROLLS — one
 * `overflow: hidden` on `html`, `body` or a wrapper and this becomes an
 * ordinary static bar with no error anywhere. Checked before relying on it:
 * `html` and `body` set only background/colour/font, and `main` sets width and
 * margin. `.tablewrap`'s overflow is a DESCENDANT of the bar's siblings, not an
 * ancestor of the bar. Anything that later adds overflow to a wrapper around
 * `.topbar` breaks this, and the only symptom will be the bar scrolling away.
 *
 * `z-index: 50` clears `.navmore-menu`'s 40. The menu lives INSIDE the bar, so
 * it stacks in the bar's context and rides above page content with it; the
 * number only has to beat anything that might later sit in the page.
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s5);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.topbar nav {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-left: auto;
}

/*
 * ⚠ `inline-flex`, and it is not cosmetic. The nav glyphs became visible on the
 * bar on 2026-08-19 and `.navicon` is `display: block`; inside a `display: block`
 * anchor that puts the icon on its own line and drops the label underneath it.
 * The owner saw exactly that — four links stacked at 54px tall while the three
 * that happened to carry another `display: flex` rule sat inline at 37px, so the
 * text baseline marched up and down the row.
 *
 * The drawer had always set `display: flex` on these, which is why the bug only
 * appeared above 1040px, and why turning the icons on looked fine in the one
 * place it was checked first.
 */
.topbar nav a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 15px;
  padding: 6px 2px;
  border-bottom: 2px solid transparent;
}

.topbar nav a:hover {
  color: var(--ink);
  border-bottom-color: var(--line-strong);
}

/* Now a link into /settings/security, so it has to out-specify `.topbar nav a`
   to keep reading as a quiet label rather than a ninth nav item. */
.topbar nav a.who {
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
  border-bottom-color: transparent;
}

.topbar nav a.who:hover {
  color: var(--ink-soft);
  border-bottom-color: var(--line-strong);
}

/* The superadmin entry, beside the address and in the same quiet register — a
   destination, not a status, so it reads a shade stronger than the label it
   sits next to. Only a session carrying `users.is_admin` renders it at all, so
   the ~50px it costs the nav row is paid by exactly one browser. */
.topbar nav a.who-admin {
  color: var(--ink-soft);
  font-weight: 600;
}

/*
 * Where you are.
 *
 * Placed after `.who` and `.who-admin` on purpose: those two reset
 * `border-bottom-color` to transparent at equal specificity, so a rule sitting
 * above them would light the eight nav links and silently fail on the two that
 * are not nav links. The 2px border every link already reserves is what gets
 * painted, so this costs no layout at all — nothing moves, and the width budget
 * the bands above ration so carefully is untouched.
 */
.topbar nav a[aria-current='page'] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--ink);
  white-space: nowrap;
}

/*
 * The wordmark and the deployed commit, stacked.
 *
 * ⚠ The two lines together are held UNDER the 26px icon beside them (17 + 9,
 * both at line-height 1), so the bar is exactly as tall as it was. The nav row
 * has twice been broken here by a handful of unmeasured pixels — see the note
 * on `.brand`.
 */
/*
 * A glyph sitting on a line of text — sized to the text it accompanies rather
 * than to a fixed pixel count, so it tracks the `.hint` it lives in.
 */
.inlineicon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-right: 0.35em;
  flex: none;
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1;
}

.brand-sha {
  margin-top: 2px;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--muted);
}

/* The source is a finished app icon — its own rounded-square shape, its own
   gradient, transparent outside the corners. So nothing here masks, recolours
   or frames it; it only gets a size and a refusal to be squashed by the flex
   row it sits in. */
.brand img {
  width: auto;
  height: 26px;
  flex: none;
}

/* The Currawong bird: the plain one on light, the white-bordered one on dark
   (the dark teal bird disappears on a dark bar). Same theme rules as the colour
   tokens: the system setting unless the page has chosen a theme. */
.brand-mark-dark {
  display: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .brand-mark-light {
    display: none;
  }
  :root:not([data-theme='light']) .brand-mark-dark {
    display: block;
  }
}
:root[data-theme='dark'] .brand-mark-light {
  display: none;
}
:root[data-theme='dark'] .brand-mark-dark {
  display: block;
}

.brand-name {
  display: flex;
  align-items: baseline;
  gap: 5px;
}

.brand-product {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

/*
 * The organisation chip: which organisation this session is in, said in the bar
 * rather than inside the drawer.
 *
 * ⚠ SMALL SCREENS ONLY. Above the drawer breakpoint the org switcher is already
 * in the row, and two copies of the same name 900px apart is not more
 * information. See the comment on the markup in src/ui/layout.tsx.
 */
.orgchip {
  display: none;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}

/* ⚠ 26px: the SAME size as the mark beside it (`.brand img`), on the owner's
   instruction 2026-09-20 — "makke the org icon the same size as the cf-email
   icon in the header". Two icons a few pixels apart in the same row read as a
   rendering fault rather than as a hierarchy, and when the wordmark drops for a
   long name these two ARE the header. If one moves, move both. */
.orgchip .siteicon {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 5px;
}

/* ⚠ The one element in the bar allowed to shrink: a 40-character organisation
   name must cost the hamburger nothing. `min-width: 0` above is what lets the
   ellipsis happen inside a flex row at all. */
.orgchip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Eight nav links plus the tenant line plus Log out is a lot for one row: with
   nothing stopping them, flex shrinks the two shortest items and "cf-email" and
   "Log out" each break across two lines. */
.topbar nav a,
.topbar nav button.link {
  white-space: nowrap;
}

/*
 * Between the hamburger breakpoint and a comfortable desktop, the tenant/email
 * line is what gives. It is the least load-bearing thing in the bar — the
 * dashboard names the tenant in a heading, and the mobile drawer still shows it
 * (this rule is scoped above the drawer's breakpoint so it does not reach it).
 */
@media (min-width: 1041px) and (max-width: 1150px) {
  /*
   * The tenant name goes; the address stays, because the address is now the
   * link to /settings/security and hiding the whole thing left that screen
   * unreachable in a 110px-wide band. The name is the part that can go — the
   * dashboard carries it in a heading and the mobile drawer still shows it.
   */
  .topbar nav a.who .who-tenant {
    display: none;
  }
}

/*
 * The brand mark is the second thing that gives, and for the same reason: it
 * costs the row 34px (26px + the gap) that this band does not have. Measured
 * 2026-08-07, minimum width before the bar overflows:
 *
 *                    no mark   with mark
 *   name hidden        1017      1051     ← 1051 > 1041, so it cannot stay
 *   name shown         1135      1169     ← 1169 > 1151, so it cannot come back
 *                                            until 1181
 *
 * Below 1041 the bar has already wrapped to the drawer layout, where the brand
 * sits alone on the top row with room to spare — so the mark is visible there,
 * hidden through the middle, and visible again once a desktop is wide enough to
 * carry it. Same rule as the tenant name above, applied to the other end of the
 * bar.
 */
@media (min-width: 1041px) and (max-width: 1180px) {
  .brand img {
    display: none !important;
  }
}

/*
 * The ninth item, and who pays for it.
 *
 * Measured in Chrome on 2026-08-07 with a 16-character tenant name and an
 * 18-character address, as the minimum width the bar needs before it overflows
 * and takes the page sideways:
 *
 *              name hidden   name shown
 *   no Admin       1017         1135     ← what the 1040/1150 breakpoints are set by
 *   Admin          1077         1195
 *
 * So an admin session breaks BOTH existing bands: 1041–1076 and 1151–1194. It
 * is also the only session that renders the link at all, so it is the only one
 * that gives anything up for it — hence `body.is-admin` rather than a global
 * breakpoint move that would put ordinary tenants behind a hamburger 40px
 * early. Two concessions, both inside widths this bar was already rationing:
 * the tenant name stays hidden further up, and the row tightens by 4px a gap.
 * That brings the requirement to 1037, below the 1041 the drawer takes over at.
 *
 * The upper bound is 1240 rather than 1150 because of the mark: an admin bar
 * carrying both the mark and the tenant name needs 1229, so the name cannot
 * come back until 1241. Every combination was measured; the four this file
 * rations are the tenant name, the mark, the nav gap and the drawer.
 */
@media (min-width: 1041px) and (max-width: 1240px) {
  body.is-admin .topbar nav {
    column-gap: var(--s3);
  }

  body.is-admin .topbar nav a.who .who-tenant {
    display: none;
  }
}

/*
 * The middle tier: wide enough for the bar, not wide enough for all nine links.
 *
 * Measured 2026-08-15 (binary search on `scrollWidth > clientWidth`, same
 * method as the bands above): the full nine-link bar needs 1250px in its worst
 * case (admin + tenant name), so it cannot come inline before 1260 — set as
 * low as the measurement allows because the owner's own maximised window with
 * the browser side panel open is 1272px, and "full screen" must mean the full
 * bar. With
 * Suppressions, Brand and API keys folded into "More ▾" the bar needs at most
 * 1059px in any state this band produces — comfortably inside 1041 even before
 * the tenant-name/brand-mark/is-admin rules above take their own cuts.
 */
@media (min-width: 1041px) {
  /*
   * ⚠ WAS `(min-width: 1041px) and (max-width: 1259px)` until 2026-08-19: these
   * three came back inline at 1260 and up. They no longer do, and the owner
   * approved the change with the measurement in hand.
   *
   * The 2026-08-15 decision to put all nine inline was made because API keys
   * was genuinely UNREACHABLE otherwise — it existed only in the mobile drawer,
   * so a desktop window offered no way to it at all. The all-pages menu removed
   * that reason: Suppressions, Brand and API keys are one click away at every
   * width now, grouped with everything else.
   *
   * What it buys, measured (documented method, corrected harness — see the band
   * comment further down for the two ways the old one lied): an admin bar
   * needed **1312px** with nine inline and needs **1272px** with six, so the
   * horizontal page scroll that ran from 1241 to ~1339px is gone at every
   * width. 1272 is the owner's own maximised window with the browser side
   * panel open, which is where it mattered.
   */
  .topbar nav.mainnav > a.nav-overflow {
    display: none;
  }

  /*
   * ⚠ …UNLESS app.js is driving, which is the normal case. `js-overflow` says
   * the row is being MEASURED rather than guessed at, so every link comes back
   * and only the ones that genuinely do not fit are moved into the menu.
   *
   * The direction is the point: no JS gets the CONSERVATIVE bar (three links
   * hidden, definitely fits), and JS widens it. The other way round would leave
   * a scripts-off session with a wrapped, broken row.
   */
  .topbar nav.mainnav.js-overflow > a.nav-overflow {
    display: inline-flex;
  }

  /* `.topbar .navmore` rather than `.navmore`: the component's own
     `display: none` default sits LATER in this file, so an equal-specificity
     rule here loses to it whatever the media query says. */
  .topbar .navmore.allpages {
    display: block;
  }
}

/* The hamburger and the theme control. Both are chrome, not content, so they
   are quiet until hovered or focused. */
.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  padding: 0 var(--s2);
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.iconbtn:hover {
  color: var(--ink);
  background: var(--surface-sunken);
  border-color: var(--line);
}

.iconbtn svg {
  width: 18px;
  height: 18px;
  display: block;
}

.navtoggle {
  display: none;
}

a {
  color: var(--accent);
}

/*
 * ⚠ SHRINK TO FIT, THEN CENTRE. The owner, looking at the first version of this:
 * *"is it possible to make them just as wide as they need to be to avoid
 * unnecessary word wrapping, and still remain centered? … the home page just has
 * everything over on the left hand side"*. Exactly right — a card stretched to
 * 1400px with 640px of content in it is not "using the width", it is a box with
 * a hole in it.
 *
 * `width: fit-content` makes the column as wide as its widest content wants to
 * be — a table at its natural column widths, a form at the measure — and no
 * wider, and `margin: auto` then centres it. The three bounds:
 *   min-width  never narrower than a form's measure (or the viewport, below it)
 *   max-width  never wider than --page, so a huge table scrolls in its own box
 *              rather than stretching the page
 *   the caps below cap PROSE, so a paragraph still wraps at a readable length
 *              even when the table beside it makes the column wide
 */
main {
  width: fit-content;
  min-width: min(100%, calc(var(--measure) + 2 * var(--s5)));
  max-width: min(var(--page), 100%);
  margin: var(--s5) auto var(--s6);
  padding: 0 var(--s5);
}

/*
 * The two screens that are not documents: /admin (by PATH, in
 * src/ui/layout.tsx — not by the admin flag) and the visual editor (by prop).
 * The first is a seven-column cross-tenant table, the second a three-panel
 * canvas; both were already asking for more than the reading width when the
 * rest of the app was capped at 780px, and both still want more than `--page`.
 *
 * ⚠ Still a number rather than `none`. An unbounded table on a 2560px monitor
 * stretches its hint text into lines too long to track across, which is the
 * failure mode at the other end. Measure with scrollWidth vs clientWidth on
 * `.tablewrap` (or on the DOCUMENT for page overflow) — do not eyeball it.
 */
body.wide main {
  /* `auto`, not `fit-content`: a canvas and a cross-tenant table are asked to
     FILL what they are given rather than to state a natural width. */
  width: auto;
  max-width: var(--page-wide);
}

/*
 * ⚠ THE COMPANION RULE TO THE WIDE SHELL — the two only make sense together.
 *
 * `main` is now up to 1400px, which is right for a table and wrong for
 * everything you read or type into. A line of body text 1400px long is hard to
 * track back to the start of; a text input 1400px wide is a worse target than a
 * 640px one, not a better one. So the SHELL widens and the CONTENT does not:
 * paragraphs, hints, definition lists and form controls stay at `--measure`,
 * and the things that genuinely want the room — tables, code blocks, the
 * editor panes, the filter bar, the pager under a table — opt out below.
 *
 * The measure is a max, never a width: at 700px everything is still 100%.
 */
main p,
main .hint,
main .done,
main .copyfield,
main dl,
main input,
main select,
main textarea {
  max-width: var(--measure);
}

/*
 * The opt-outs, each for a reason:
 *   .pager      sits under a table and its "Next →" belongs at the table's edge
 *   .filters    a row of controls, not a column of them
 *   .bulkbar    same
 *   .grid *     a cell is as wide as its column, never 640px
 *   textarea.code / .gjs-*  the editors ARE the page at that point
 *   .codeblock  code wraps badly; it scrolls in its own box instead
 */
main .pager,
main .filters,
main .filters input,
main .filters select,
main .bulkbar,
main .bulkbar select,
main .grid p,
main .grid .hint,
main .grid dl,
main .grid ul,
main .grid input,
main .grid select,
main textarea.code,
main .gjs-pane,
main .gjs-host,
main .codeblock,
main .layoutpick,
main .stats {
  max-width: none;
}

/*
 * Two cards that answer one question, side by side once there is room for both.
 *
 * The owner, on the message page: *"on a wide desktop the 'The message that was
 * sent' box could fit up by the first box"* — right, and the same is true of any
 * pair where neither half wants more than the measure. Below 1100px they stack,
 * which is also what a phone gets.
 *
 * ⚠ `minmax(0, 1fr)`, not `1fr`. A grid track's default minimum is `auto`, so a
 * long unbroken value inside — a provider id, an address — would push the column
 * wider than its share instead of wrapping, and the page would scroll sideways.
 * `align-items: start` keeps the shorter card its own height rather than
 * stretching an empty box down to match the taller one.
 */
.split {
  display: grid;
  gap: var(--s4);
  align-items: start;
}

@media (min-width: 1100px) {
  .split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

/* Stacked, the second card needs the usual gap; side by side, `gap` supplies it. */
.split > .card + .card {
  margin-top: 0;
}

@media (max-width: 1099px) {
  .split > .card + .card {
    margin-top: var(--s4);
  }
}

/* The unsubscribe pages have no app chrome: a recipient is not a tenant, and
   showing them a navigation bar into someone else's account would be wrong. */
body.standalone main {
  max-width: 520px;
  margin-top: 64px;
}

/* --------------------------------------------- the pages a subscriber sees
 *
 * The signup-confirmation and unsubscribe pages. See src/ui/public-page.tsx
 * for why they are branded at all: an unbranded confirmation page reads as a
 * phishing page.
 *
 * ⚠ The masthead sits ABOVE `main`, so its own margin has to replace main's
 * 64px rather than add to it, or the card lands half a screen down.
 */
body.standalone:has(.publicmast) main {
  margin-top: var(--s5);
}

.publicmast {
  max-width: 520px;
  margin: 48px auto 0;
  padding: 0 var(--s4);
  text-align: center;
}

.publicmast-logo {
  max-width: 220px;
  max-height: 72px;
  width: auto;
  height: auto;
}

/*
 * A dark-line-art logo on a dark page, flipped to light.
 *
 * ⚠ THE CLASS IS OPT-IN PER TENANT (tenant_brands.logo_invert_dark). Inverting
 * every logo would turn a colour wordmark into a photographic negative — blue
 * comes out orange — which is worse than an invisible logo and wrong on
 * somebody else's brand rather than ours.
 *
 * ⚠ BOTH DARK ROUTES, and they are not the same thing. The system-default case
 * is `prefers-color-scheme: dark` with no attribute stamped; an explicit choice
 * stamps `data-theme="dark"`. A rule written for only one of them looks correct
 * on the developer's machine and fails on half of everyone else's — the header
 * of this file spells the three states out, and the `:not([data-theme="light"])`
 * guard is what stops a reader who has explicitly chosen light from getting an
 * inverted logo because their OS says dark.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .publicmast-logo.invertible {
    filter: invert(1);
  }
}

:root[data-theme='dark'] .publicmast-logo.invertible {
  filter: invert(1);
}

.publicmast-name {
  margin: 0;
  font-size: 1.4rem;
  font-weight: 700;
}

.publicmast-back {
  margin: var(--s5) 0 0;
  text-align: center;
}

.publicfoot {
  max-width: 520px;
  margin: var(--s5) auto 48px;
  text-align: center;
}

.publicfoot a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 0.8rem;
  text-decoration: none;
}

.publicfoot a:hover {
  text-decoration: underline;
}

.publicfoot img {
  opacity: 0.75;
}

/* ----------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s5);
  box-shadow: var(--shadow);
}

.card + .card {
  margin-top: var(--s4);
}

h1 {
  margin-top: 0;
  font-size: 25px;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

h2 {
  font-size: 18px;
  letter-spacing: -0.01em;
}

h3 {
  font-size: 15px;
}

/* ----------------------------------------------------------------- forms */

label {
  display: block;
  margin: 0 0 var(--s4);
  font-weight: 600;
  font-size: 15px;
}

input,
textarea,
select {
  display: block;
  width: 100%;
  margin-top: var(--s1);
  padding: 10px 12px;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
}

input:hover,
textarea:hover,
select:hover {
  border-color: var(--muted);
}

::placeholder {
  color: var(--muted);
  opacity: 1;
}

.hint {
  display: block;
  margin-top: var(--s1);
  font-weight: 400;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

button {
  min-height: 40px;
  padding: 9px 16px;
  font: inherit;
  font-weight: 600;
  font-size: 15px;
  color: var(--accent-ink);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

button:hover {
  filter: brightness(1.08);
}

/*
 * A submit that is waiting on the form. `required` alone blocks the SUBMIT but
 * leaves the button looking live, which reads as a broken button rather than an
 * unfinished form — reported on the webhook form, where the button invited a
 * click the browser then silently refused.
 */
button:disabled,
button:disabled:hover {
  color: var(--muted);
  background: var(--surface-sunken);
  border-color: var(--line);
  cursor: not-allowed;
  filter: none;
}

/* A link that starts an action, styled as the button it behaves like — the
   "New campaign" affordance is a navigation, not a form submit. */
a.button {
  display: inline-block;
  min-height: 40px;
  padding: 9px 16px;
  font-weight: 600;
  font-size: 15px;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: var(--radius-sm);
  text-decoration: none;
}

button.secondary {
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
}

button.secondary:hover {
  background: var(--surface-sunken);
  filter: none;
}

button.link {
  min-height: 0;
  padding: 0;
  color: var(--accent);
  background: none;
  border: 0;
  font-weight: 400;
  font-size: inherit;
  text-decoration: underline;
}

button.link:hover {
  filter: none;
}

.muted {
  color: var(--muted);
}

/* ---------------------------------------------------------------- flashes */

.flash {
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-sunken);
  color: var(--ink-soft);
  font-size: 15px;
}

.flash p {
  margin: 0;
}

.flash p + p {
  margin-top: var(--s2);
}

.flash-error {
  color: var(--bad);
  border-color: var(--bad-line);
  background: var(--bad-bg);
}

.flash-ok {
  color: var(--ok);
  border-color: var(--ok-line);
  background: var(--ok-bg);
}

/* A caution the user should read before proceeding — not an error, so it must
   not shout in red next to one. First used for Brevo's tracking warning. */
.flash-warn {
  color: var(--warn);
  border-color: var(--warn-line);
  background: var(--warn-bg);
}

/* ------------------------------------------------------------------ done */

/*
 * "There is nothing left for you to do here." The two screens that needed this
 * (webhook, domain) each had a correct-but-buried status pill and an open form
 * underneath, which reads as unfinished work however the label is worded.
 */
.done {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  padding: var(--s4);
  border: 1px solid var(--ok-line);
  border-radius: var(--radius-sm);
  background: var(--ok-bg);
  color: var(--ok);
}

.done svg {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;
}

.done p {
  margin: 0;
  color: var(--ok);
}

.done p + p {
  margin-top: var(--s1);
}

.done strong {
  display: block;
  font-size: 16px;
}

/* ----------------------------------------------------------------- stats */

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4) var(--s6);
  margin: 0 0 var(--s5);
}

.stats dt {
  font-size: 13px;
  color: var(--muted);
}

.stats dd {
  margin: 2px 0 0;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* --------------------------------------------- tiles, records, pills ---- */

.tiles,
.records,
.rows-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.tile {
  padding: var(--s4) 0;
  border-top: 1px solid var(--line);
}

.tile h2 {
  margin: 0;
}

.tile-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}

.tile-head h1,
.tile-head h2,
.tile-head h3 {
  margin: 0;
}

.tile p {
  margin: 6px 0 0;
}

.pill {
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid var(--line);
  background: var(--surface-sunken);
  color: var(--muted);
}

.pill-ok {
  color: var(--ok);
  border-color: var(--ok-line);
  background: var(--ok-bg);
}

.pill-warn {
  color: var(--warn);
  border-color: var(--warn-line);
  background: var(--warn-bg);
}

.pill-bad {
  color: var(--bad);
  border-color: var(--bad-line);
  background: var(--bad-bg);
}

/*
 * A pill that becomes its glyph alone on a phone (`compact` on `StatusPill`).
 *
 * Both the icon and the word ship in the markup and CSS chooses — the same
 * swap the copy button uses. Nothing measures anything at runtime, and the word
 * never leaves the accessibility tree: below 700px it is visually hidden
 * rather than `display: none`, and `title=` puts it back on hover.
 */
.pill-compact {
  /*
   * ⚠ `relative` is load-bearing, and the bug it fixes is invisible. The label
   * below is `position: absolute` when hidden; without a positioned ancestor
   * its containing block is the page itself, so the surrounding `.tablewrap`
   * cannot clip it — a 1px box sitting at x=713 inside a horizontally scrolled
   * table then made the whole DOCUMENT 719px wide at a 390px viewport, with
   * nothing visible out there to explain it. Measured, not theorised.
   */
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.pill-glyph {
  display: none;
  flex: none;
  width: 15px;
  height: 15px;
}

@media (max-width: 700px) {
  .pill-compact {
    padding: 5px;
  }

  .pill-compact .pill-glyph {
    display: block;
  }

  /* Same properties as `.visually-hidden` — present to a screen reader, absent
     to everything else. Not `display: none`, which would take the word out of
     the accessibility tree and leave a tick meaning nothing. */
  .pill-compact .pill-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/*
 * Columns a phone does not have room for. The send log keeps who it went to,
 * what it said and whether it worked; which application sent it is a desktop
 * question, and on a phone it was pushing State off the screen entirely — the
 * page scrolled sideways by 331px at 390px wide (measured 2026-08-21).
 */
@media (max-width: 700px) {
  .grid .col-desktop {
    display: none;
  }

  /*
   * The same 700px cut, for text INSIDE a kept cell rather than a whole column.
   * "Sends from" holds two favicons and two names; on a phone the names are
   * what has to go, because the icons carry the same two facts in a fifth of
   * the width. The wrapper keeps its `title`, so the words are still reachable.
   */
  .grid .col-inline-desktop {
    display: none;
  }

  /* Nothing left but icons — the gap that separated words now separates them. */
  .sendsfrom {
    gap: var(--s2);
  }
}

/*
 * A site's favicon beside its name, inline in a table cell.
 *
 * ⚠ 18px, not the 32px `.siteicon` renders at elsewhere. This sits on a line of
 * body text rather than heading a row of its own, and at 32 it set the row
 * height and pushed the table taller than every other table in the app.
 */
.sendsfrom {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.sendsfrom .siteicon {
  width: 18px;
  height: 18px;
}

/*
 * A favicon beside the name of the site it belongs to.
 *
 * The box is reserved at its full size whether or not an icon arrives, so a row
 * does not reflow when a lazy image lands and a site with no favicon still
 * lines up with its neighbours. `object-fit: contain` because these are
 * somebody else's images: they arrive square, oblong and occasionally enormous.
 *
 * 32px, not the 16 this started at. Most sites now serve an apple-touch icon at
 * 180px, which the probe prefers over `/favicon.ico` precisely so there is
 * resolution to spend here — a site that only has a 16px .ico will look soft at
 * this size, and that is the honest signal that it has nothing better.
 */
.withicon {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
}

.siteicon {
  width: 32px;
  height: 32px;
  flex: none;
  object-fit: contain;
}

/*
 * Nav glyphs — now on EVERYWHERE, and the history matters.
 *
 * They were drawer-only from 2026-08-06, for a good reason at the time: the bar
 * carried eight or nine links and had no spare pixels, and eight icons at 17px
 * plus their gaps is ~180px it did not have.
 *
 * What changed on 2026-08-19 was not the arithmetic but the link count. The
 * all-pages menu made Suppressions, Brand and API keys reachable from every
 * screen, so they left the bar; six links plus icons is narrower than nine
 * without them. **Swept every width from 1041 to 1500px, admin and non-admin:
 * no horizontal scroll anywhere** — which is strictly better than before the
 * icons existed, when an admin bar scrolled from 1241 to ~1339px.
 *
 * ⚠ So the constraint that produced the old rule is real and unchanged: the bar
 * has no slack to give away. Adding a SEVENTH inline link would spend what the
 * icons are now using. Re-measure before adding one.
 */
.navicon {
  display: block;
  width: 17px;
  height: 17px;
  flex: none;
}

/*
 * The "More ▾" dropdown — the middle tier of the three-tier nav (see the NAV
 * comment in src/ui/layout.tsx for the 2026-08-15 measurements). Hidden by
 * default: below 1041px the drawer shows the overflow links stacked, and from
 * 1280px they sit inline on the bar. Only the 1041–1279px media block below
 * turns this on.
 *
 * A <details>, so it opens and closes with scripts off; app.js adds
 * close-on-outside-click as polish, not as function.
 */
.navmore {
  display: none;
  position: relative;
}

.navmore summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--ink-soft);
  font-size: 15px;
  padding: 6px 2px;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  -webkit-user-select: none;
}

.navmore summary::-webkit-details-marker {
  display: none;
}

.navmore summary:hover {
  color: var(--ink);
  border-bottom-color: var(--line-strong);
}

/* The underline the inline link would have carried: when the current page is
   one of the collapsed three, the summary is what says so. */
.navmore summary.is-here {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/*
 * The organisation switcher.
 *
 * It borrows `.navmore`'s geometry — same summary, same chevron, same dropdown
 * panel — because the nav row has no spare width and a second dropdown style
 * would be both more CSS and more pixels. What it does NOT borrow is
 * `.navmore { display: none }`: the More menu is band-scoped and this is not,
 * so it needs an override at higher specificity rather than a separate rule
 * further down the file (see the `.topbar .navmore` note above — a media query
 * does not beat later-in-file equal specificity, and that trap cost a deploy).
 *
 * ⚠ It renders for EVERY signed-in session, including single-organisation ones.
 * That is a correction (2026-08-17): it first rendered only above one
 * organisation, and since the only links to /orgs and /orgs/people live inside
 * it, that left both screens reachable only by typing a URL. See the comment at
 * the `<details class="navmore orgswitch">` in src/ui/layout.tsx. Every session
 * therefore pays the ~8px measured below — the right trade against a feature
 * nobody can find.
 *
 * It reads in the quiet register of `.who` beside it, not as a nav link: it
 * names where you are, and it is only secondarily somewhere to go.
 */
.navmore.orgswitch {
  display: block;
}

/*
 * ⚠ WIDTH. Measured headlessly 2026-08-17 by the documented method
 * (binary-search `scrollWidth > clientWidth` on `.mainnav`), with the SAME
 * organisation name rendered both ways so only the structure differed.
 *
 * The naive version cost **+27px**: the nav's 16px `gap` for a new flex item,
 * plus a 14px chevron and its 4px gap, less the ~7px of the " · " the `who`
 * link no longer renders. That is worth keeping down to as little as possible on
 * a row that is already the tightest thing in this app.
 *
 * So the switcher is pulled tight against the address it replaces the first
 * half of: they were one unit before this ("Org · email") and they stay one
 * unit. Negative margin cancels the nav gap down to 4px, and the chevron is
 * 10px with a 1px gap. Re-measured after each attempt: the first tightening
 * still cost +13px; this one costs **+8px**.
 *
 * ⚠ An earlier version of this comment claimed +8px was "inside the headroom" of
 * a documented 1250px worst case against a 1260px cutoff. **Both halves of that
 * were wrong**, and the correction is in the band comment further down: the
 * measuring method could not detect overflow at all, the harness ignored the
 * viewport it was given, and the real admin bar needs ~1315px. The +8px delta
 * survived re-measurement; the reassurance did not.
 *
 * ⚠ Anything that widens this — a bigger chevron, an avatar, a second line —
 * must be re-measured the same way. Do not eyeball it.
 */
.topbar .orgswitch {
  margin-right: calc(4px - var(--s4));
}

.topbar .orgswitch summary {
  color: var(--muted);
  font-size: 13px;
  gap: 1px;
}

/* Somebody in ONE organisation: the same icon and name, with no chevron and
   nothing to open. Matches the summary above so the bar does not shift
   depending on how many organisations a person happens to be in. */
.topbar .orgswitch-only {
  display: flex;
  align-items: center;
  gap: 1px;
  color: var(--muted);
  font-size: 13px;
}

/*
 * The organisation's own favicon, reusing `/icons/site/:host` — the same
 * mechanism and the same three caches as /domains and /providers. `.siteicon`
 * ships at 32px for tables; the bar wants 16, and the menu rows want 16 too so
 * the summary and the list read as the same object.
 *
 * ⚠ It costs the bar ~17px (16 + the summary's 1px gap), which is why the
 * measurement below was redone with it in place. The icon is what SURVIVES in
 * the narrow bands and the name is what goes — an icon identifies an
 * organisation in 16px where its name needs ~110px, so dropping the name and
 * keeping the icon is strictly better than the reverse, which is what this
 * replaced.
 */
.topbar .orgswitch .siteicon,
.topbar .orgswitch-item .siteicon,
.topbar .orgswitch-current .siteicon {
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 3px;
}

/* Holds the 16px column open for an organisation with no verified domain, so a
   list of names does not go ragged where one icon is missing. */
.topbar .siteicon-none {
  display: inline-block;
  width: 16px;
  height: 16px;
  flex: none;
}

/* The menu rows are flex so the icon and the name sit on one baseline; the
   `.orgswitch-item`/`-current` padding is already set above. */
.topbar .orgswitch-item,
.topbar .orgswitch-current {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.topbar .orgswitch .navmore-chev {
  width: 10px;
  height: 10px;
}

.topbar .orgswitch summary:hover {
  color: var(--ink-soft);
}

/* Rows in the panel: a `<button>` inside a `<form>` has to be talked out of
   looking like a button, since switching must be a POST. Matched to
   `.topbar .navmore-menu a` so the current organisation, the switch targets and
   the two links below them read as one list rather than three controls. */
.topbar .orgswitch .navmore-menu form {
  display: block;
  margin: 0;
}

.topbar .orgswitch-item,
.topbar .orgswitch-current {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  font: inherit;
  font-size: 14px;
  color: var(--ink-soft);
  cursor: pointer;
}

.topbar .orgswitch-item:hover {
  background: var(--surface-sunken);
  color: var(--ink);
}

/* Where you are. Not a button, not clickable — switching to the organisation
   you are already in is a no-op, and offering it invites the click that proves
   nothing happened. */
.topbar .orgswitch-current {
  background: var(--surface-sunken);
  color: var(--ink);
  font-weight: 600;
  cursor: default;
}

/*
 * The same two bands that hide the tenant name in `.who` hide it here, for the
 * same width reason. The chevron stays, so the switcher is still reachable at
 * every width instead of disappearing for the people who need it most — that
 * is the /api-keys lesson (a feature reachable only by typing its URL is not
 * reachable), applied to ~40px rather than to a nav item.
 */
@media (min-width: 1041px) and (max-width: 1150px) {
  .topbar .orgswitch .who-tenant {
    display: none;
  }
}

@media (min-width: 1041px) and (max-width: 1240px) {
  body.is-admin .topbar .orgswitch .who-tenant {
    display: none;
  }
}

/*
 * ⚠ THE BAR ICON IS BAND-SCOPED, and getting these numbers right took THREE
 * corrections — two of them to the measuring method itself. Read this before
 * touching a width in here.
 *
 * **The documented method was measuring the wrong element.** PLAN.md says to
 * binary-search `scrollWidth > clientWidth` on `.mainnav`. `.mainnav` is a
 * content-sized flex item, so that comparison is ALWAYS false on it — it can
 * never report overflow. Horizontal overflow lands on the DOCUMENT. Measure
 * `document.documentElement.scrollWidth > clientWidth`, and read `.topbar`'s
 * own scrollWidth for what the row actually needs.
 *
 * **And the harness lied about the viewport.** Playwright's
 * `newContext({ viewportSize })` was silently ignored with the Chrome build
 * here: every reading came back at 1280px whatever width was asked for. Use
 * `page.setViewportSize` and ASSERT `window.innerWidth` matches before trusting
 * any number.
 *
 * What the corrected measurements say (this app, /contacts, one organisation):
 *
 *   non-admin session   the row fits at every width from 1041 up. Nothing here
 *                       is tight for an ordinary tenant.
 *   admin session       the row needs **1315px** without the icon and 1332px
 *                       with it — so the page scrolls horizontally from 1241 to
 *                       ~1339px. ⚠ THAT IS LARGELY PRE-EXISTING: before the
 *                       organisation menu existed the same bar needed ~1307px,
 *                       against a documented inline cutoff of 1260. The
 *                       "1250px worst case" in PLAN.md does not reproduce, and
 *                       was probably measured with one of the two broken
 *                       methods above.
 *
 * So the icon appears only where the row it sits in genuinely fits WITH it.
 * ⚠ RE-MEASURED 2026-08-17 after the owner asked for the icon at the brand
 * mark's size (26px, up from 16px) — those 10px moved both thresholds, which is
 * exactly why they are measured and not reasoned about:
 *
 *   ≥1300px  ordinary session. Verified: no horizontal scroll at 1300 and above.
 *   ≥1355px  admin session, whose extra Admin link makes that bar the tight one.
 *            At 26px it needs 1342px, so the old 1340px threshold OVERFLOWED BY
 *            2px — caught only by re-measuring.
 *
 * Below those widths the NAME survives instead, because the name is the thing a
 * person reads.
 *
 * ≤1240px the existing rules hide the name instead and the icon is the right
 * survivor — it identifies an organisation in 16px where its name needs ~110px,
 * and that band is where the "More ▾" tier has already collapsed three links,
 * so the row needs only ~810px and has room to spare.
 *
 * Only the SUMMARY's icon is rationed. The dropdown's rows keep theirs at every
 * width: a panel is not the row, and it has its own space.
 */
@media (min-width: 1241px) and (max-width: 1299px) {
  .topbar .orgswitch > summary .siteicon {
    display: none;
  }
}

/* The admin bar carries one more link, and is the only configuration that
   overflows at all. It waits until the row provably fits with the icon. */
@media (min-width: 1241px) and (max-width: 1354px) {
  body.is-admin .topbar .orgswitch > summary .siteicon {
    display: none;
  }
}

@media (min-width: 1151px) and (max-width: 1299px) {
  body:not(.is-admin) .topbar .orgswitch > summary .siteicon {
    display: none;
  }
}

.navmore-chev {
  width: 14px;
  height: 14px;
  flex: none;
}

.navmore[open] .navmore-chev {
  transform: rotate(180deg);
}

.navmore-menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 40;
  min-width: 190px;
  display: grid;
  gap: 2px;
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* `.topbar .navmore-menu` out-specifies `.topbar nav a…`: menu items are rows,
   not underlined bar links, so the bar's border-bottom marking must not reach
   them — current-page marking here is a filled row instead. */
.topbar .navmore-menu a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 8px 10px;
  border-radius: 6px;
  border-bottom: none;
}

.topbar .navmore-menu a:hover {
  background: var(--surface-sunken);
  color: var(--ink);
}

.topbar .navmore-menu a[aria-current='page'] {
  background: var(--surface-sunken);
  color: var(--ink);
}

.topbar .navmore-menu .navicon {
  display: block;
}

/*
 * Back, for a home-screen app with no browser chrome.
 *
 * Hidden until STANDALONE_BOOT adds `is-standalone` in the head, so it never
 * appears in an
 * ordinary tab (where the browser's own back is better) and never appears
 * without a working handler.
 */
.backbtn {
  display: none;
}

html.is-standalone .backbtn {
  display: inline-flex;
}

/*
 * The layout picker: three radio cards, each showing the layout rendered with
 * this tenant's own brand.
 *
 * The thumbnail is a 600px email in a ~210px box, so it is an iframe scaled by
 * transform rather than a resized image — the email keeps its real proportions
 * and the frame keeps its real CSS. `pointer-events: none` matters: without it
 * the frame swallows the click and the radio never gets selected, which is a
 * picker that silently does nothing.
 */
.layoutpick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin: var(--s4) 0;
  padding: 0;
  border: 0;
}

.layoutpick legend {
  padding: 0;
  margin-bottom: var(--s2);
  font-weight: 600;
  font-size: 14px;
}

/*
 * The commit row. `.layoutpick` is a wrapping flex row of cards, so without
 * `flex-basis: 100%` this becomes a fifth card squeezed in beside them. It
 * carries `.actions` too, for that class's own flex and gap; only the basis and
 * the hint's margin are new here.
 */
.layoutpick-commit {
  flex: 1 0 100%;
}

.layoutpick-commit .hint {
  margin: 0;
}

.layoutcard {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  cursor: pointer;
}

.layoutcard-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 600;
  font-size: 14px;
}

/* The sheet stretches every input to `width: 100%` for text fields, which
   turns a radio into a 152px-wide button with the label shoved off to the
   right. `label.inline` and `label.check` above opt out the same way. */
.layoutcard-head input {
  width: auto;
  margin-top: 0;
}

/* Sits between the name and the picture, and is capped to the frame's width so
   a four-card row keeps its columns instead of one long line stretching the
   flex item. */
.layoutcard-note {
  display: block;
  max-width: 210px;
  font-size: 12px;
  line-height: 17px;
  font-weight: 400;
  color: var(--muted);
}

.layoutcard-frame {
  display: block;
  width: 210px;
  height: 260px;
  overflow: hidden;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.layoutcard-frame iframe {
  width: 600px;
  height: 743px;
  border: 0;
  transform: scale(0.35);
  transform-origin: top left;
  pointer-events: none;
}

.layoutcard:has(input:checked) .layoutcard-frame {
  border-color: var(--accent);
}

.layoutcard:hover .layoutcard-frame {
  border-color: var(--line-strong);
}

@media (max-width: 700px) {
  .layoutpick {
    gap: var(--s3);
  }

  .layoutcard-note {
    max-width: 150px;
  }

  .layoutcard-frame {
    width: 150px;
    height: 190px;
  }

  .layoutcard-frame iframe {
    transform: scale(0.25);
  }
}

/*
 * The template editors: the create-form mode picker, the block composer and
 * the mode-switch forms. The composer's inputs keep the sheet-wide
 * `width: 100%` on purpose (they are text fields); only the mode radios need
 * the usual `width: auto` opt-out, which `label.check` already carries.
 */
.modepick {
  margin: var(--s4) 0;
  padding: 0;
  border: 0;
}

.socialfields {
  margin: var(--s4) 0;
  padding: 0;
  border: 0;
}

.socialfields legend,
.modepick legend {
  padding: 0;
  margin-bottom: var(--s2);
  font-weight: 600;
  font-size: 14px;
}

.modepick label.check {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s2);
}

.blockrow {
  margin: var(--s3) 0 0;
  padding: var(--s3) var(--s4) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.blockrow-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.blockrow-title {
  font-weight: 600;
  font-size: 14px;
  color: var(--ink-soft);
}

.blockrow-actions {
  display: flex;
  gap: var(--s1);
  margin-left: auto;
}

.blockrow-actions button {
  min-height: 28px;
  padding: 2px 10px;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-soft);
  background: var(--surface-sunken);
  border: 1px solid var(--line);
}

.blockrow-actions button:hover {
  filter: none;
  border-color: var(--line-strong);
}

.blockrow-actions button:disabled {
  opacity: 0.4;
  cursor: default;
}

.blockrow-actions button:disabled:hover {
  border-color: var(--line);
}

.blockadd {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s3);
}

/*
 * Blocks v2 (the `blocksEditor` island). Everything below is either drawn by
 * the island or only matters once it runs; without JavaScript the composer is
 * the plain stack above and the preview sits where the page put it.
 */
.blocksplit {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
}

@media (min-width: 1100px) {
  .blocksplit {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
  }

  /* The preview follows the composer down the page instead of sitting a
     screen below it. */
  .blocksplit-preview {
    position: sticky;
    top: var(--s4);
  }

  .blocksplit-preview iframe.preview {
    height: calc(100vh - 2 * var(--s4));
    min-height: 480px;
  }
}

/* A one-row form inside a table cell (admin AI allowances). */
.inline-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}

.inline-form input[name='cap'] {
  width: 6em;
}

/* "Design with AI" above the Blocks editor. */
.aipanel {
  margin: var(--s3) 0;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}

.aipanel summary {
  font-weight: 600;
  cursor: pointer;
}

.aipanel form {
  margin-top: var(--s3);
}

.aipanel-mode {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0 0 var(--s2);
  padding: 0;
  border: 0;
}

/* Floating Save: only while leaving would lose something. Above the phone
   home indicator, clear of the content edge. */
.savefloat {
  position: fixed;
  right: max(16px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: 50;
  padding: 12px 20px;
  border-radius: 999px;
  font-size: 16px;
  box-shadow: 0 4px 16px rgba(16, 20, 28, 0.25);
}

.savefloat[hidden] {
  display: none;
}

#blocks-composer[aria-busy='true'] {
  opacity: 0.6;
  pointer-events: none;
}

.blockrow-grip {
  cursor: grab;
  touch-action: none;
  user-select: none;
  padding: 0 var(--s1);
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
}

.blockrow.dragging {
  opacity: 0.5;
}

.blockrow.dropping {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
}

.blockdrop {
  height: 3px;
  margin: var(--s2) 0 calc(-1 * var(--s1));
  border-radius: 2px;
  background: var(--accent);
}

.blockrow-pick {
  margin: 0 0 var(--s2);
}

/* One picture of a "pictures side by side" row: a quiet box per slot, so
   three sets of address/alt/link fields read as three pictures. */
.blockslot {
  margin: 0 0 var(--s2);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.blockslot legend {
  padding: 0 var(--s1);
  font-weight: 600;
}

/* "+" between two blocks: a small round button on the gap, a menu of types
   when opened. */
.blockinsert {
  margin: var(--s1) 0 calc(-1 * var(--s2));
  text-align: center;
}

.blockinsert summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.blockinsert summary::-webkit-details-marker {
  display: none;
}

.blockinsert summary:hover,
.blockinsert[open] summary {
  border-color: var(--accent);
  color: var(--accent);
}

.blockinsert-menu {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2);
  margin: var(--s2) 0 var(--s3);
}

.blocks-status {
  font-size: 13px;
  color: var(--muted);
}

.blocks-status-ok {
  color: var(--ok);
}

.blocks-status-bad {
  color: var(--bad);
}

.hint.warn {
  color: var(--warn);
}

dialog.imagepicker {
  width: min(720px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
}

dialog.imagepicker::backdrop {
  background: rgba(16, 20, 28, 0.45);
}

.imagepicker-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.imagepicker-head strong {
  margin-right: auto;
}

.imagepicker-upload {
  cursor: pointer;
}

.imagepicker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--s2);
  margin-top: var(--s2);
}

.imagepicker-tile {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s1);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 400;
  text-align: left;
}

.imagepicker-tile:hover {
  border-color: var(--accent);
  filter: none;
}

.imagepicker-tile img {
  width: 100%;
  height: 90px;
  object-fit: contain;
  background: #fff;
}

.imagepicker-tile span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.modeswitch {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}

.modeswitch:first-of-type {
  border-top: 0;
}

/*
 * The brand logo, checked against both backgrounds it will actually land on.
 * A transparent PNG that looks right in the app and vanishes in a dark-mode
 * mail client is the failure this exists to make visible, so the two boxes are
 * fixed light and fixed dark — NOT theme tokens. Themeing them would hide the
 * exact case being tested.
 */
.logocheck {
  margin: var(--s3) 0 var(--s4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}

.logocheck-light,
.logocheck-dark {
  flex: 1 1 200px;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.logocheck-light {
  background: #ffffff;
}

.logocheck-dark {
  background: #1b1d21;
}

.logocheck img.is-inverted {
  filter: invert(1);
}

.logocheck img {
  max-width: 180px;
  max-height: 72px;
  width: auto;
  height: auto;
}

.logocheck .hint {
  flex: 1 1 100%;
  margin: 0;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
  margin-top: var(--s4);
}

.actions form {
  margin: 0;
}

/* A bare list — used for the "who clicked" disclosure, where bullets and the
   default indent add nothing to a column of addresses. */
.plain {
  list-style: none;
  margin: var(--s2) 0 0 0;
  padding: 0;
}

.plain li {
  padding: 2px 0;
}

/*
 * A button that has just saved. Deliberately a colour change and a word rather
 * than a tick alone — on a phone the whole point is that something visibly
 * different happened where your thumb already is.
 */
button.saved,
button.secondary.saved {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok);
}

/* ------------------------------------------------- API key permissions ---- */

/*
 * One block per permission, with that permission's own fields indented under
 * its radio. The indent is the whole point: the fields used to sit in a flat
 * run after all three radios, which read as belonging to whichever radio came
 * last (owner, 2026-09-17). Ownership has to be visible, not inferred.
 */
.permission + .permission {
  margin-top: var(--s4);
}

.permission-detail {
  margin-left: var(--s5);
  padding-left: var(--s4);
  border-left: 2px solid var(--line);
}

.permission-detail > :first-child {
  margin-top: var(--s2);
}

/* ---------------------------------------------------------- /docs/api ---- */

/* The rendered API reference. Wide code and tables scroll inside their own
   containers (`.codeblock`, `.tablewrap`) — the page body never scrolls
   sideways. */
.doc h2 {
  margin-top: var(--s6);
}

.doc h3 {
  margin-top: var(--s5);
}

.doc pre.codeblock {
  padding: 12px 14px;
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
}

.doc hr {
  border: none;
  border-top: 1px solid var(--line);
  margin: var(--s6) 0;
}

/* The Cloudflare auto-DNS offer, above the record list. A boxed reveal rather
   than a bare summary line: the owner found the bare version "almost missable"
   at the bottom of the page, and the whole point of the feature is being seen
   BEFORE someone starts copying records by hand. */
details.reveal-offer {
  margin: var(--s3) 0 var(--s4);
  padding: var(--s2) var(--s3);
  background: var(--accent-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

details.reveal-offer > summary {
  font-weight: 600;
  color: var(--ink);
}

/* The labelled variant of the copy button — "Copy all records" on the domain
   wizard. Same island, same icon swap; a visible border because it stands
   alone rather than beside the field it copies. */
button.copy.copyall {
  border-color: var(--line-strong);
  color: var(--ink-soft);
  margin-bottom: var(--s3);
}

button.copy.copyall:hover {
  color: var(--ink);
}

/* Two buttons side by side in a table cell (re-roll / revoke on a key row). */
.row-actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.row-actions form {
  margin: 0;
}

.mono,
code {
  font-family: var(--mono);
  font-size: 13px;
}

.breakable {
  overflow-wrap: anywhere;
}

/*
 * ⚠ This class was USED in four places and DEFINED NOWHERE until 2026-08-21,
 * which is why the send log's Sent column broke `2026-08-14` across three
 * lines: a squeezed column will break at the hyphens unless something stops
 * it. A timestamp, a `YYYY-MM` archive month and a "First/Then" rank are all
 * atoms — they either fit on one line or the table scrolls in its own box.
 */
.nowrap {
  white-space: nowrap;
}

/* The provider's own words, never paraphrased — kept visually distinct. */
.verbatim {
  margin: var(--s2) 0 0;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------ copy field */

.copyfield {
  margin: var(--s2) 0 var(--s3);
}

.copyfield-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}

.copyfield-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}

.copyfield code {
  display: block;
  margin-top: var(--s1);
  padding: 10px 12px;
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
  user-select: all;
}

/* For a value whose line structure is part of reading it — a JSON policy
   document, not a DNS record. `pre-wrap` rather than `pre` so a long line
   still wraps instead of scrolling the card sideways. */
.copyfield code.pre {
  white-space: pre-wrap;
}

.copyfield .hint {
  margin-top: 6px;
}

/* 40px square: a copy button is one of the two things anyone taps on the DNS
   screen, and the old text link was ~30px tall with no hit padding. */
button.copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 40px;
  min-height: 40px;
  padding: 0 var(--s2);
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

button.copy:hover {
  color: var(--ink);
  background: var(--surface-sunken);
  border-color: var(--line);
  filter: none;
}

button.copy svg {
  width: 16px;
  height: 16px;
  display: block;
}

button.copy .copy-done {
  display: none;
}

button.copy.is-copied {
  color: var(--ok);
}

button.copy.is-copied .copy-idle {
  display: none;
}

button.copy.is-copied .copy-done {
  display: block;
}

/* ---------------------------------------------------------------- record */

.record {
  padding: var(--s4) 0;
  border-top: 1px solid var(--line);
}

.record-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}

.record h3 {
  margin: 0;
  font-size: 15px;
}

.record .hint:empty {
  display: none;
}

/* ------------------------------------------------------- definition rows */

.rows {
  margin: var(--s4) 0 0;
}

.rows div {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--line);
}

.rows dt {
  font-size: 14px;
  color: var(--muted);
}

.rows dd {
  margin: 0;
  text-align: right;
  overflow-wrap: anywhere;
}

.rows-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--s2) 0;
}

.rows-list li form {
  margin: 0;
}

/* ---------------------------------------------------------------- tables */

/*
 * Wide tables scroll inside their own box. The page body must never scroll
 * sideways — a horizontally scrolling document on a phone makes every screen
 * feel broken, not just the one with the table.
 */
.tablewrap {
  margin: var(--s4) 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.grid {
  width: 100%;
  margin: var(--s4) 0;
  border-collapse: collapse;
}

/*
 * No forced min-width: the header cells are `white-space: nowrap`, so a table
 * that genuinely cannot fit creates its own overflow and scrolls, while a
 * three-column one lays out inside 390px instead of being clipped mid-pill by
 * a width it never needed.
 */
.tablewrap .grid {
  margin: 0;
}

.grid th,
.grid td {
  padding: 10px var(--s3) 10px 0;
  text-align: left;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.grid th {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.grid tbody tr:hover {
  background: var(--surface-sunken);
}

/*
 * A cell holding a SENTENCE rather than a value. Without this the column is as
 * wide as the sentence is long — and because the page column is now shrink-to-
 * fit, one paragraph in one table cell dragged a whole screen out to the 1400px
 * cap with everything else stranded on the left. Measured: the placeholder
 * reference table wanted 1676px, and 801px with this.
 */
.grid td.prose {
  max-width: var(--measure);
}

.grid td.tick,
.grid th.tick {
  width: 28px;
  padding-right: 0;
}

.grid input[type='checkbox'] {
  width: auto;
  margin: 0;
}

/* ------------------------------------------------- filters, bulk actions */

.filters,
.bulkbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin: var(--s4) 0;
}

.filters input,
.filters select,
.bulkbar select {
  width: auto;
  min-width: 160px;
  margin-top: 0;
}

label.inline {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin: 0 0 var(--s2);
}

label.inline input,
label.inline select {
  width: auto;
  margin-top: 0;
}

label.check {
  font-weight: 400;
}

label.check input {
  width: auto;
}

.pager {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  margin: var(--s4) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  font-size: 15px;
}

form.danger {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}

.record .muted.mono {
  max-width: 45%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ------------------------------------------------------- template editor */

textarea.code {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  tab-size: 2;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  resize: vertical;
  background: var(--surface-sunken);
}

/*
 * The preview is tenant-authored HTML in a sandboxed frame, so it cannot be
 * measured from here — a fixed tall box with its own scrollbar is the honest
 * option, and it matches how the email will actually be read: in a window.
 *
 * It stays WHITE in dark mode on purpose: it shows what the recipient will get,
 * and a dark-themed preview of an email that will arrive light is a lie.
 */
iframe.preview {
  display: block;
  width: 100%;
  height: 640px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color-scheme: light;
}

label.inline input[type='color'] {
  width: 44px;
  height: 34px;
  padding: 2px;
}

/*
 * Two `label.inline`s in a row are inline-flex boxes with no whitespace
 * between them (JSX eats it), so they need an explicit gap or they read as one
 * run-on label.
 */
.swatches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s6);
  margin-bottom: var(--s2);
}

.swatches label.inline {
  margin-bottom: 0;
}

/* Maintenance actions on an already-configured thing. Collapsed by default:
   an empty input with a Save button beside it reads as unfinished setup no
   matter how the label is worded, which is exactly how the stored signing
   secret was misread. */
/*
 * The recommended next step on a domain's health panel — the DMARC rung.
 *
 * Deliberately NOT a `.reveal`: this was one until the owner reported reading
 * "Monitoring only (p=none)" and finding no way to change it (2026-08-15). A
 * reveal hides maintenance on a screen that is already FINISHED; an offer to
 * climb the DMARC ladder is the opposite — it is the thing the panel is asking
 * you to do, and a collapsed triangle made it invisible. It reads as a called-
 * out block rather than a bare button so the warning above it carries the
 * weight the disclosure used to.
 */
.steprow {
  margin: var(--s4) 0 var(--s5);
  padding: var(--s3) var(--s4);
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.steprow h4 {
  margin: 0 0 var(--s2);
  font-size: 15px;
}

.steprow p:last-of-type {
  margin-bottom: var(--s3);
}

.reveal {
  margin-top: var(--s4);
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
}

/* The rule exists to separate maintenance from the setup above it. When the
   reveal IS the card's only content there is nothing to separate, and the
   border reads as an empty section. */
.card > .reveal:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.reveal > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 14px;
  padding: 6px 0;
}

.reveal > summary:hover {
  color: var(--ink);
}

.reveal[open] > summary {
  margin-bottom: var(--s2);
}

/* -------------------------------------------------------------- sign-in */

/*
 * The handover code, read off one screen and typed into another. Big, spaced
 * and monospaced because that is the whole job: someone is holding a phone at
 * arm's length copying six characters. The alphabet already excludes I, O, 0
 * and 1 (src/lib/cross-browser.ts); this makes what remains unambiguous too.
 */
.handover-code {
  display: inline-block;
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  background: var(--accent-soft);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  margin: var(--s2) 0 var(--s4);
}

/* ------------------------------------------- the nav's own breakpoint ---- */

/*
 * 1040px, and it is NOT the phone breakpoint — the two are separate on purpose.
 * Eight nav links plus "Log out" plus the theme control measure ~1000px on one
 * row even with the tenant line hidden, so below that the bar overflowed and
 * took the whole page sideways with it. This breakpoint is set by what the
 * navigation actually measures; everything else on the page is still
 * comfortable down to ~700px and is handled in the block after this one.
 */
@media (max-width: 1040px) {
  .topbar {
    flex-wrap: wrap;
    gap: var(--s2);
  }

  .navtoggle {
    display: inline-flex;
  }

  /* The nav drops to its own row (order 3), so these two are what sits on the
     right of the top row. */
  .themetoggle {
    margin-left: auto;
  }

  /*
   * ⚠ CENTRED ON THE BAR ITSELF, NOT PLACED AFTER THE MARK — and that is what
   * takes it OUT of the flex row.
   *
   * In flow it sat immediately after the mark, so its position was the sum of
   * everything to its left: the back arrow (which exists on every page except
   * home), the mark, and the wordmark (which is there in a browser tab and not
   * in the installed app). Three variables, so the organisation name landed
   * somewhere different on almost every screen. Owner, 2026-09-20: "can the org
   * be centered in the titlebar? otherwise it keeps moving around."
   *
   * Absolute centring makes it independent of all three: the same place on
   * every page, in the app and in a tab alike. `.topbar` is `position: sticky`,
   * which is a positioned element, so it is already the containing block.
   *
   * ⚠ CENTRED ON THE ROW, NOT PLACED AT A FIXED `top`. A fixed offset was the
   * first attempt and it sat a few pixels high (owner's screenshot,
   * 2026-09-20): the row is as tall as its TALLEST item — the hamburger, not
   * the 26px mark — and `.topbar` centres everything else inside that, so a
   * chip measured off the padding edge cannot line up with an icon centred in
   * the row. `top: 50%` is the same rule the rest of the bar already follows,
   * so the two icons share a centre line by construction rather than by
   * arithmetic that goes stale the moment a control changes height.
   *
   * ⚠ IT NEEDS NO SPECIAL CASE FOR THE OPEN MENU, because the open menu is
   * taken out of the header's box rather than allowed to grow it — see the
   * `nav-open` rule below. That is what `app.js` puts `nav-open` on the body
   * for; CSS has no way to look from the chip at a sibling further down.
   *
   * ⚠ `max-width` KEEPS IT CLEAR OF BOTH ENDS. 14rem is the arrow, the mark and
   * the wordmark on the left plus the theme control and the hamburger on the
   * right; a long name ellipses inside what is left rather than sliding under
   * either. `pointer-events: none` because it is a label and must never eat a
   * tap meant for the bar behind it.
   */
  .orgchip {
    display: inline-flex;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-width: calc(100% - 14rem);
    justify-content: center;
    pointer-events: none;
  }

  /*
   * ⚠ THE OPEN MENU LEAVES THE HEADER'S BOX, so the bar stays the height of its
   * top row and the chip's `top: 50%` keeps meaning the same thing.
   *
   * The alternative was to re-pin the chip whenever the menu was open, and that
   * is what shipped first: it put the organisation name a few pixels high the
   * moment the menu opened (owner, 2026-09-20: "now when i click the menu the
   * emus logo and text moves up and is no longer vertically aligned"). The
   * arithmetic could not be made right — the row's height comes from `.iconbtn`
   * at 40px, or 44px on a touch screen, so any constant written here is wrong
   * at one of the two breakpoints. Taking the menu out of flow removes the
   * second case instead of guessing at it: ONE rule for the chip, in every
   * state.
   *
   * A menu that overlays the page rather than shoving it down is also the
   * behaviour people expect of a drawer. It sits inside `.topbar`, which has a
   * z-index of 50, so it is above the page without a z-index of its own.
   *
   * ⚠ ITS SIDE PADDING HAS TO MATCH THE HEADER'S, and cannot inherit it: out of
   * flow, `left: 0` is the padding box's edge, so without this the links would
   * start hard against the window. The phone breakpoint below changes both, and
   * they must change together.
   */
  body.nav-open .topbar nav.mainnav.is-open {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    padding-left: var(--s5);
    padding-right: var(--s5);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
  }

  /* ⚠ The wordmark gives way to the organisation name, and only when the name
     is long enough to need the room — `long-org` is set server-side in
     src/ui/layout.tsx. The 26px mark stays: it is what says which app this is,
     and it costs a fraction of what the word beside it does. The deploy SHA
     lives inside `.brand-text` and goes with it; it is a debugging affordance,
     and /admin still prints it. */
  body.long-org .brand-text,
  /* ⚠ AND ALWAYS IN THE INSTALLED APP, whatever the name's length. Only there
     does the row also carry the back arrow, and that was enough to wrap the
     hamburger onto a second line even for a short org name (owner, 2026-09-20:
     "now when the back arrow appears its forcing the burger menu button onto a
     new line. please just remove the cf-email text for the pwa"). An installed
     app does not need to caption itself — the user chose its icon off their own
     home screen, which is more than the word says. Scoped inside the drawer
     breakpoint, so a desktop-installed window keeps the wordmark it has room
     for. */
  html.is-standalone .brand-text {
    display: none;
  }

  /* Only the signed-in nav collapses. The signed-out one is two short links and
     fits on a phone as it is — hiding it behind a hamburger would put the login
     link one tap further away for no gain. */
  .topbar nav.mainnav {
    display: none;
    order: 3;
    width: 100%;
    margin-left: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--s2) 0 var(--s3);
    border-top: 1px solid var(--line);
  }

  /*
   * ⚠ THE OPEN DRAWER SCROLLS ITSELF, AND MUST. It is a child of `.topbar`,
   * which is `position: sticky; top: 0` — so before this rule the open menu was
   * thirteen links tall INSIDE a pinned header. Scrolling then moved the page
   * behind a header that stayed put: the scrollbar travelled and the menu did
   * not move at all, until eventually the sticky header gave up its position
   * and the bottom of the list arrived.
   *
   * Owner, 2026-09-20, in the installed app: "when the menu is visible after
   * clicking the hamburger, i have to scroll for ages to got to the bottom of
   * the menu and for the start it doesnt move at all even though the scroll bar
   * is moving."
   *
   * Bounding it to the viewport gives the panel its own scroll, so the first
   * drag moves the thing under the finger. `100dvh` rather than `100vh`
   * because a phone browser's address bar makes those differ by ~60px, and the
   * difference is exactly the last menu item. `5rem` is the header row above
   * it; `overscroll-behavior: contain` stops a flick that reaches the end of
   * the list from carrying on into the page underneath.
   */
  .topbar nav.mainnav.is-open {
    display: flex;
    max-height: calc(100dvh - 5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .topbar nav.mainnav a {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: 11px 2px;
    font-size: 16px;
    border-bottom: 1px solid var(--line);
  }

  /* The drawer is a full-width column, so these cost no horizontal room. */
  .navicon {
    display: block;
  }

  .topbar nav.mainnav a:hover {
    border-bottom-color: var(--line);
  }

  .topbar .who {
    padding: 11px 2px 0;
    white-space: normal;
  }
}

/* ------------------------------------------------------------- phone ---- */

@media (max-width: 700px) {
  main {
    margin: var(--s4) auto var(--s5);
    padding: 0 var(--s4);
  }

  .card {
    padding: var(--s4);
    border-radius: var(--radius-sm);
  }

  h1 {
    font-size: 21px;
  }

  .topbar {
    padding: var(--s2) var(--s4);
  }

  /* ⚠ Follows the header's padding above — see the overlay rule: the open menu
     is out of flow, so it does not inherit it. Change both or neither. */
  body.nav-open .topbar nav.mainnav.is-open {
    padding-left: var(--s4);
    padding-right: var(--s4);
  }

  .tile-head,
  .record-head,
  .copyfield-head {
    flex-wrap: wrap;
  }

  .rows div {
    flex-direction: column;
    gap: 2px;
  }

  .rows dd {
    text-align: left;
  }

  .record .muted.mono {
    max-width: 100%;
  }

  .filters input,
  .filters select,
  .bulkbar select {
    width: 100%;
    min-width: 0;
  }

  .stats {
    gap: var(--s4) var(--s5);
  }

  /*
   * Inside a scroll container, let the cells keep their natural width instead
   * of compressing to fit. Without this a table squeezes its widest column
   * until an email address wraps to one word per line and the row is unreadable
   * — the scrollbar exists precisely so that does not have to happen. A table
   * whose content does fit (the 3-column domain list) is unaffected.
   */
  .tablewrap .grid th,
  .tablewrap .grid td {
    white-space: nowrap;
  }

  /*
   * …except the one or two columns that carry free text. `nowrap` above is
   * right for atoms (a timestamp, a pill, a count) and wrong for a subject
   * line or an address, where it makes ONE column as wide as its longest value
   * and pushes every column after it out of view. Wrapping those makes the row
   * taller and keeps the whole table on the screen — a phone reader can then
   * see the state without scrolling sideways, which is the point.
   */
  .tablewrap .grid th.col-wrap,
  .tablewrap .grid td.col-wrap {
    white-space: normal;
    overflow-wrap: anywhere;
  }

  iframe.preview {
    height: 420px;
  }

  /* Anything tappable clears 44px. */
  button,
  a.button,
  button.copy,
  .iconbtn {
    min-height: 44px;
  }

  button.copy,
  .iconbtn {
    min-width: 44px;
  }
}

/* ------------------------------------------------------- the visual editor
 *
 * GrapesJS ships its own chrome, which is dark in both themes and therefore
 * jarring on a light page and merely inconsistent on a dark one. It exposes
 * five class hooks for exactly this, so the panels are re-pointed at the app's
 * own tokens rather than restyled rule by rule:
 *
 *   .gjs-one-bg    panel backgrounds        .gjs-two-color   secondary text
 *   .gjs-three-bg  selection / active       .gjs-four-color  highlight
 *
 * These rules load only on visual-editor pages, alongside the vendored
 * stylesheet, so nothing here can affect any other screen. ⚠ The `.gjs-*`
 * classes are the vendor's public theming contract; anything BELOW them in
 * their stylesheet is not, so do not reach past these into internals — a
 * library update would silently undo it.
 *
 * The canvas itself is deliberately NOT themed: it shows an email, and an
 * email is rendered on whatever background the recipient's client uses. Making
 * it follow the app's dark mode would be a lie about what gets sent.
 */

.gjs-pane {
  margin-block: var(--s4);
}

.gjs-host {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  /* Before the island boots — and permanently if the vendored files 404 —
     this is all there is, so it needs to occupy the space the canvas will. */
  min-height: 240px;
}

.gjs-booting {
  padding: var(--s5);
  text-align: center;
}

.gjs-host .gjs-one-bg {
  background-color: var(--surface-sunken);
}

.gjs-host .gjs-one-color,
.gjs-host .gjs-one-color-h:hover {
  color: var(--ink);
}

.gjs-host .gjs-two-color {
  color: var(--ink-soft);
}

.gjs-host .gjs-three-bg {
  background-color: var(--accent);
  color: var(--accent-ink);
}

.gjs-host .gjs-four-color,
.gjs-host .gjs-four-color-h:hover {
  color: var(--accent);
}

/* The panels' own borders and inputs, which the five hooks do not cover.
   Without these the block palette's tiles keep a near-black border that reads
   as a rendering fault against a light surface. */
.gjs-host .gjs-block,
.gjs-host .gjs-sm-sector,
.gjs-host .gjs-clm-tags,
.gjs-host .gjs-blocks-c,
.gjs-host .gjs-pn-panel {
  border-color: var(--line);
}

.gjs-host .gjs-block {
  color: var(--ink-soft);
}

.gjs-host .gjs-field {
  background-color: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
}

/* The canvas frame sits on the app's sunken surface so the email's own white
   page edge stays visible in light mode instead of blending into it. */
.gjs-host .gjs-cv-canvas {
  background-color: var(--surface-sunken);
}

/* An organisation's name with its favicon beside it, on /orgs. Matches the
   topbar menu's rows so the same organisation looks the same in both places;
   `.siteicon-none` holds the column open when there is no verified domain. */
.orgrow {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.orgrow .siteicon,
.orgrow .siteicon-none {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 3px;
}

/*
 * The invitation banner.
 *
 * Above the flash and above the page on every screen, because an invitation is
 * the one thing in this app addressed TO the person rather than produced BY
 * them. Deliberately not a modal: a dialog that blocks the app to announce
 * something optional is worse than a banner, and there is nothing here that
 * cannot wait. It leaves by being accepted or declined, so no dismissed state
 * is stored.
 *
 * Uses the accent, not a warning colour — being invited is good news, and the
 * warning palette in this app means "something is wrong with your sending".
 */
.invite-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
}

.invite-banner-actions {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex: none;
}

.invite-banner form {
  margin: 0;
}

/*
 * The organisation icon in the bar, at the brand mark's size.
 *
 * The owner asked for parity with the cf-email mark top-left (26px) — at 16px it
 * read as a favicon in a list rather than as the thing identifying where you
 * are. ⚠ It costs the row 10px more than 16px did, and the bar is the tightest
 * thing in this app, so the band thresholds below were RE-MEASURED after this
 * change rather than adjusted by arithmetic.
 */
.topbar .orgswitch > summary .siteicon {
  width: 26px;
  height: 26px;
  border-radius: 5px;
}

/*
 * The "All pages" menu — every destination, at every desktop width.
 *
 * ⚠ It replaced the old "More ▾" dropdown, which only ever held the three links
 * the bar dropped between 1041 and 1259px. That answered "where did that link
 * go?" and never "where is the screen I have never seen?" — and this app kept
 * producing screens with no reliable door: /api-keys was drawer-only on desktop,
 * /orgs and /orgs/people had their only links inside a menu that did not render
 * for a single-organisation owner, /messages and /messages/archive were
 * reachable from one paragraph each. The bar shows what fits; this shows
 * everything.
 *
 * Hidden ≤1040px, where the drawer stacks the whole list instead — a nested
 * dropdown inside an already-stacked panel is a worse control than a longer
 * list.
 */
.navmore.allpages {
  display: block;
}

.topbar .allpages > summary svg {
  width: 20px;
  height: 20px;
  display: block;
}

.topbar .allpages > summary {
  padding: 6px 4px;
  color: var(--ink-soft);
}

.topbar .allpages > summary:hover {
  color: var(--ink);
}

/* ⚠ The menu's icons are always on, unlike the bar's. A dropdown panel has its
   own width and pays nothing for them, and this is a list of sixteen
   destinations where a glyph is what makes it scannable rather than read. */
.topbar .allpages-menu .navicon,
.topbar nav.mainnav .drawer-only .navicon {
  display: block;
}

.allpages-menu {
  min-width: 230px;
  max-height: min(70vh, 560px);
  overflow-y: auto;
}

/* A section label, not a link — it must not read as one. */
.allpages-group {
  margin: var(--s2) 0 2px;
  padding: 0 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.allpages-group:first-child {
  margin-top: 0;
}

/*
 * The same destinations again for the drawer, where the menu above is hidden.
 * Only ever one of the two is on screen.
 *
 * ⚠ SPECIFICITY, and it bit hard. This was `.drawer-only { display: none }`
 * (0,1,0) while `.topbar nav a` is (0,2,1) — so the moment bar links were given
 * `display: inline-flex` on 2026-08-19, every drawer link became VISIBLE on
 * desktop and quietly added ~480px to the row. The symptom did not look like a
 * specificity problem at all: the priority+ code correctly measured a row that
 * really was too wide and moved almost everything into the menu, so the bar
 * showed ONE link at 1272px and the arithmetic was right about a wrong layout.
 *
 * Same trap as the `.topbar .navmore` note above (PLAN.md, 2026-08-15): a rule
 * that must beat a later, more specific one has to out-specify it, and a media
 * query does not help.
 */
.topbar nav.mainnav > a.drawer-only {
  display: none;
}

/*
 * ⚠ The group headings need their OWN rule, and the same specificity care as
 * the links above. `.allpages-group` alone is (0,1,0) and would lose to
 * anything more specific in the bar; and because these are `p` rather than `a`,
 * the rule above does not cover them — a heading left visible on desktop would
 * appear in the middle of the nav row.
 */
.topbar nav.mainnav > p.allpages-group.drawer-only {
  display: none;
}

@media (max-width: 1040px) {
  .topbar nav.mainnav > a.drawer-only {
    display: flex;
  }

  .topbar nav.mainnav > p.allpages-group.drawer-only {
    display: block;
    margin: var(--s3) 0 2px;
  }

  .topbar .navmore.allpages {
    display: none;
  }
}

/*
 * Bar links that have been moved into the menu because the row ran out of room.
 *
 * They are the SAME elements, relocated — so they arrive carrying `.nav-overflow`
 * and the bar's own link styling, which has to be undone here rather than by
 * giving them a second class on the way in. A rule keyed to where an element IS
 * beats bookkeeping about what it has been through.
 */
.allpages-overflow {
  display: grid;
  gap: 2px;
  padding-bottom: var(--s2);
  margin-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}

.topbar .allpages-overflow > a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 8px 10px;
  border-radius: 6px;
  border-bottom: none;
  font-size: 15px;
}

.topbar .allpages-overflow > a:hover {
  background: var(--surface-sunken);
  color: var(--ink);
}

.topbar .allpages-overflow > a[aria-current='page'] {
  background: var(--surface-sunken);
  color: var(--ink);
}

/*
 * ⚠ The menu itself is only worth showing when it has something to say. With a
 * bar wide enough to hold every link it still carries the destinations that were
 * never in the bar, so it never becomes empty — but if that ever changes, an
 * empty dropdown is a control that lies about having content.
 */
.allpages-overflow[hidden] {
  display: none;
}

/* ------------------------------------------------ the public front page */

/*
 * `src/routes/landing.tsx` — the one page a visitor with no account sees.
 *
 * Both lists here are (title, explanation) pairs, which is a two-line item, so
 * neither can use `.rows-list` (a single centred flex row). They are grids: the
 * marker column is fixed, the text column takes the rest, and the explanation
 * starts under the title rather than under the marker.
 *
 * ⚠ `main p` caps paragraphs at `--measure`; these lists are NOT paragraphs and
 * would otherwise run the full width of a wide window. The cap is repeated on
 * the list itself so a 2560px monitor gets the same line length as a laptop.
 */
.numsteps,
.ledger {
  list-style: none;
  margin: var(--s4) 0 0;
  padding: 0;
  max-width: var(--measure);
}

.numsteps {
  counter-reset: numstep;
}

.numsteps li,
.ledger li {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  gap: 2px var(--s3);
  padding: var(--s3) 0;
  border-top: 1px solid var(--line);
}

.numsteps li:first-child,
.ledger li:first-child {
  border-top: none;
}

.numsteps li::before {
  counter-increment: numstep;
  content: counter(numstep);
  grid-row: span 2;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.7;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.numsteps li > .muted,
.ledger li > .muted {
  grid-column: 2;
}

/* The tone carries which list this is; the bullet is decorative and hidden from
   a screen reader, which reads the two headings above instead. */
.ledger-mark {
  grid-row: span 2;
  font-size: 15px;
  line-height: 1.5;
}

.ledger-yes .ledger-mark {
  color: var(--ok);
}

.ledger-no .ledger-mark {
  color: var(--warn);
}

/*
 * The front page's own width. `main` is shrink-to-fit around its widest child
 * and the ledger below is two columns, so without this the single-column cards
 * are stretched to `--page` with their text still at `--measure` — a ribbon of
 * words down the left of an empty card. 1040px is the width at which both
 * ledger columns still hold a two-line row without wrapping the title.
 */
.frontpage {
  max-width: 1040px;
}

/* ---------------------------------------- the front page, dressed up a little */

/*
 * The front page is the one screen here that has to persuade rather than
 * inform, so it is allowed a hero, a tint and a type scale the tool screens are
 * not. Everything below still comes out of the same tokens — no new colours, no
 * imported font — because a landing page that looks like a different product
 * from the app behind it is a worse lie than a plain one.
 */

.hero {
  padding: var(--s6) var(--s5) var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, transparent 60%),
    var(--surface);
  box-shadow: var(--shadow);
}

.hero h1 {
  margin: var(--s3) 0 0;
  font-size: clamp(28px, 5vw, 44px);
  line-height: 1.08;
  letter-spacing: -0.02em;
  max-width: 18ch;
  text-wrap: balance;
}

.hero-eyebrow {
  margin: 0;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

/* ⚠ `main p` caps every paragraph at --measure; the lede wants to be shorter
   than that, not longer, so this narrows rather than widens. */
.hero-lede {
  margin: var(--s4) 0 0;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 54ch;
}

.hero .actions {
  margin-top: var(--s5);
}

/* The four relays, as marks rather than a sentence: the names are what a
   visitor is scanning for, and four favicons say "these are real" faster than
   a list does. */
.hero-marks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  margin: var(--s5) 0 0;
  padding: var(--s4) 0 0;
  border-top: 1px solid var(--line);
  list-style: none;
}

.hero-mark {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink-soft);
}

.hero-marks .hero-mark:first-child {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* A quiet label above a section heading, so the page has a rhythm instead of a
   stack of equal cards. */
.sectionlabel {
  margin: 0 0 var(--s1);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The two ledger columns. The tint is the whole point: "what it does" and "what
   it does not" must be distinguishable before a word is read. */
.ledgercard {
  border-top: 3px solid var(--line);
}

.ledgercard h2 {
  margin-top: 0;
}

.ledger-yes.ledgercard,
.card.ledgercard.ledger-does {
  border-top-color: var(--ok-line);
}

.card.ledgercard.ledger-gaps {
  border-top-color: var(--warn-line);
}

.card.ledgercard.ledger-does h2 {
  color: var(--ok);
}

.card.ledgercard.ledger-gaps h2 {
  color: var(--warn);
}

/* Numerals in a ring rather than loose digits — at 11px they read as debris. */
.numsteps li {
  grid-template-columns: 26px minmax(0, 1fr);
}

.numsteps li::before {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
}

/* The closing band: the same offer as the hero, at the point somebody has
   finished reading and is deciding. */
.closing {
  text-align: center;
  background:
    radial-gradient(100% 160% at 50% 0%, var(--accent-soft) 0%, transparent 65%),
    var(--surface);
}

.closing h2 {
  margin-top: 0;
}

.closing p {
  margin-left: auto;
  margin-right: auto;
}

.closing .actions {
  justify-content: center;
}

/* ------------------------------------------------------- the image library */

/*
 * A grid of cards rather than a table: the thing being scanned is a picture,
 * and the metadata beside it is what you read once you have found the right one.
 */
.medialist {
  list-style: none;
  margin: var(--s4) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--s4);
}

.mediaitem {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}

/*
 * ⚠ A checkerboard behind the image, not a flat colour. A PNG with a
 * transparent background is invisible against white and looks broken against
 * dark — and transparency is exactly what you need to see before putting a logo
 * in an email whose background you do not control.
 */
.mediathumb {
  width: 100%;
  height: 160px;
  object-fit: contain;
  background-color: var(--surface-sunken);
  background-image:
    linear-gradient(45deg, var(--line) 25%, transparent 25%),
    linear-gradient(-45deg, var(--line) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--line) 75%),
    linear-gradient(-45deg, transparent 75%, var(--line) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
  border-bottom: 1px solid var(--line);
}

/* Filters above the image library: plain links styled as toggles. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: 0 0 var(--s3);
}
.chip {
  display: inline-flex;
  gap: var(--s1);
  align-items: center;
  padding: 6px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: inherit;
  text-decoration: none;
  font-size: 14px;
}
.chip.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}

/* "Which do I want?" on the API keys and Signup forms pages: a quiet box
   with the rule of thumb for choosing between a secret key and a form. */
.signup-choice {
  margin: var(--s3) 0;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  background: var(--accent-soft);
  font-size: 14px;
}
.signup-choice ul {
  margin: var(--s2) 0;
  padding-left: 1.2em;
}
.signup-choice li + li {
  margin-top: var(--s1);
}

/* Signup form embed codes: several lines of code beside a copy button. */
.copyrow-block {
  align-items: flex-start;
}
.copyrow-block textarea {
  width: 100%;
  font-size: 13px;
  line-height: 1.5;
  resize: vertical;
}

.mediameta {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s3) var(--s4) var(--s4);
  font-size: 14px;
  min-width: 0;
}

/* The address field earns its full width here — it is the reason for the page. */
.mediameta .copyfield {
  width: 100%;
  font-family: var(--mono);
  font-size: 12px;
}

/* Clickable column headings (2026-09-20). Inherit the heading's colour so a
   sortable column reads as a heading first and a link second. */
.grid th .sortlink {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted currentColor;
}

.grid th .sortlink:hover {
  border-bottom-style: solid;
}

/* The key-kind symbol beside a key's name (2026-09-20). */
.keykind {
  display: inline-flex;
  vertical-align: -2px;
  color: var(--muted);
}

/* The rename pencil: a <details> whose summary is an icon button and whose
   body is the one-line form, dropped in beside the heading. */
.rename summary {
  list-style: none;
  cursor: pointer;
}

.rename summary::-webkit-details-marker {
  display: none;
}

.rename[open] {
  flex: 1 1 100%;
}

.rename form {
  margin-top: var(--s3);
}

/* ------------------------------------------------ the delete-list panel */

/* Red, two-step, and everything the deletion takes with it stacked inside
   the step that does it (2026-09-20). `.button.danger` is a <summary> dressed
   as a button so the first press only opens the panel; `button.danger` is
   the real one. */
.dangerzone {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}

.dangerzone h3 {
  margin: 0 0 var(--s3);
}

.button.danger,
button.danger {
  display: inline-block;
  padding: 8px 14px;
  border: 1px solid var(--bad);
  border-radius: 8px;
  background: var(--bad);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.button.danger::-webkit-details-marker {
  display: none;
}

.confirm[open] > .button.danger {
  background: transparent;
  color: var(--bad);
}

.confirm-body {
  margin-top: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--bad-line);
  border-radius: 10px;
  background: var(--bad-bg);
  display: grid;
  gap: var(--s3);
  justify-items: start;
}

.confirm-body p {
  margin: 0;
}

.confirm-body label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}

/*
 * The image library's address with its copy button beside it. The input keeps
 * its own width rules; the row only stops the button wrapping under it.
 */
.copyrow {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.copyrow .copyfield {
  flex: 1 1 auto;
  min-width: 0;
}

/*
 * A new campaign's editor choice: the buttons sit in one row, so the ruled
 * separators `.modeswitch` draws between warned conversions do not apply.
 */
.modechoice .modeswitch {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* 0039. Where an AI tool's sign-in approval is sent, big enough to read before
   pressing Allow: the consent screen's one security job. */
.consent-host {
  display: block;
  margin: 0.4rem 0;
  font-size: 1.35rem;
  overflow-wrap: anywhere;
}


/*
 * The organisation chip AS THE SWITCHER, on the phone header (2026-09-25).
 * Owner: "I want the org switch to be in the header on mobile. At the moment
 * it shows the current org. Might as well just put the switcher there
 * instead." Only for someone in more than one organisation — one organisation
 * keeps the plain label, because a menu with nothing to switch to is not a menu.
 *
 * ⚠ THE DRAWER'S COPY IS HIDDEN AT THE SAME BREAKPOINT, so there is never
 * more than one switcher on screen. Above 1041px the chip is hidden and the
 * row's own switcher is the one.
 */
.orgchip-switch > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
  list-style: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
  padding: 2px 4px;
}

.orgchip-switch > summary::-webkit-details-marker {
  display: none;
}

.orgchip-switch > summary:hover,
.orgchip-switch[open] > summary {
  color: var(--ink);
}

.orgchip-chev {
  width: 12px;
  height: 12px;
  flex: none;
}

.orgchip-switch[open] .orgchip-chev {
  transform: rotate(180deg);
}

/* Centred under the chip, which is itself centred in the bar. `max-content`
   so a long organisation name widens the menu rather than wrapping. */
.orgchip-menu {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  width: max-content;
  min-width: 220px;
  max-width: calc(100vw - 32px);
  display: grid;
  gap: 2px;
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: left;
}

.orgchip-menu form {
  display: block;
  margin: 0;
}

@media (max-width: 1040px) {
  /* The label chip ignores taps (it sits over the bar); the switcher must not. */
  .orgchip.orgchip-switch {
    pointer-events: auto;
  }

  .topbar nav.mainnav .orgswitch {
    display: none;
  }
}

/* Icon and name on one line with a gap, as in the row's switcher: a later
   `.topbar .orgswitch-item { display: block }` otherwise wins on source order
   and puts the favicon hard against the name. */
.topbar .orgchip-menu .orgswitch-item,
.topbar .orgchip-menu .orgswitch-current {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.topbar .orgchip-menu .siteicon,
.topbar .orgchip-menu .siteicon-none {
  width: 16px;
  height: 16px;
  flex: none;
}

/*
 * 0041. A separate logo for dark pages (The Good Behaviours' cream logo). Both
 * images are in the page; this shows one. Same two dark routes as `.invertible`
 * above, for the same reason.
 */
.publicmast-logo.for-dark {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .publicmast-logo.has-dark {
    display: none;
  }

  :root:not([data-theme='light']) .publicmast-logo.for-dark {
    display: inline;
  }
}

:root[data-theme='dark'] .publicmast-logo.has-dark {
  display: none;
}

:root[data-theme='dark'] .publicmast-logo.for-dark {
  display: inline;
}

/* =====================================================================
 * The front page (src/routes/landing.tsx), 2026-09-26 Currawong rebrand.
 *
 * The logo's own palette runs it: deep teal, envelope orange, the yellow of
 * the bird's eye. The teal bands and the orange price band keep their
 * colours in both themes (they are the brand, and every colour on them is
 * chosen against them); the plain bands use the theme tokens, so the page is
 * readable in light and dark alike.
 *
 * ⚠ ORANGE CARRIES DARK TEXT, NEVER WHITE: white on #f17f22 is 2.7:1. The
 * orange buttons are teal-on-orange (7:1).
 * ===================================================================== */
.lp {
  --cw-deep: #00283a;
  --cw-teal: #003e56;
  --cw-teal-2: #0c617c;
  --cw-orange: #f17f22;
  --cw-yellow: #f9c80e;
  --cw-mist: #d8dfe0;
  display: flex;
  flex-direction: column;
  gap: 56px;
  width: 100%;
}

.lp h1,
.lp h2,
.lp h3 {
  letter-spacing: -0.02em;
}

.lp-kicker {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cw-orange);
}

/* ---- hero ------------------------------------------------------------ */
.lp-hero {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: center;
  gap: 24px;
  padding: 64px 56px 110px;
  border-radius: 28px;
  color: #ffffff;
  background:
    radial-gradient(circle at 78% 30%, rgba(12, 97, 124, 0.95), transparent 55%),
    radial-gradient(circle at 10% 110%, rgba(241, 127, 34, 0.25), transparent 45%),
    linear-gradient(160deg, var(--cw-teal) 0%, var(--cw-deep) 100%);
}

.lp-hero-words {
  position: relative;
  z-index: 1;
}

.lp .lp-wordmark {
  margin: 0 0 28px;
  max-width: none;
  font-size: clamp(52px, 8.4vw, 124px);
  font-weight: 900;
  line-height: 0.86;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: #ffffff;
}

.lp-wordmark span {
  display: block;
  margin-top: 14px;
  font-size: clamp(12px, 1.35vw, 19px);
  font-weight: 700;
  letter-spacing: 0.46em;
  color: var(--cw-yellow);
}

.lp-hero h1 {
  margin: 0 0 18px;
  max-width: 17ch;
  font-size: clamp(26px, 3.1vw, 42px);
  line-height: 1.08;
  font-weight: 800;
  color: #ffffff;
}

.lp .lp-lede {
  margin: 0 0 30px;
  max-width: 54ch;
  font-size: 17px;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.84);
}

.lp-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 18px;
}

.lp-cta {
  display: inline-block;
  padding: 15px 26px;
  border-radius: 999px;
  font-size: 16px;
  font-weight: 800;
  text-decoration: none;
  color: var(--cw-deep);
  background: var(--cw-orange);
  box-shadow: 0 10px 30px -10px rgba(241, 127, 34, 0.7);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.lp-cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 34px -10px rgba(241, 127, 34, 0.85);
}

.lp-ghost {
  display: inline-block;
  padding: 13px 22px;
  border-radius: 999px;
  border: 2px solid rgba(255, 255, 255, 0.55);
  font-weight: 700;
  text-decoration: none;
  color: #ffffff;
}

.lp-ghost:hover {
  border-color: #ffffff;
}

.lp-link {
  font-weight: 700;
  text-decoration: none;
  color: var(--cw-yellow);
}

.lp-link:hover {
  text-decoration: underline;
}

.lp-hero-art {
  position: relative;
  z-index: 1;
}

.lp-bird {
  display: block;
  width: 118%;
  max-width: none;
  height: auto;
  margin-left: -4%;
  transform: rotate(-5deg);
  filter: drop-shadow(0 30px 40px rgba(0, 0, 0, 0.45));
}

.lp-swoosh {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  width: 100%;
  height: 90px;
  fill: var(--cw-orange);
  opacity: 0.92;
}

/* ---- the relays strip ---------------------------------------------- */
.lp-relays {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px 28px;
  margin-top: -24px;
}

.lp .lp-relays p {
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

.lp-relays ul {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.lp-relays li {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  color: var(--ink-soft);
}

/* ---- plain bands ---------------------------------------------------- */
.lp-band h2 {
  margin: 0 0 18px;
  font-size: clamp(30px, 3.6vw, 50px);
  line-height: 1.04;
  font-weight: 850;
  color: var(--ink);
}

.lp .lp-band p {
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
}

.lp-show {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
  gap: 56px;
  align-items: center;
  padding: 0 8px;
}

/* ---- the drawn email ------------------------------------------------ */
.lp-mock {
  border-radius: 18px;
  overflow: hidden;
  background: #ffffff;
  transform: rotate(2deg);
  box-shadow: 0 40px 80px -30px rgba(0, 40, 58, 0.55), 0 0 0 1px rgba(0, 40, 58, 0.08);
}

.lp-mock-bar {
  display: flex;
  gap: 6px;
  padding: 12px 14px;
  background: var(--cw-mist);
}

.lp-mock-bar span {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(0, 40, 58, 0.25);
}

.lp-mock-body {
  padding: 22px 24px 18px;
  color: #18181b;
}

.lp .lp-mock p {
  margin: 0;
  max-width: none;
  color: inherit;
  font-size: 14px;
  line-height: 1.45;
}

.lp .lp-mock .lp-mock-from {
  font-size: 12px;
  color: #71717a;
}

.lp .lp-mock .lp-mock-subject {
  margin: 4px 0 16px;
  font-size: 22px;
  font-weight: 800;
  color: var(--cw-teal);
}

.lp-mock-gig {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 14px;
  align-items: center;
  padding: 14px;
  border-radius: 10px;
  background: #f4f4f5;
}

.lp-mock-poster {
  aspect-ratio: 3 / 4;
  border-radius: 6px;
  background:
    radial-gradient(circle at 30% 30%, var(--cw-yellow), transparent 40%),
    linear-gradient(150deg, var(--cw-orange), var(--cw-teal-2));
}

.lp .lp-mock .lp-mock-when {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #b45309;
}

.lp .lp-mock .lp-mock-title {
  font-size: 17px;
  font-weight: 800;
}

.lp .lp-mock .lp-mock-line {
  color: #52525b;
}

.lp-mock-button {
  display: inline-block;
  margin-top: 8px;
  padding: 7px 14px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 800;
  color: #ffffff;
  background: var(--cw-teal);
}

.lp-mock-video {
  position: relative;
  margin-top: 14px;
  aspect-ratio: 16 / 7;
  border-radius: 10px;
  background:
    radial-gradient(circle at 70% 40%, rgba(249, 200, 14, 0.35), transparent 40%),
    linear-gradient(135deg, #1f2937, var(--cw-deep));
}

.lp-mock-play {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
}

.lp-mock-play::after {
  content: '';
  position: absolute;
  top: 17px;
  left: 21px;
  border-style: solid;
  border-width: 10px 0 10px 16px;
  border-color: transparent transparent transparent var(--cw-deep);
}

.lp .lp-mock .lp-mock-foot {
  margin-top: 14px;
  font-size: 11px;
  color: #a1a1aa;
}

/* ---- features ------------------------------------------------------- */
.lp-features-band {
  padding: 0 8px;
}

.lp-features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin: 28px 0 0;
  padding: 0;
  list-style: none;
}

.lp-feature {
  position: relative;
  padding: 26px 24px 22px;
  border-radius: 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.lp-feature:hover {
  transform: translateY(-3px);
  border-color: var(--cw-orange);
}

.lp-feature-n {
  display: block;
  margin-bottom: 10px;
  font-size: 34px;
  font-weight: 900;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--cw-orange);
}

.lp-feature h3 {
  margin: 0 0 8px;
  font-size: 19px;
  font-weight: 800;
  color: var(--ink);
}

.lp .lp-feature p {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
}

/* ---- the rule: a teal band ------------------------------------------ */
.lp-rule {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: 48px;
  padding: 56px;
  border-radius: 28px;
  background: linear-gradient(160deg, var(--cw-teal), var(--cw-deep));
}

.lp-rule h2 {
  color: #ffffff;
}

.lp .lp-rule p {
  color: rgba(255, 255, 255, 0.82);
}

.lp-rule-list {
  counter-reset: rule;
  display: grid;
  gap: 22px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.lp-rule-list li {
  counter-increment: rule;
  display: grid;
  grid-template-columns: 56px 1fr;
  column-gap: 16px;
  color: rgba(255, 255, 255, 0.82);
  line-height: 1.55;
}

.lp-rule-list li::before {
  content: counter(rule);
  grid-row: span 2;
  font-size: 48px;
  font-weight: 900;
  line-height: 1;
  color: var(--cw-yellow);
}

.lp-rule-list b {
  font-size: 18px;
  color: #ffffff;
}

/* ---- the honest ledger ---------------------------------------------- */
.lp-ledger {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}

.lp-ledger-col {
  padding: 32px 30px;
  border-radius: 22px;
  background: var(--surface);
  border: 1px solid var(--line);
}

.lp-does {
  border-top: 6px solid var(--cw-teal-2);
}

.lp-gaps {
  border-top: 6px solid var(--cw-orange);
}

.lp-ledger h2 {
  font-size: clamp(24px, 2.4vw, 32px);
}

.lp-ledger ul {
  display: grid;
  gap: 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.lp-ledger li {
  display: grid;
  gap: 2px;
  padding-left: 22px;
  position: relative;
}

.lp-ledger li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 7px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--cw-teal-2);
}

.lp-gaps li::before {
  background: transparent;
  border: 2px solid var(--cw-orange);
}

.lp-ledger li b {
  color: var(--ink);
}

.lp-ledger li span {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}

/* ---- price: the orange band ----------------------------------------- */
.lp-price {
  position: relative;
  overflow: hidden;
  padding: 64px 56px;
  border-radius: 28px;
  text-align: left;
  background:
    radial-gradient(circle at 90% 10%, rgba(249, 200, 14, 0.55), transparent 45%),
    var(--cw-orange);
  color: var(--cw-deep);
}

.lp-price .lp-kicker {
  color: var(--cw-deep);
}

.lp .lp-price-big {
  margin: 0 0 16px;
  max-width: none;
  font-size: clamp(64px, 10vw, 140px);
  font-weight: 900;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--cw-deep);
}

.lp-price-big span {
  display: block;
  margin-top: 8px;
  font-size: clamp(18px, 2vw, 26px);
  font-weight: 800;
  letter-spacing: 0;
}

.lp .lp-price-note {
  max-width: 58ch;
  margin: 0 0 28px;
  font-size: 16px;
  line-height: 1.6;
  color: rgba(0, 40, 58, 0.86);
}

.lp-price .lp-cta {
  color: #ffffff;
  background: var(--cw-deep);
  box-shadow: 0 10px 30px -10px rgba(0, 40, 58, 0.7);
}

.lp-price .lp-link {
  color: var(--cw-deep);
}

.lp-price-bird {
  position: absolute;
  right: -30px;
  bottom: -20px;
  width: min(46%, 520px);
  height: auto;
  transform: rotate(-8deg);
  opacity: 0.95;
}

@media (max-width: 860px) {
  .lp {
    gap: 40px;
  }
  .lp-hero,
  .lp-show,
  .lp-rule,
  .lp-ledger {
    grid-template-columns: 1fr;
  }
  .lp-hero {
    padding: 40px 24px 90px;
  }
  .lp-bird {
    width: 100%;
    margin: 0;
  }
  .lp-rule,
  .lp-price {
    padding: 40px 24px;
  }
  .lp-price-bird {
    position: static;
    display: block;
    width: 70%;
    margin: 0 0 16px auto;
  }
  .lp-mock {
    transform: none;
  }
}
