/* ==========================================================================
   Design system — "mail koenig". A royal-postal register: warm parchment
   and soft ink instead of stark black/white, with a single signet-red
   accent reserved for the handful of moments that actually commit
   something (sending, starting, converting) — everything else stays
   ink-on-parchment. Sharp corners throughout (the "ledger" language) except
   one cut corner on the highest-commitment actions, echoing a torn stamp.
   Light-first with a dark variant. Layered on top of Pico CSS (pico.min.css).
   Structure:
     1. Design tokens (light + dark)
     2. Pico variable bridge
     3. Typography
     4. Layout (topbar, account menu, theme switch, shells)
     5. Buttons / forms
     6. Tables
     7. Flash
     8. Auth shell
     9. Landing hero
     10. Public subscribe page
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens — warm parchment/ink neutrals carry all structure; the
   signet-red accent is reserved for --color-accent alone (brand seal, and
   the .button-primary modifier on the few genuine "commit" actions: sending a
   campaign, starting a new one, converting a visitor). Dark is the same
   roles inverted, opted into via prefers-color-scheme:dark or an explicit
   data-theme="dark" (toggled by the account-menu switch).
   -------------------------------------------------------------------------- */

:root {
  --color-background: #fbf8f2;
  --color-surface: #ffffff;
  --color-surface-alt: #f3eee1;
  --color-border: #e2dac7;
  --color-border-strong: #1b1812;
  --color-text: #1b1812;
  --color-text-muted: #6e6553;

  /* Ink action color — the default for every button/submit in the app.
     Kept near-black rather than pure #000 so it reads as warm ink, not
     print toner. */
  --color-primary: #1b1812;
  --color-primary-hover: #35302a;
  --color-primary-inverse: #fbf8f2;

  /* Signet red — a wax-seal accent used ONLY for: the brand mark's seal
     dot, the active nav underline, and the .button-primary modifier on
     campaigns/new + campaigns/index "New campaign" + the landing CTA +
     "Send campaign". Never used for structure, borders, or body text —
     see design-thinking's "reserve impact for punctuation". */
  --color-accent: #ae3428;
  --color-accent-hover: #8f2a20;
  --color-accent-inverse: #fbf3ea;

  --color-success: #3e6b4c;
  --color-danger: #7a3229;
  --color-focus: #1b1812;

  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;
  --spacing-3xl: 4.5rem;

  --radius: 0;

  /* Elevation — reserved for things that genuinely float above the page
     (dropdowns, popovers) or a card the whole of which is a link, never
     for a plain static card sitting flat on the page (a flat card has no
     shadow in real life either). Warm-ink-tinted, not generic gray. */
  --shadow-sm: 0 6px 18px -8px rgba(27, 24, 18, 0.22);
  --shadow-md: 0 16px 36px -14px rgba(27, 24, 18, 0.28);

  --font-display: "Bitter", Georgia, serif;
  --font-body: "Libre Franklin", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Container widths — named rather than the six one-off numbers this
     replaced (each shell/form picked its own max-width independently). */
  --container-xs: 400px; /* .subscribe-page */
  --container-sm: 420px; /* form default, .auth-shell */
  --container-narrow: 560px; /* .card-narrow */
  --container-md: 640px; /* #campaign-form, .public-shell */
  --container-lg: 720px; /* .container (landing) */
  --container-xl: 960px; /* .app-shell */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-background: #15120d;
    --color-surface: #1c1811;
    --color-surface-alt: #262019;
    --color-border: #3a3226;
    --color-border-strong: #f3eee1;
    --color-text: #f3eee1;
    --color-text-muted: #b3a890;

    --color-primary: #f3eee1;
    --color-primary-hover: #d9d2bf;
    --color-primary-inverse: #15120d;

    --color-accent: #dd6152;
    --color-accent-hover: #e87c6f;
    --color-accent-inverse: #1b1510;

    --color-success: #6fae84;
    --color-danger: #d0806a;
    --color-focus: #f3eee1;

    --shadow-sm: 0 6px 18px -6px rgba(0, 0, 0, 0.55);
    --shadow-md: 0 18px 40px -12px rgba(0, 0, 0, 0.6);
  }
}
:root[data-theme="dark"] {
  --color-background: #15120d;
  --color-surface: #1c1811;
  --color-surface-alt: #262019;
  --color-border: #3a3226;
  --color-border-strong: #f3eee1;
  --color-text: #f3eee1;
  --color-text-muted: #b3a890;

  --color-primary: #f3eee1;
  --color-primary-hover: #d9d2bf;
  --color-primary-inverse: #15120d;

  --color-accent: #dd6152;
  --color-accent-hover: #e87c6f;
  --color-accent-inverse: #1b1510;

  --color-success: #6fae84;
  --color-danger: #d0806a;
  --color-focus: #f3eee1;

  --shadow-sm: 0 6px 18px -6px rgba(0, 0, 0, 0.55);
  --shadow-md: 0 18px 40px -12px rgba(0, 0, 0, 0.6);
}

/* --------------------------------------------------------------------------
   2. Pico variable bridge — retheme buttons/forms/tables/cards with zero
   HTML changes. Pico redefines these on its own light-scheme selector at
   higher specificity than a plain :root, so every value here is !important.
   -------------------------------------------------------------------------- */

:root {
  --pico-background-color: var(--color-background) !important;
  --pico-color: var(--color-text) !important;
  --pico-muted-color: var(--color-text-muted) !important;
  --pico-muted-border-color: var(--color-border) !important;
  --pico-border-radius: var(--radius) !important;

  --pico-h1-color: var(--color-text) !important;
  --pico-h2-color: var(--color-text) !important;
  --pico-h3-color: var(--color-text) !important;

  --pico-primary: var(--color-primary) !important;
  --pico-primary-background: var(--color-primary) !important;
  --pico-primary-border: var(--color-primary) !important;
  --pico-primary-underline: transparent !important;
  --pico-primary-hover: var(--color-primary-hover) !important;
  --pico-primary-hover-background: var(--color-primary-hover) !important;
  --pico-primary-hover-border: var(--color-primary-hover) !important;
  --pico-primary-hover-underline: transparent !important;
  --pico-primary-focus: color-mix(in srgb, var(--color-primary) 25%, transparent) !important;
  --pico-primary-inverse: var(--color-primary-inverse) !important;
  --pico-button-box-shadow: none !important;
  --pico-button-hover-box-shadow: none !important;

  --pico-form-element-background-color: var(--color-surface) !important;
  --pico-form-element-border-color: var(--color-border-strong) !important;
  --pico-form-element-color: var(--color-text) !important;
  --pico-form-element-active-background-color: var(--color-surface) !important;
  --pico-form-element-active-border-color: var(--color-focus) !important;
  --pico-form-element-focus-color: var(--color-focus) !important;

  --pico-card-background-color: var(--color-surface) !important;
  --pico-card-border-color: var(--color-border-strong) !important;
  --pico-card-box-shadow: none !important;

  --pico-table-border-color: var(--color-border) !important;
  --pico-table-row-stripped-background-color: transparent !important;

  --pico-ins-color: var(--color-success) !important;
  --pico-del-color: var(--color-danger) !important;
}

/* Global focus-visible backstop — the button/card/table shadow resets above
   (--pico-button-box-shadow etc.) are visual-weight choices, but Pico's own
   focus rings are also built on --pico-box-shadow (now left at its
   default), so this makes sure every interactive element still gets a
   clearly visible outline on keyboard focus regardless of what any other
   rule in this file does to box-shadow. */
:is(a, button, [type="submit"], [type="button"], input, select, textarea, summary, [contenteditable="true"]):focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Pico draws a rotated-chevron ::after on every <summary> (its accordion
   affordance) — the account menu's avatar trigger isn't an accordion and
   doesn't read as one; the stray chevron just floated next to the avatar
   with no visual relationship to it. It opens a floating menu, not an
   in-page expand, so it doesn't want Pico's inline chevron. */
.account-trigger::after {
  content: none;
  display: none;
}

/* --------------------------------------------------------------------------
   3. Typography — Bitter (slab serif, headline/masthead register) paired
   with Libre Franklin (a Franklin Gothic descendant) for body/UI/tables.
   -------------------------------------------------------------------------- */

/* Reserves the vertical scrollbar's width whether or not it's currently
   showing. Without this, opening the account menu on a short page (its
   dropdown is position:absolute, but still extends the page's scrollable
   area) can cross the threshold where a scrollbar appears, which shifts
   the entire page sideways by the scrollbar's width right as the menu
   opens — most noticeable on mobile Chrome/Firefox where that's a visible
   jump, not just a cosmetic gutter. */
html {
  scrollbar-gutter: stable;
}

body {
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  background: var(--color-background);
  color: var(--color-text);
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.008em;
}
h1 { font-size: 2rem; line-height: 1.15; }
h2 { font-size: 1.28rem; line-height: 1.3; }

/* Small uppercase context label above a heading ("ONBOARDING" above "Get
   started") — muted by default so it reads as quiet orientation, not a
   second headline; colored accent only inside a featured/hero context
   (the two places this system spends its one accent color on copy). */
.eyebrow {
  display: block;
  margin: 0 0 var(--spacing-xs);
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.card-featured .eyebrow,
.hero .eyebrow {
  color: var(--color-accent);
}

/* Pico only styles the bare `article` element (background/padding/radius,
   no border) — this app uses `class="card"` on both <article> (auth shell)
   and <section> (contacts/domains), so it needs its own real definition
   rather than relying on that. Without this, a card had no border and a
   background identical to the page behind it — effectively invisible. */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  padding: var(--spacing-lg);
}

/* The one onboarding-critical card per page (Dashboard's "Get started") —
   a slightly heavier register than a plain card: a warm parchment tint, a
   3px accent rule along the top, and the cut top-right corner otherwise
   reserved for .button-primary, so the single most important card on a
   new account's dashboard visibly outranks the plain stat cards beside it. */
.card-featured {
  background: var(--color-surface-alt);
  border-top: 3px solid var(--color-accent);
  clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
}

/* A card holding one narrow, single-column form (Settings, Contact detail,
   "Ready to send") — without this the border stretches to the full
   app-shell width while the form inside stays capped at 420px, leaving a
   big dead gap on the right on any normal desktop width. Cards holding
   something that actually needs the width (a data table, an email preview)
   skip this modifier. */
.card-narrow {
  max-width: var(--container-narrow);
}

/* Replaces the repeated inline style="margin-bottom: 24px;" that used to be
   hand-copied onto every standalone <section class="card"> across
   contacts/show, campaigns/show, settings/index. */
.card-spaced {
  margin-bottom: var(--spacing-xl);
}

/* Contacts page: a plain <input type=file> renders its browse button and
   "No file chosen" text inline with no reliable cross-browser way to
   restyle either — so the native input is visually hidden and swapped for
   a bordered tile with an icon + label (a real label, so it opens the
   picker natively with no JS needed). .file-field needs position:relative
   — otherwise the absolutely positioned input escapes to the nearest
   positioned ancestor (or the page root) and its native default size
   (Pico gives type=file the same width:100% as any other input, unlike
   checkbox/radio, which the !importants below correct) can end up
   overlapping unrelated content instead of staying pinned to its own tile. */
.file-field {
  position: relative;
}
.file-field-input {
  position: absolute;
  opacity: 0;
  width: 1px !important;
  height: 1px !important;
}
.file-field-button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-xs);
  width: 100%;
  height: 108px;
  margin-bottom: var(--spacing-md);
  border: 1.5px dashed var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface-alt);
  color: var(--color-text);
  cursor: pointer;
  transition: background 150ms ease, border-color 150ms ease, transform 150ms ease;
}
.file-field-icon {
  width: 24px;
  height: 24px;
  color: var(--color-text-muted);
  transition: color 150ms ease;
}
.file-field-text {
  font-size: 0.85rem;
  font-weight: 600;
}
.file-field-button:hover {
  background: var(--color-surface);
  border-color: var(--color-accent);
}
.file-field-button:hover .file-field-icon {
  color: var(--color-accent);
}
/* A file is chosen — swap the dashed "drop zone" border for a solid
   confirmed one, so the tile itself signals success rather than relying
   on the filename text alone. */
.file-field-button-filled {
  border-style: solid;
  border-color: var(--color-success);
}
.file-field-button-filled .file-field-icon {
  color: var(--color-success);
}
.file-field-input:focus-visible + .file-field-button {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Contacts page: "Import from CSV" and "Signup page" side by side — two
   unrelated, similarly-sized actions that don't need the full page width
   each. Collapses to stacked below 640px rather than squeezing both thin. */
.contacts-top-row {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-xl);
  margin-bottom: 24px;
}
.contacts-top-row > section {
  flex: 1;
  min-width: 0;
}
@media (max-width: 640px) {
  .contacts-top-row {
    flex-direction: column;
    /* align-items:flex-start (set above for row layout, where it controls
       vertical alignment) would otherwise shrink each card to its own
       content width once the axis flips to column — stretch so they fill
       the viewport instead of overflowing it. */
    align-items: stretch;
  }
}

/* The two links off the Contacts page's "Signup page" section — same icons
   as the public subscribe page itself, so the two clearly read as a pair.
   The whole tile is a link, so — unlike the static cards around it — it
   earns a hover lift: a real affordance for a real click target. */
.signup-links {
  display: flex;
  gap: var(--spacing-md);
  flex-wrap: wrap;
}
.signup-link {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  flex: 1 1 240px;
  min-width: 0;
  padding: var(--spacing-md);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  color: var(--color-text);
  text-decoration: none;
  transition: background 150ms ease, transform 150ms ease, box-shadow 150ms ease;
}
.signup-link:hover {
  background: var(--color-surface-alt);
  transform: translateY(-2px);
  box-shadow: var(--shadow-sm);
}
.signup-link svg {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
}
.signup-link span {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.signup-link small {
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */

.app-shell { max-width: var(--container-xl); margin: 0 auto; padding: 0 16px; }

/* Visually hidden until keyboard-focused — lets a keyboard user jump past
   the brand + four nav links + account menu straight to the page content
   instead of tabbing through the whole topbar on every single page. */
.skip-link {
  position: absolute;
  top: -40px;
  left: 8px;
  z-index: 100;
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--color-primary);
  color: var(--color-primary-inverse);
  text-decoration: none;
  transition: top 120ms ease;
}
.skip-link:focus {
  top: 8px;
}

/* Three-column grid rather than flex space-between — with exactly two
   fixed-width ends (brand, account trigger) and nav in between, a grid's
   auto/1fr/auto tracks center that middle column in the *whole* bar
   regardless of how wide the brand or the account chip are; space-between
   only guarantees equal gaps around it, which isn't the same as centered
   when the two ends differ in width (they do here). At <640px the middle
   column has no visible content (.tab-bar is display:none there) and
   collapses to zero width, so brand/account still land at the two ends. */
.topbar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-md) 0;
  border-bottom: 2px solid var(--color-border-strong);
}
.topbar-brand {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--color-text);
  text-decoration: none;
}
.brand-mark { width: 30px; height: 24px; color: var(--color-text); flex-shrink: 0; }
/* The one spot of brand color on an otherwise ink-only mark — a wax seal
   set into the crown (see views/_brand-mark.ejs). Stays visible against
   both themes since it's an independent accent token, not tied to the
   surrounding ink. */
.brand-mark-seal,
.hero-mark .brand-mark-seal,
.public-footer-mark .brand-mark-seal {
  fill: var(--color-accent);
}

/* Primary nav — Dashboard/Campaigns/Contacts as an inline row on wider
   screens (.tab-bar); below 640px that row is hidden and the same three
   links reappear inside the account menu's dropdown instead (see
   .account-dropdown-mobile-only) — one trigger at every width, rather than
   a separate hamburger plus a separate avatar menu. */
.topbar-actions {
  display: flex;
  align-items: center;
  justify-self: end;
  gap: var(--spacing-sm);
}

.tab-bar {
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-lg);
  min-width: 0;
}
.tab-bar-link {
  padding: var(--spacing-xs) 0;
  border-bottom: 2px solid transparent;
  color: var(--color-text-muted);
  font-weight: 600;
  font-size: 0.9rem;
  text-decoration: none;
  transition: color 120ms ease, border-color 120ms ease;
  white-space: nowrap;
}
.tab-bar-link:hover { color: var(--color-text); }
/* Active tab is the one place primary navigation itself carries the brand
   color — orientation ("where am I") tied to the same accent as "what can
   I commit to", not spent anywhere else in the chrome. */
.tab-bar-link-active {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}
@media (min-width: 640px) {
  .tab-bar {
    display: flex;
  }
  /* Already visible in .tab-bar at this width — no reason to also list
     Dashboard/Campaigns/Contacts inside the account dropdown. */
  .account-dropdown-mobile-only {
    display: none;
  }
}

/* The account menu overlays page content rather than pushing it down (a
   <details> disclosure has no room to push anything), which on a short
   page can mean it sits directly over the h1 that says what page you're
   even on — more likely now that it's carrying the full nav too on
   mobile. A scrim behind it makes that read as "a menu is open on top of
   the page" rather than "the page title disappeared" — :has() lets the
   scrim live on <body> without any JS. */
body:has(.account-menu[open])::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 15;
  background: rgba(27, 24, 18, 0.32);
}

/* Account menu — plain <details>/<summary>, no JS beyond the theme switch.
   On mobile it's every destination in the app (nav + Settings + theme +
   log out); on desktop the tab bar already covers Dashboard/Campaigns/
   Contacts, so this holds just Settings plus account-level actions. */
.account-menu { position: relative; z-index: 16; margin-bottom: 0; }
.account-menu summary {
  list-style: none;
  cursor: pointer;
  margin-bottom: 0;
}
.account-menu[open] > summary { margin-bottom: 0; }
.account-menu summary::-webkit-details-marker { display: none; }

/* Circular, not square — the avatar inside it is a circle, so the trigger
   echoes that (nested-radius: a rounded child wants a rounded parent, not
   a sharp-cornered box with dead space in the corners). The one deliberate
   exception to this system's otherwise-sharp corner language, same as the
   avatar and the theme switch. */
.account-trigger {
  display: flex;
  align-items: center;
  padding: 3px;
  border: 1px solid var(--color-border-strong);
  border-radius: 50%;
  background: var(--color-surface);
  transition: background 120ms ease;
}
.account-trigger:hover { background: var(--color-surface-alt); }

.account-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-primary-inverse);
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 700;
}

.account-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + var(--spacing-xs));
  z-index: 20;
  min-width: 200px;
  margin: 0;
  padding: var(--spacing-xs);
  list-style: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  /* This is a genuinely floating layer above page content — the one place
     besides the link popover that earns real elevation. */
  box-shadow: var(--shadow-sm);
}
.account-dropdown-email {
  padding: var(--spacing-sm);
  padding-top: var(--spacing-xs);
  font-size: 0.82rem;
  color: var(--color-text-muted);
  overflow-wrap: break-word;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--spacing-xs);
}
.account-dropdown li { list-style: none; }
.account-dropdown li + li { margin-top: 2px; }
.account-dropdown a,
.account-dropdown .link-button {
  display: block;
  width: 100%;
  padding: var(--spacing-sm);
  border: none;
  border-radius: var(--radius);
  background: none;
  color: var(--color-text);
  font-size: 0.88rem;
  font-family: inherit;
  font-weight: 400;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.account-dropdown a:hover,
.account-dropdown .link-button:hover {
  background: var(--color-surface-alt);
}
.account-dropdown a.nav-link-active {
  font-weight: 700;
}
.account-dropdown form { max-width: none; margin: 0; }
.account-dropdown-divider {
  height: 1px;
  margin: var(--spacing-xs) 0;
  background: var(--color-border);
  border: none;
}

.theme-switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm);
  font-size: 0.85rem;
  color: var(--color-text);
}
label.switch {
  position: relative;
  display: inline-block;
  width: 2.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}
.switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.switch-track {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--color-border-strong);
  border-radius: 999px;
  opacity: 0.35;
  transition: opacity 120ms ease;
}
.switch-track::before {
  content: "";
  position: absolute;
  width: 1rem;
  height: 1rem;
  left: 0.125rem;
  top: 0.125rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: 50%;
  transition: transform 120ms ease;
}
.switch input:checked ~ .switch-track { opacity: 1; }
.switch input:checked ~ .switch-track::before { transform: translateX(1rem); }
.switch input:focus-visible ~ .switch-track {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.logout-form { margin: 0; }

.content { padding: var(--spacing-xl) 0 var(--spacing-3xl); }

/* Only used by the logged-out landing page today (see app.ts) — width
   matches .hero below so that isn't squeezed narrower by this wrapping it;
   no vertical margin here since .hero supplies its own top/bottom padding
   (stacking both used to double up into a huge, lopsided gap above the
   headline). */
.container { max-width: var(--container-lg); margin: 0 auto; padding: 0 20px; }

/* --------------------------------------------------------------------------
   5. Buttons / forms
   -------------------------------------------------------------------------- */

button, [type="submit"], [type="button"] {
  width: auto;
  font-weight: 600;
  transition: background 150ms ease, border-color 150ms ease, color 150ms ease, transform 100ms ease, box-shadow 150ms ease;
}
/* Tactility — a button should feel pressable, not just clickable: it lifts
   a hair on hover and settles back down on press (industrial design's
   "tactility" principle). */
button:not(.link-button):hover,
[type="submit"]:hover,
.button:hover {
  transform: translateY(-1px);
}
button:not(.link-button):active,
[type="submit"]:active,
.button:active {
  transform: translateY(0);
}

/* min-height/width rather than padding — these sit inline with body text
   (pagination, "Resend confirmation email") where visible padding would
   throw off that alignment, but WCAG 2.5.8 still wants a 24px hit target. */
.link-button {
  background: none;
  border: none;
  min-width: 24px;
  min-height: 24px;
  color: var(--color-text-muted);
  font-weight: 400;
  padding: 0;
}
.link-button:hover { background: none; color: var(--color-text); }

/* A plain <a> styled as the same solid CTA as a submit button — for the
   primary action on an index page (e.g. "New campaign"), which is a
   navigation link, not a form submit, but should carry the same visual
   weight as one rather than reading as a quiet inline text link. Ink by
   default; .button-primary (below) is the accent-colored escalation. */
.button {
  display: inline-block;
  padding: 9px 18px;
  border: 1px solid var(--color-primary);
  border-radius: var(--radius);
  background: var(--color-primary);
  color: var(--color-primary-inverse);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  transition: background 150ms ease, border-color 150ms ease, color 150ms ease, transform 100ms ease, box-shadow 150ms ease;
}
.button:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  color: var(--color-primary-inverse);
  box-shadow: var(--shadow-sm);
}

/* The signet-red escalation — reserved for the handful of true "commit"
   moments (see the token comment at the top of this file): starting or
   sending a campaign, and the logged-out conversion CTA. The cut top-right
   corner is this system's one signature device, otherwise seen only on
   .card-featured — it marks these specific controls as a different kind
   of action, not just a bigger button. */
.button-primary,
button.button-primary,
[type="submit"].button-primary {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-inverse);
  clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
}
.button-primary:hover {
  background: var(--color-accent-hover);
  border-color: var(--color-accent-hover);
  color: var(--color-accent-inverse);
  box-shadow: var(--shadow-sm);
}

/* Larger CTA register — landing hero's CTA is the only user, was
   previously a near-duplicate .hero-cta rule with slightly bigger padding. */
.button-lg {
  padding: 11px 22px;
  font-size: 1rem;
}

/* Destructive actions (e.g. deleting a contact) — outlined rather than
   solid-filled, so it doesn't carry as much visual weight as the page's
   actual primary action, and a distinct oxblood rather than the brighter
   signet red so "commit" and "destroy" never share a hue at a glance. */
.button-danger {
  background: var(--color-surface);
  border-color: var(--color-danger);
  color: var(--color-danger);
}
.button-danger:hover {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: var(--color-primary-inverse);
}

.manage-actions {
  display: flex;
  gap: var(--spacing-md);
}
.manage-actions form { margin: 0; }

.checkbox-label {
  flex-direction: row;
  align-items: center;
  gap: var(--spacing-sm);
}

/* The one checkbox in the app that gates an irreversible action (sending a
   campaign) — a quiet parchment-and-accent-hairline panel around it so the
   confirmation reads as a deliberate checkpoint, not just another form row. */
.send-confirm {
  padding: var(--spacing-md);
  margin-bottom: var(--spacing-md);
  background: var(--color-surface-alt);
  border-left: 3px solid var(--color-accent);
}
.send-confirm .checkbox-label {
  margin: 0;
}

.inline-form {
  display: inline;
  margin: 0;
}

/* A small bordered pill rather than bare uppercase text — reads as a step
   badge (à la a boarding-pass stub) instead of a caption line. */
.step-indicator {
  display: inline-block;
  padding: 3px var(--spacing-sm);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--spacing-md);
}

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--spacing-sm) var(--spacing-md);
  margin-bottom: var(--spacing-lg);
}
.page-header h1, .page-header h2 { margin: 0; }
.contacts-search {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--spacing-sm);
  width: auto;
  max-width: none;
  margin-left: auto;
}
/* One bordered, flush-jointed control (icon + input + submit) instead of a
   borderless input floating next to a disconnected text button — reads as
   a single search field the way the rest of the form language does. */
/* A fixed height, not sized by the input's own content box — Chromium's
   intrinsic height for a text input doesn't track its CSS line-height the
   way a normal block element would (it comes out taller than padding +
   line-height alone would predict), so leaving height to "whatever the
   input wants" made the field itself tall and loose, and the button
   (stretched to match) inherited that same excess. Pinning it here gives
   every child a single, predictable height to align to instead. */
.contacts-search-field {
  display: flex;
  align-items: center;
  height: 40px;
  width: 380px;
  max-width: 100%;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  transition: border-color 120ms ease;
}
.contacts-search-field:focus-within {
  border-color: var(--color-focus);
}
.contacts-search-icon {
  width: 16px;
  height: 16px;
  margin-left: var(--spacing-sm);
  color: var(--color-text-muted);
  flex-shrink: 0;
}
/* A plain text input, not type=search — WebKit/Blink give search fields
   their own native rounded decoration and focus ring that survives
   appearance:none and clashes with this app's square, borderless look.
   Pico styles every text input via `input:not([type=checkbox],...)`,
   whose :not() specificity (0,1,0,1) beats a plain class like
   `.contacts-search-input` (0,1,0,0) — so its own font-size/padding won
   over these unless the selector is qualified to match or exceed that
   specificity, which is what the parent-scoped selector below does
   instead of a bare class (this was silently losing the fight and
   collapsing the input to ~40px wide inside its 260px field until this
   was measured and fixed). */
.contacts-search-field .contacts-search-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0 var(--spacing-sm) 0 var(--spacing-xs);
  border: none;
  border-radius: 0;
  background: transparent;
  font-size: 0.9rem;
}
.contacts-search-input:focus {
  box-shadow: none;
}
/* The global focus-visible rule uses a positive outline-offset, which is
   fine floating in open space but here pokes outward into the icon and
   the button's divider since the input sits flush between them with no
   room to spare — inset it instead so the ring stays inside the field's
   own border, and let :focus-within on the field (above) carry the
   "this whole control is focused" signal instead of double-ringing it. */
.contacts-search-input:focus-visible {
  outline-offset: -2px;
}
/* Pico also sets `button[type=submit] { width: 100% }`, which — because
   this button has no explicit flex-basis of its own — becomes ITS
   flex-basis, so it claimed the field's full width before shrinking,
   squeezing the input beside it down to a few px. `width: auto` here
   needs the same parent-scoped specificity bump as the input above to
   actually beat that Pico rule. */
.contacts-search-field .contacts-search-submit {
  /* Pico gives every [type=submit] `margin-bottom: var(--pico-spacing)`
     (its normal block-form vertical rhythm) — as a flex item, that
     margin ate into this button's cross-axis box, leaving it short and
     pushed toward the top instead of filling the field's height. Zeroing
     it is the actual fix; height/box-sizing here just make the resulting
     full-height box explicit rather than relying on align-self:stretch,
     which form controls resist in Chromium regardless of this margin. */
  margin: 0;
  height: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;
  flex-shrink: 0;
  padding: 0 var(--spacing-md);
  font-size: 0.85rem;
  white-space: nowrap;
  color: var(--color-text-muted);
  border-left: 1px solid var(--color-border);
}
.contacts-search-submit:hover {
  color: var(--color-text);
  background: var(--color-surface-alt);
}
@media (max-width: 640px) {
  /* Search first (full width), contact count below it — the fixed-width
     search box would otherwise be squeezed down to nothing next to the
     count on a narrow screen. column-reverse rather than reordering the
     markup, since the count should still come first for a screen reader. */
  .contacts-list-header {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  .contacts-list-header .contacts-search,
  .contacts-list-header .contacts-search-field {
    width: 100%;
    margin-left: 0;
  }
}

form { max-width: var(--container-sm); }

/* Tags — square badges/chips matching the app's border-and-surface-alt
   language rather than colored pills, since the design system is
   deliberately restrained. */
.tag-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: 2px var(--spacing-sm);
  margin: 0 var(--spacing-xs) var(--spacing-xs) 0;
  border: 1px solid var(--color-border);
  background: var(--color-surface-alt);
  font-size: 0.8rem;
  white-space: nowrap;
}
.tag-badges { margin-bottom: var(--spacing-md); }
.tag-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  background: none;
  border: none;
  padding: 0;
  margin: 0 calc(var(--spacing-xs) * -1);
  font-weight: 400;
  line-height: 1;
  color: var(--color-text-muted);
}
.tag-remove:hover { color: var(--color-danger); background: none; }
.tag-add-form {
  display: flex;
  gap: var(--spacing-sm);
  max-width: none;
}
.tag-add-form input { margin: 0; }

.tag-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-md);
}
.tag-chip {
  padding: 2px var(--spacing-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.8rem;
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.tag-chip:hover { background: var(--color-surface-alt); }
.tag-chip-active {
  border-color: var(--color-border-strong);
  background: var(--color-primary);
  color: var(--color-primary-inverse);
}

.contact-row-unconfirmed { color: var(--color-text-muted); }
.contact-row-unconfirmed a { color: inherit; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-md);
  margin-top: var(--spacing-md);
}
.pagination-status { color: var(--color-text-muted); font-size: 0.85rem; }

/* Campaign composer needs more room than a typical narrow form — a
   toolbar plus an email body, not a couple of single-line fields. */
#campaign-form { max-width: var(--container-md); }

/* Campaign composer — one contenteditable body with a small execCommand
   toolbar (see public/campaign-editor.js), replacing what used to be two
   separate html/plain-text textareas. Styled to match Pico's own
   input/textarea borders so it doesn't read as a different kind of control. */
.editor {
  border: var(--pico-border-width) solid var(--pico-form-element-border-color);
  border-radius: var(--radius);
  margin-bottom: var(--pico-spacing);
}
.editor-toolbar {
  display: flex;
  gap: 2px;
  padding: var(--spacing-xs);
  background: var(--color-surface-alt);
  border-bottom: var(--pico-border-width) solid var(--pico-form-element-border-color);
  /* On a phone-width viewport the full button row (icons plus the text
     labels on Link/Button/Image) is wider than the screen — scroll it
     horizontally instead of letting flex squeeze buttons until their
     labels overlap and clip past the edge. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.editor-toolbar button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  margin: 0;
  min-width: 32px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-text);
  font-size: 0.9rem;
  font-weight: 400;
  cursor: pointer;
  flex: 0 0 auto;
  white-space: nowrap;
}
.editor-toolbar button svg {
  width: 18px;
  height: 18px;
}
.editor-toolbar button:hover {
  background: var(--color-surface);
  border-color: var(--color-border);
}
/* Active-state feedback (bold/italic/list currently on at the caret) — set
   via document.queryCommandState in campaign-editor.js on selectionchange.
   Inverted colors rather than a border, matching .tag-chip-active. */
.editor-toolbar button.is-active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-inverse);
}
.editor-toolbar button.is-active:hover {
  background: var(--color-primary-hover);
}
.editor-body {
  min-height: 240px;
  padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
  outline: none;
}
.editor-body:empty::before {
  content: attr(data-placeholder);
  color: var(--color-text-muted);
}
.editor-body:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
}
.editor-body p { margin: 0 0 var(--spacing-sm); }
.editor-body h1, .editor-body h2, .editor-body h3 { margin: 0 0 var(--spacing-sm); }

/* A plain <div>, not <p> — the sent HTML can contain block-level tags
   (<p>, <ul>, <div>) from the editor, which would otherwise close a <p>
   wrapper early and break the preview's layout. */
.email-preview {
  padding: var(--spacing-md);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
}
.email-preview p { margin: 0 0 var(--spacing-sm); }
.email-preview h1, .email-preview h2, .email-preview h3 { margin: 0 0 var(--spacing-sm); }
.email-preview img { max-width: 100%; }

/* Draft-time preview width toggle (public/campaign-editor.js) — a plain
   max-width swap, not real device emulation, since the goal is just to
   catch obviously-too-wide layouts before sending. */
.preview-toggle {
  display: flex;
  gap: var(--spacing-xs);
  margin-bottom: var(--spacing-sm);
}
.preview-toggle button {
  width: auto;
  margin: 0;
  padding: 4px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.preview-toggle button.is-active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-inverse);
}
.email-preview.is-mobile {
  max-width: 375px;
  margin: 0 auto;
}

/* Inline popover for inserting/editing/removing a link — replaces
   window.prompt (no validation, no way to edit/remove afterward, and
   blocked outright in some embedding contexts). Positioned in normal flow
   right under the toolbar rather than absolutely, so it never overlaps the
   editor body it's editing a selection within. */
.editor-link-popover {
  display: flex;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm);
  border-bottom: var(--pico-border-width) solid var(--pico-form-element-border-color);
  background: var(--color-surface-alt);
}
.editor-link-popover[hidden] { display: none; }
.editor-link-popover input {
  flex: 1;
  min-width: 0;
  margin: 0;
}
.editor-link-popover button {
  width: auto;
  margin: 0;
  padding: 4px 10px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.editor-link-popover button:hover { background: var(--color-surface-alt); }

/* Same popover pattern, for image insertion. */
.editor-image-popover {
  display: flex;
  gap: var(--spacing-xs);
  padding: var(--spacing-sm);
  border-bottom: var(--pico-border-width) solid var(--pico-form-element-border-color);
  background: var(--color-surface-alt);
}
.editor-image-popover[hidden] { display: none; }
.editor-image-popover input {
  flex: 1;
  min-width: 0;
  margin: 0;
}
.editor-image-popover button {
  width: auto;
  margin: 0;
  padding: 4px 10px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.editor-image-popover button:hover { background: var(--color-surface-alt); }

/* Dashboard — three at-a-glance cards (contacts / sending identity / last
   campaign), stacked on narrow viewports since none of them need to be
   side-by-side to be useful. */
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--spacing-lg);
}
.dashboard-stat {
  margin: 0;
  font-family: var(--font-display);
  font-size: 2.25rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.dashboard-stat-label {
  margin: 0 0 var(--spacing-md);
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

/* The one primary action on a brand-new dashboard — the first unfinished
   onboarding step, promoted out of the checklist so there's a single
   obvious next click instead of the checklist's three links plus three
   more "View X" links below competing for attention. */
.dashboard-next-step {
  display: inline-block;
  margin: var(--spacing-sm) 0 var(--spacing-lg);
}

/* While onboarding is incomplete, the stat cards' own links have nothing
   meaningful behind them yet (0 contacts, no domain, no campaigns) — kept
   as plain quiet text rather than the usual link-button arrow treatment,
   so they don't visually compete with .dashboard-next-step above. */
.dashboard-quiet-link {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  text-decoration: underline;
  text-decoration-color: var(--color-border);
}
.dashboard-quiet-link:hover {
  color: var(--color-text);
}

/* A week-over-week delta reads as "movement", which a static total doesn't
   — kept in the ink/muted register rather than a success-green badge,
   since more contacts isn't unambiguously "good" the way e.g. a lower
   bounce rate is (a big spike is often a CSV import, not organic growth). */
.dashboard-delta {
  font-weight: 600;
  color: var(--color-text);
}

/* Things that need a decision, not just a number to glance at — sits above
   the static stat grid so it reads first. Left-accented like .flash rather
   than boxed per item, so several items read as one prioritized list. */
.attention-card {
  border-left: 3px solid var(--color-accent);
}
.attention-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.attention-list li + li {
  margin-top: var(--spacing-xs);
}
.attention-list a {
  color: var(--color-text);
}

/* Four rates in a row on a wide card, stacked in pairs once the card
   itself gets narrow (dashboard-grid's own minmax(220px,1fr) tracks can
   get tight) — a <dl> rather than a table since these are labelled values,
   not tabular records with equal weight across rows. */
.performance-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: var(--spacing-md);
  margin: var(--spacing-sm) 0 0;
}
.performance-grid dt {
  color: var(--color-text-muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.performance-grid dd {
  margin: 2px 0 0;
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.checklist {
  list-style: none;
  margin: 0;
  padding: 0;
}
.checklist li {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) 0;
  color: var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
}
.checklist li:last-child { border-bottom: none; }
/* A real circular badge instead of a bare ✓/○ character — empty rings for
   what's left, a filled accent seal for what's done (the checklist's own
   small nod to the wax-seal motif: each finished step gets "sealed").
   align-items:flex-start (above) + a small top margin here keeps the badge
   aligned with the first line of its label if that label wraps. */
.checklist-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.2rem;
  height: 1.2rem;
  margin-top: 2px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1.5px solid var(--color-border-strong);
  font-size: 0.7rem;
  line-height: 1;
  color: transparent;
}
.checklist-done .checklist-icon {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-inverse);
}
.checklist-done { color: var(--color-text); }

/* --------------------------------------------------------------------------
   6. Tables — dense, tabular figures for campaign stats
   -------------------------------------------------------------------------- */

/* Wraps any table wide enough to overflow a narrow viewport (campaign
   progress, DNS records, contacts list) — scrolls horizontally instead of
   squeezing columns unreadably or breaking the page's own layout. */
.table-scroll {
  overflow-x: auto;
}
table { font-size: 0.9rem; }
th, td { font-variant-numeric: tabular-nums; }
thead th {
  color: var(--color-text-muted);
  font-weight: 600;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-bottom: 2px solid var(--color-border-strong);
}
tbody tr:hover {
  background: var(--color-surface-alt);
}
/* A single-cell "no rows" message spanning the whole table reads as a
   broken grid unless it's given real room and centered treatment — this
   is every list page's empty state (campaigns, contacts). */
tbody tr:has(td[colspan]) {
  background: none;
}
tbody tr:has(td[colspan]):hover {
  background: none;
}
td[colspan] {
  padding: var(--spacing-xl) var(--spacing-md);
  text-align: center;
  color: var(--color-text-muted);
  font-style: italic;
}

/* --------------------------------------------------------------------------
   7. Flash — quiet left-accent banner
   -------------------------------------------------------------------------- */

.flash {
  padding: var(--spacing-sm) var(--spacing-md);
  margin: 0 0 var(--spacing-lg);
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-text-muted);
  background: var(--color-surface-alt);
  font-size: 0.9rem;
}
.flash-success { border-left-color: var(--color-success); }
.flash-error { border-left-color: var(--color-danger); }

/* --------------------------------------------------------------------------
   8. Auth shell
   -------------------------------------------------------------------------- */

.auth-shell {
  max-width: var(--container-sm);
  margin: 10vh auto;
  padding: 0 16px;
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-lg);
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--color-text);
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   9. Landing hero
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  padding: 14vh 0 10vh;
  overflow: hidden;
}
/* One large, quiet watermark of the brand mark — the "reserve impact for
   punctuation" hero moment this system allows itself once. A mask-image
   (not a plain background/opacity image) so it correctly follows
   --color-text into dark mode instead of needing a second dark asset. */
.hero::before {
  content: "";
  position: absolute;
  top: 4%;
  right: -4%;
  width: 340px;
  height: 272px;
  background-color: var(--color-text);
  opacity: 0.05;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 32'%3E%3Cpolygon points='5,27 5,20 9,8 15,15 20,6 25,15 31,8 35,20 35,27'/%3E%3Ccircle cx='20' cy='16' r='2.25'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 32'%3E%3Cpolygon points='5,27 5,20 9,8 15,15 20,6 25,15 31,8 35,20 35,27'/%3E%3Ccircle cx='20' cy='16' r='2.25'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  pointer-events: none;
}
.hero-content {
  position: relative;
}
.hero-mark { width: 64px; height: 52px; margin-bottom: var(--spacing-lg); color: var(--color-text); }
.hero h1 { font-size: 3rem; line-height: 1.08; max-width: 11ch; }
.hero p {
  font-size: 1.05rem;
  color: var(--color-text-muted);
  max-width: 46ch;
  margin: var(--spacing-md) 0 var(--spacing-xl);
}
@media (max-width: 640px) {
  .hero { padding: 8vh 0 6vh; }
  .hero::before { width: 150px; height: 120px; top: 0; right: 0; opacity: 0.06; }
  .hero h1 { font-size: 2.1rem; }
}

/* --------------------------------------------------------------------------
   10. Public subscribe page — standalone (no app chrome), reachable by
   anyone with the link or QR code, no account required.
   -------------------------------------------------------------------------- */

.public-shell {
  max-width: var(--container-md);
  margin: 0 auto;
  padding: 10vh 20px 8vh;
}
.subscribe-page { max-width: var(--container-xs); margin: 0 auto; text-align: center; }
.subscribe-page .hero-mark { width: 48px; height: 39px; margin: 0 auto var(--spacing-lg); color: var(--color-text); }
.subscribe-page h1 { font-size: 1.9rem; line-height: 1.15; margin-bottom: var(--spacing-lg); }
.subscribe-page > p { color: var(--color-text-muted); margin: 0 0 var(--spacing-lg); }
.subscribe-page form { text-align: left; }

/* Fixed white/black regardless of theme — a QR code's scannability depends
   on that exact contrast and quiet zone, not on matching dark mode. */
.qr-frame {
  display: inline-block;
  padding: var(--spacing-sm);
  background: #ffffff;
}
.qr-frame svg { display: block; width: 160px; height: 160px; }
.qr-frame-large { padding: var(--spacing-lg); }
.qr-frame-large svg { width: 320px; height: 320px; }

/* "Powered by" line on every public (unauthenticated) page — layout-public.ejs */
.public-footer {
  margin-top: var(--spacing-2xl);
  text-align: center;
}
.public-footer a {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  color: var(--color-text-muted);
  font-size: 0.78rem;
  text-decoration: none;
}
.public-footer a:hover { color: var(--color-text); }
.public-footer-mark { width: 16px; height: 13px; color: currentColor; }

@media print {
  /* Printing a dark-mode page would waste ink and, worse, could print the
     QR as white-on-white — force the light palette regardless of the
     viewer's OS/theme preference. The footer is pointless on paper. */
  .subscribe-page {
    background: #ffffff;
    color: #000000;
  }
  .subscribe-page .hero-mark,
  .subscribe-page h1,
  .subscribe-page > p {
    color: #000000;
  }
  .public-footer {
    display: none;
  }
}
