/* ── Theme (#173) ───────────────────────────────────────
 *
 * **Theme the chrome. Never theme the code.** A QR must stay dark modules on a
 * light field -- inverting it breaks a large share of scanners, and contrast
 * ratio does not save it, because many decoders simply assume the polarity. So
 * dark mode darkens the page, the cards, the text and the controls, and the QR
 * keeps its own light field (see --code-field below).
 *
 * The sharper half of that rule is about *data*: `dotColor` and `bgColor` are
 * persisted user data that cross platforms. Nothing here is ever read into
 * them, so a code made in dark mode is not saved dark.
 *
 * Three blocks, in this order, and the order is the behaviour:
 *
 *   :root                     the light palette, and the default
 *   :root[data-theme=…]       an explicit choice, which beats the system
 *   @media prefers-color-…    the system, for everyone who has not chosen
 *
 * The media query is last and excludes an explicit `light`, so a visitor who
 * picks Light on a dark-mode OS gets light -- the toggle has to win in *both*
 * directions, which a bare media query cannot do. It carries its own copy of
 * the dark values because CSS cannot share one declaration block across a
 * media boundary; `tests/theme-tokens.test.js` asserts the two copies agree.
 *
 * `--accent` lightens in dark: #4F46E5 is a 6.3:1 read on white and a muted,
 * low-contrast one on a dark ground, so dark mode takes the soft variant --
 * and therefore needs `--on-accent` to darken, since white on #818CF8 is only
 * 3:1. Anything sitting on an accent fill must use it rather than `#fff`.
 */
:root {
  color-scheme: light;

  --ink:         #1E2430;
  --bg:          #F7F6F3;
  --surface:     #FFFFFF;
  --accent:      #4F46E5;
  --accent-soft: #818CF8;
  --on-accent:   #FFFFFF;
  --text:        #2B303B;
  --muted:       #6B7280;
  --line:        #E5E2DB;

  --header-bg:   rgba(247, 246, 243, 0.9);
  --shadow:      rgba(30, 36, 48, 0.12);

  /* Advisory ("you may want to reconsider"), blocking ("this is broken") and
     confirming. Each is a fill, a hairline and a text colour that have to be
     legible *together*, which is why they move as a set. */
  --warn-fg:     #92400E;
  --warn-bg:     #FEF3C7;
  --warn-line:   #FDE68A;
  --danger-fg:   #991B1B;
  --danger-bg:   #FEE2E2;
  --danger-line: #FECACA;
  --danger-ink:  #B91C1C;
  --success-fg:  #15803D;
  --success-bg:  #DCFCE7;

  /* The cookie banner is a deliberately inverted bar: dark ground, cream text,
     so it reads as an interruption rather than as more page. That intent does
     not invert with the theme -- it stays a dark bar in dark mode, just lifted
     off --surface enough to still separate from the page behind it. */
  --banner-bg:   #1E2430;
  --banner-fg:   #F5F7FA;
}

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

  --ink:         #F5F7FA;
  --bg:          #14181F;
  --surface:     #1E2430;
  --accent:      #818CF8;
  --accent-soft: #A5B4FC;
  --on-accent:   #14181F;
  --text:        #E8EAED;
  --muted:       #9BA3AF;
  --line:        #2E3542;

  --header-bg:   rgba(20, 24, 31, 0.9);
  /* Black rather than tinted ink: a shadow the colour of the light palette's
     ink is invisible on a dark ground, so the cards lose their lift. */
  --shadow:      rgba(0, 0, 0, 0.5);

  /* The light set is a tint-and-darken pair (Tailwind's 100 fill, 800 text).
     Dark inverts the relationship rather than dimming it: a deep desaturated
     fill with a *light* text, because a #FEF3C7 block on a #14181F page is a
     glare patch whichever text sits on it. */
  --warn-fg:     #FCD34D;
  --warn-bg:     #2B2210;
  --warn-line:   #7C5E10;
  --danger-fg:   #FCA5A5;
  --danger-bg:   #2C1618;
  --danger-line: #7F1D1D;
  --danger-ink:  #F87171;
  --success-fg:  #86EFAC;
  --success-bg:  #10261B;

  --banner-bg:   #252C3A;
  --banner-fg:   #F5F7FA;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --ink:         #F5F7FA;
    --bg:          #14181F;
    --surface:     #1E2430;
    --accent:      #818CF8;
    --accent-soft: #A5B4FC;
    --on-accent:   #14181F;
    --text:        #E8EAED;
    --muted:       #9BA3AF;
    --line:        #2E3542;

    --header-bg:   rgba(20, 24, 31, 0.9);
    --shadow:      rgba(0, 0, 0, 0.5);

    --warn-fg:     #FCD34D;
    --warn-bg:     #2B2210;
    --warn-line:   #7C5E10;
    --danger-fg:   #FCA5A5;
    --danger-bg:   #2C1618;
    --danger-line: #7F1D1D;
    --danger-ink:  #F87171;
    --success-fg:  #86EFAC;
    --success-bg:  #10261B;

    --banner-bg:   #252C3A;
    --banner-fg:   #F5F7FA;
  }
}

/* Constants: the same in both themes, and deliberately not in either palette.
 *
 * `--code-field` is the load-bearing one. It is the field the QR itself sits
 * on, and it is white in dark mode too -- #173's whole premise. Keeping it out
 * of the palettes is what makes "never theme the code" something you have to
 * *undo* rather than something you have to remember.
 *
 * The brand pair is fixed for a different reason: the auth panel is a
 * saturated indigo block by design, so its background and the text on it are a
 * closed system that the page's theme has no say in.
 */
html {
  --code-field:   #FFFFFF;
  --brand-indigo: #4F46E5;
  --brand-cream:  #F7F6F3;
  --max:          960px;
}

* {
  box-sizing: border-box;
}

/* Global safety net: any author rule setting `display` on an element (.cta,
   .field, .content-grid, .account-grid, etc.) otherwise wins over the
   browser's own `[hidden] { display: none }` UA-stylesheet rule at equal
   specificity, since author styles always beat the user-agent stylesheet
   regardless of selector weight -- silently un-hiding a `hidden` element.
   Hit this exact bug four separate times across four different classes
   before adding this; !important is the correct, standard fix for `[hidden]`
   specifically (not a general escape hatch) since nothing should ever need
   to override "this element is hidden" with a competing `display` value.
   The handful of existing single-class `[hidden]` overrides below are now
   redundant but harmless. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  font-family: "Inter", sans-serif;
  background-color: var(--bg);
  color: var(--text);
  line-height: 1.6;
  font-size: 15px;
}

h1, h2 {
  font-family: "Sora", sans-serif;
  line-height: 1.2;
  letter-spacing: -0.3px;
  margin: 0 0 0.75rem;
}

.container {
  width: min(100% - 2rem, var(--max));
  margin-inline: auto;
}

/* ── Header ─────────────────────────────────────────── */

.site-header {
  border-bottom: 1px solid var(--line);
  background: var(--header-bg);
  backdrop-filter: blur(8px);
  position: sticky;
  top: 0;
  /* Above .subnav. Its .subnav-dropdown children are position:relative, so
     they paint as positioned elements later in the DOM and would otherwise
     show through the mobile nav panel, which is absolutely positioned inside
     this header. Also keeps the sticky header over page content generally. */
  z-index: 300;
}

.site-header .container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 0;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
}

.brand-mark {
  width: clamp(30px, 3.5vw, 38px);
  height: clamp(30px, 3.5vw, 38px);
  flex-shrink: 0;
}

.brand-mark svg {
  width: 100%;
  height: 100%;
}

/* One rule rather than a dark-mode override, because in light mode it resolves
   to exactly the fills already written on the rects -- so there is nothing to
   duplicate under [data-theme] or the media query, and nothing to keep in step.
   The cutout follows the *page*, not the card: the mark sits on the header
   band, and a cutout frozen at cream would be a bright hole in a dark header.
   Scoped to .brand-mark so it leaves .auth-brand-mark alone -- that one is
   inverted for the fixed indigo panel and has no business following the page. */
.brand-mark .mark-outer,
.brand-mark .mark-core {
  fill: var(--accent);
}

.brand-mark .mark-cutout {
  fill: var(--bg);
}

.brand-text {
  font-family: "Sora", sans-serif;
  font-weight: 700;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  letter-spacing: -0.3px;
}

.w-primary { color: var(--ink); }
.w-accent  { color: var(--accent); }

.site-nav {
  display: flex;
  gap: 1.5rem;
}

.site-nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
}

.site-nav a:hover { color: var(--accent); }
.site-nav a[aria-current="page"] { color: var(--accent); }

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

/* Hamburger trigger -- hidden entirely on desktop, where the nav sits inline.
   See the mobile-chrome block at the end of this file for the panel it opens. */
.nav-toggle {
  display: none;
  background: none;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  padding: 0.5rem 0.6rem;
  cursor: pointer;
  line-height: 0;
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: var(--ink);
  content: "";
}

.nav-toggle-bars::before { transform: translateY(-6px); }
.nav-toggle-bars::after  { transform: translateY(4px); }

.nav-toggle[aria-expanded="true"] {
  border-color: var(--accent);
  background: var(--bg);
}

/* ── Subnav ──────────────────────────────────────────── */

.subnav {
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.subnav-inner {
  width: min(100% - 2rem, var(--max));
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 0.55rem 0;
  flex-wrap: wrap;
}

.subnav-dropdown {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.2rem;
}

.subnav-dropdown-label {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0.85;
}

.subnav-dropdown-label:hover { color: var(--accent); opacity: 1; }
.subnav-dropdown-label[aria-current="page"] { color: var(--accent); opacity: 1; }

.subnav-dropdown-toggle {
  background: none;
  border: none;
  padding: 0.1rem 0.2rem;
  font: inherit;
  font-size: 0.8rem;
  color: var(--muted);
  opacity: 0.85;
  cursor: pointer;
  line-height: 1;
}

.subnav-dropdown-toggle:hover { color: var(--accent); opacity: 1; }
.subnav-dropdown-toggle[aria-expanded="true"] { color: var(--accent); opacity: 1; }

.subnav-dropdown-menu {
  display: none;
  flex-direction: column;
  position: absolute;
  top: 100%;
  left: 0;
  /* The visual gap lives inside this element as padding (not a margin/offset
     outside it) so the mouse is still "hovering" this element the whole way
     from the label down into the menu — an outside gap would make
     :hover-to-open flicker shut before the menu is reached. */
  padding-top: 0.4rem;
  min-width: 230px;
  z-index: 200;
}

.subnav-dropdown-menu.open,
.subnav-dropdown:hover .subnav-dropdown-menu {
  display: flex;
}

.subnav-dropdown-menu-inner {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 0.35rem 0;
  box-shadow: 0 8px 24px var(--shadow);
}

.subnav-dropdown-menu a {
  display: block;
  padding: 0.45rem 1rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  opacity: 0.9;
}

.subnav-dropdown-menu a:hover { color: var(--accent); opacity: 1; }
.subnav-dropdown-menu a[aria-current="page"] { color: var(--accent); opacity: 1; }

/* Log Out is a <button>, not a link (it triggers Auth.signOut() rather than
   navigating) -- same visual treatment as the menu's <a> items above. */
.subnav-dropdown-menu button {
  display: block;
  width: 100%;
  border: none;
  background: none;
  text-align: left;
  padding: 0.45rem 1rem;
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  opacity: 0.9;
  cursor: pointer;
}

.subnav-dropdown-menu button:hover { color: var(--accent); opacity: 1; }

/* ── Appearance control (#173) ──────────────────────────
   A three-state radiogroup, mounted by theme.js into whatever carries
   `data-appearance-control` — today only the Appearance panel on
   /profile.html. Sized for a panel rather than a menu, so it reads as a
   setting alongside the other forms on that page. */
.appearance-control {
  margin-top: 0.75rem;
}

/* Wraps rather than squeezing: three segments at a readable size need about
   330px, and the account grid's column is narrower than that on a phone. */
.appearance-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.appearance-option {
  flex: 1 1 6rem;
  text-align: center;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--bg);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  /* 44px, so the segments are a comfortable tap target rather than three
     small ones sitting side by side. */
  min-height: 44px;
}

.appearance-option:hover {
  border-color: var(--accent-soft);
  color: var(--accent);
}

.appearance-option.selected {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.appearance-option.selected:hover {
  color: var(--on-accent);
}

.appearance-help {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0.55rem 0 0;
}

.appearance-note {
  font-size: 0.78rem;
  color: var(--muted);
  line-height: 1.5;
  margin: 0.45rem 0 0;
}

/* ── Header account dropdown ────────────────────────────
   Reuses .subnav-dropdown's structure/behavior (see nav.js's
   wireSubnavDropdown) but needs its own positioning and sizing: it sits at
   the far-right edge of the header, not the centered subnav bar, so the
   menu must open flush with the right edge of its trigger instead of the
   left (which would otherwise overflow off-screen); the label/toggle also
   need to match .site-nav's link sizing, not the smaller subnav-bar default. */
.nav-account-dropdown .subnav-dropdown-menu {
  left: auto;
  right: 0;
}

.site-nav .subnav-dropdown-label,
.site-nav .subnav-dropdown-toggle {
  font-size: 0.875rem;
}

.subnav-dropdown-placeholder {
  display: block;
  padding: 0.45rem 1rem;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--muted);
  opacity: 0.7;
  white-space: nowrap;
}

/* ── Hero ────────────────────────────────────────────── */

.hero {
  padding: 3.5rem 0 2.5rem;
}

.eyebrow {
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  font-size: 0.7rem;
  font-weight: 600;
  margin: 0 0 0.75rem;
}

h1 {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
}

.lead {
  max-width: 55ch;
  color: var(--muted);
  font-size: 0.95rem;
}

/* ── Tool ────────────────────────────────────────────── */

.tool {
  padding-bottom: 4rem;
}

.tool-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

/* Same grid-blowout fix as .content-grid's. The track sizings above already
   use minmax(0, 1fr), but that constrains the TRACK -- the ITEMS still default
   to min-width: auto, so a panel whose min-content is wider than the container
   stretched the column past it (288px container, 305.8px column) and pushed
   the generator page 2px wider than the viewport at 320px. Found by the
   mobile-overflow assertion in e2e/smoke.mjs. */
.tool-grid > * {
  min-width: 0;
}

/* ── Content pages (about, privacy) ──────────────────── */

.content-page {
  padding-bottom: 4rem;
}

.content-grid {
  display: grid;
  gap: 1.25rem;
  max-width: 720px;
}

/* Same fix as .cta[hidden]/.btn-secondary[hidden] elsewhere in this file --
   .content-grid's own `display: grid` otherwise wins over the browser's
   built-in [hidden] rule at equal specificity. */
.content-grid[hidden] {
  display: none;
}

/* Grid items default to `min-width: auto`, meaning the column is sized to the
   widest item's MIN-CONTENT -- so a single wide descendant (a .data-table with
   a min-width, a <pre>, a diagram) silently stretches the whole column past
   the container and puts the page into horizontal scroll on a phone. Setting
   min-width: 0 lets items shrink to the grid's actual width, after which the
   scroll containers below (.table-scroll, pre) do their job internally. */
.content-grid > * {
  min-width: 0;
}

.content-grid .panel p {
  margin: 0 0 0.75rem;
}

.content-grid .panel p:last-child {
  margin-bottom: 0;
}

.content-grid .panel ul {
  margin: 0 0 0.75rem;
  padding-left: 1.25rem;
}

.back-link {
  display: inline-block;
  margin-top: 1.5rem;
  font-size: 0.9rem;
}

.dynqr-detail-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin: 0;
}

.dynqr-detail-nav .back-link {
  margin-top: 1.5rem;
}

.breadcrumb-back {
  display: inline-block;
  margin-bottom: 0.75rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.breadcrumb-back:hover {
  color: var(--accent);
}

.footer-note {
  margin-top: 1.5rem;
  font-size: 0.9rem;
  color: var(--muted);
}

/* Help section (GitHub #62) -- real product screenshots embedded in a
   documentation panel. */
.help-screenshot {
  display: block;
  max-width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  margin: 0.75rem 0;
}

.help-search {
  width: 100%;
  max-width: 480px;
  font-size: 1rem;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  margin-bottom: 1.5rem;
}

.help-category h2 {
  margin-top: 2rem;
}

.help-category:first-of-type h2 {
  margin-top: 0;
}

.help-topic-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}

.help-topic-list a {
  font-weight: 600;
}

.help-topic-list p {
  margin: 0.15rem 0 0;
  color: var(--muted);
  font-size: 0.9rem;
}

/* ── Content-hub article furniture ───────────────────────
   Figures, legends, data tables, callouts and FAQ blocks used across
   _pages/qr-codes/**. The diagrams themselves are generated SVGs -- see
   build-diagrams.mjs. */

.figure {
  margin: 1.25rem 0;
}

.figure img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
}

.figure figcaption {
  margin-top: 0.55rem;
  font-size: 0.83rem;
  color: var(--muted);
  line-height: 1.5;
}

/* Numbered legend keying a diagram's coloured regions to real HTML text --
   the colour is never the only channel carrying the mapping. */
.figure-legend {
  list-style: none;
  margin: 0.9rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

@media (min-width: 620px) {
  .figure-legend {
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem 1.25rem;
  }
}

.figure-legend li {
  display: grid;
  grid-template-columns: 1.55rem 1fr;
  gap: 0.55rem;
  align-items: start;
  font-size: 0.86rem;
  line-height: 1.45;
}

.figure-legend .key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.55rem;
  height: 1.55rem;
  border-radius: 50%;
  background: var(--legend-color, var(--muted));
  color: var(--on-accent);
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1;
}

.figure-legend strong {
  display: block;
  color: var(--ink);
}

/* Wide tables must scroll inside their own box rather than pushing the page
   into horizontal scroll on a phone. */
.table-scroll {
  overflow-x: auto;
  margin: 1rem 0;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
}

.data-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 460px;
  font-size: 0.86rem;
}

.data-table th,
.data-table td {
  text-align: left;
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.data-table thead th {
  background: var(--bg);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  white-space: nowrap;
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

.data-table td strong {
  color: var(--ink);
}

/* Payload-format samples in the content hub. */
.content-grid code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 0.25rem;
  padding: 0.1em 0.32em;
  word-break: break-word;
}

.content-grid pre {
  margin: 0.6rem 0 1rem;
  padding: 0.85rem 1rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  overflow-x: auto;
  font-size: 0.82rem;
  line-height: 1.55;
}

.content-grid pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  word-break: normal;
}

.callout {
  margin: 1.1rem 0;
  padding: 0.85rem 1rem;
  border-left: 3px solid var(--accent);
  border-radius: 0 0.4rem 0.4rem 0;
  background: var(--bg);
  font-size: 0.9rem;
}

.callout p:last-child {
  margin-bottom: 0;
}

.callout .callout-label {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.3rem;
}

.callout-warn {
  border-left-color: var(--danger-ink);
}

.callout-warn .callout-label {
  color: var(--danger-ink);
}

/* Key-takeaways summary that opens the longer articles -- also the block most
   likely to be lifted as a featured snippet. */
.takeaways {
  margin: 0 0 1.25rem;
  padding: 1rem 1.15rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 0.6rem;
}

.takeaways h2 {
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.6rem;
}

.takeaways ul {
  margin: 0;
  padding-left: 1.15rem;
  font-size: 0.9rem;
}

.takeaways li {
  margin-bottom: 0.35rem;
}

.takeaways li:last-child {
  margin-bottom: 0;
}

/* On-page table of contents for the pillar pages. */
.toc {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.4rem;
  font-size: 0.9rem;
}

.toc a {
  text-decoration: none;
}

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

.faq-item + .faq-item {
  border-top: 1px solid var(--line);
  margin-top: 1rem;
  padding-top: 1rem;
}

.faq-item h3 {
  font-size: 0.95rem;
  margin: 0 0 0.4rem;
  color: var(--ink);
}

.faq-answer p:last-child {
  margin-bottom: 0;
}

/* Prev/next stepper at the foot of each article in a cluster. */
.cluster-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  margin-top: 1.5rem;
  font-size: 0.9rem;
}

.panel h3 {
  font-family: "Sora", sans-serif;
  font-size: 0.98rem;
  margin: 1.4rem 0 0.5rem;
  color: var(--ink);
}

.panel h3:first-child {
  margin-top: 0;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 1.5rem;
}

.panel h2 {
  font-size: 1.05rem;
  margin-bottom: 1.1rem;
}

/* Teaser list inside a pillar page's "Articles in this section" panel */
.panel article + article {
  border-top: 1px solid var(--line);
  margin-top: 1.1rem;
  padding-top: 1.1rem;
}

.panel article h3 {
  font-size: 0.95rem;
  margin: 0 0 0.4rem;
}

.field {
  display: block;
  margin-bottom: 1rem;
}

.field[hidden] {
  display: none;
}

.field-checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.field-checkbox input[type="checkbox"] {
  width: auto;
  margin: 0;
}

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

/* GitHub #51 -- two side-by-side datetime-local inputs (plus the "Ends
   (optional)" label) are wide enough that they can overflow the tool
   panel's own column at plenty of realistic desktop widths, not just on
   mobile -- scoped to the calendar fields specifically (not a .field-row
   breakpoint change) since every other .field-row pairing fits two-up
   fine at the same widths. */
[data-type-field="calendar"] .field-row {
  grid-template-columns: 1fr;
}

.field-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--muted);
  margin-bottom: 0.35rem;
}

/* Label with a trailing control (the contextual help dot) pushed to the
   right edge, rather than sitting immediately after the label text. */
.field-label--row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* Deliberately quiet: muted outline, no fill, and it only takes the accent
   on hover/focus. It sits above the type buttons on every page load, so
   anything louder would compete with the tool itself. 1.15rem keeps it
   under the 44px tap target guidance on purpose -- it's a secondary
   affordance next to a full-width button row, not a primary action -- so
   the padding-free box is widened by the surrounding label's line height. */
.help-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  transition: color 0.15s, border-color 0.15s;
}

.help-dot:hover,
.help-dot:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
}

/* Warnings link out to the page explaining the failure they describe. The
   amber body colour is kept (a blue link inside an amber block reads as a
   different message) and carried by an underline instead. */
.risk-warning a,
.field-error a {
  color: inherit;
  text-decoration: underline;
  font-weight: 500;
}

.risk-warning a:hover,
.field-error a:hover {
  text-decoration: none;
}

/* Every text-entry control, not just the two the generator happens to use.
   This listed `text` and `url` only, so the entire auth flow -- email and
   password on login, signup, forgot and reset -- and the event generator's
   date fields fell through to the browser's default input. On white that
   looked close enough that nobody caught it in four years; the moment the page
   went dark it was six unstyled boxes on every form that matters.

   An explicit list rather than `input:not([type=color]):not(…)`: the negation
   chain reads as an exclusion of the three special inputs but is really an
   inclusion of checkboxes, radios and submit buttons too, and it carries the
   specificity of everything it names. tests/theme-tokens.test.js walks the
   built pages and fails if a type appears in the markup and not here, which is
   the guard the old list was missing rather than the list being wrong. */
input:is([type="text"], [type="email"], [type="password"], [type="url"],
         [type="search"], [type="tel"], [type="number"], [type="date"],
         [type="time"], [type="datetime-local"]),
select,
textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
}

textarea {
  resize: vertical;
}

input:is([type="text"], [type="email"], [type="password"], [type="url"],
         [type="search"], [type="tel"], [type="number"], [type="date"],
         [type="time"], [type="datetime-local"]):focus,
select:focus,
textarea:focus {
  outline: 2px solid var(--accent-soft);
  outline-offset: 1px;
}

.type-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.type-toggle-item {
  position: relative;
  flex: 1;
  display: flex;
}

/* Same fix as .cta[hidden]/.btn-secondary[hidden] elsewhere in this file --
   this class's own `display: flex` otherwise wins over the browser's
   built-in [hidden] rule, so a hidden Gradient toggle item (free/registered
   tier, before entitlements resolve) would still render. */
.type-toggle-item[hidden] {
  display: none;
}

/* Grid instead of the shared .type-toggle's flex-wrap, specifically for the
   main type selector — flex-wrap stretches a lone item on a partial last
   row to fill the whole row (e.g. a 7th type wrapping alone becomes as
   wide as the other six combined). auto-fill reserves as many ~100px
   tracks as the container actually fits and keeps that column count fixed
   even when the last row has fewer real items than tracks, so a partial
   row just leaves empty trailing cells instead of stretching into them —
   same fix applied to the logo source toggle would be wrong, though: with
   only 2 items it'd never reach a second track, so there's nothing to gain
   and it'd lose the clean 50/50 split flex already gives it. Mobile
   overrides this further down with a fixed 3-column count instead of
   auto-fill's width-based one, since "predictable N per row" was the
   actual ask there, not just "don't stretch." */
#qr-type-toggle {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}

/* Generic version of .type-toggle-item's positioning role, for anchoring a
   tooltip to something that isn't one of several equal-width segmented
   buttons (e.g. a single checkbox row) — .type-toggle-item stays as-is so
   existing markup keeps working unchanged. */
.tooltip-anchor {
  position: relative;
}

.type-toggle-btn {
  flex: 1;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.type-toggle-btn:hover {
  border-color: var(--accent-soft);
}

.type-toggle-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* The tooltip is a sibling of the button, not a child — nesting it inside
   the button would let its text bleed into the button's accessible name
   (name-from-content) once visible on hover/focus, so a screen reader would
   announce it twice: once as the name, once via aria-describedby below.
   Delayed on purpose (~1.1s) so it doesn't flash on quick mouse passes —
   only shows when someone actually pauses on a type. Pure CSS: the
   transition-delay only applies going into :hover/:focus-within, so
   dismissal on mouseleave/blur is instant. */
.tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 200px;
  padding: 0.5rem 0.65rem;
  border-radius: 0.4rem;
  background: var(--text);
  color: var(--surface);
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  box-shadow: 0 8px 24px var(--shadow);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease, visibility 0s linear 0.15s;
  z-index: 20;
}

.tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--text);
}

.type-toggle-item:hover .tooltip,
.type-toggle-item:focus-within .tooltip,
.tooltip-anchor:hover .tooltip,
.tooltip-anchor:focus-within .tooltip {
  opacity: 1;
  visibility: visible;
  transition-delay: 1.1s;
}

input[type="color"] {
  width: 100%;
  height: 40px;
  padding: 0.2rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  cursor: pointer;
}

input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
  cursor: pointer;
}

/* ── Quick color templates (GitHub #56) ───────────────── */
/* Available to every tier -- a QOL shortcut, not an upsell, unlike the
   gradient toggle below it. Each button only ever sets a solid dot/bg
   pair (see app.js), so it can't be used to smuggle gradients past the
   registered+ gate. */

.color-template-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.color-template-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid var(--line);
  padding: 0;
  cursor: pointer;
  transition: transform 0.1s ease, border-color 0.1s ease;
}

.color-template-btn:hover {
  transform: scale(1.12);
  border-color: var(--accent);
}

.color-template-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Dark-background subgroup (GitHub #61) -- visually separated from the
   light-background presets above rather than folded into the same row,
   since the scan-reliability note only applies to this subset. */
.color-template-subgroup-label {
  margin-top: 0.9rem;
}

/* ── Solid/gradient color mode (dot + background) ─────── */

.color-mode-toggle {
  max-width: 220px;
  margin-bottom: 0.5rem;
}

.color-mode-fields {
  margin-top: 0.25rem;
}

.color-gradient-fields {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* The solid variant keeps input[type="color"]'s default full-width look
   (a single swatch filling the field) -- only the gradient variant's two
   swatches need to shrink to sit side by side with the type select. */
.color-gradient-fields input[type="color"] {
  width: 48px;
  flex-shrink: 0;
}

.color-gradient-fields select {
  flex: 1;
  min-width: 0;
}

input[type="file"] {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
}

input[type="file"]::file-selector-button {
  margin-right: 0.75rem;
  padding: 0.4rem 0.8rem;
  border: none;
  border-radius: 0.3rem;
  background: var(--accent);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  background: var(--accent-soft);
}

/* Shared amber advisory style — "you may want to reconsider this" rather
   than the red .field-error's "this is broken". Used for both the
   contrast warning and the logo scan-risk warning below. */
.risk-warning {
  font-size: 0.825rem;
  color: var(--warn-fg);
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-radius: 0.4rem;
  padding: 0.6rem 0.75rem;
  margin: 0;
}

.risk-warning + .risk-warning {
  margin-top: 0.75rem;
}

.field-error {
  font-size: 0.825rem;
  color: var(--danger-fg);
  background: var(--danger-bg);
  border: 1px solid var(--danger-line);
  border-radius: 0.4rem;
  padding: 0.6rem 0.75rem;
  margin: 0 0 1rem;
}

.logo-preview-wrap {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.logo-preview-wrap[hidden] {
  display: none;
}

/* Checkerboard so a transparent (background-removed) logo is visually
   distinguishable from the panel's own white background. */
.logo-preview-img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background-image: repeating-conic-gradient(var(--line) 0% 25%, transparent 0% 50%);
  background-size: 12px 12px;
}

.logo-remove-btn {
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.8rem;
  padding: 0.4rem 0.7rem;
  border-radius: 0.4rem;
  cursor: pointer;
}

.logo-remove-btn:hover {
  border-color: var(--accent-soft);
  color: var(--accent);
}

.panel-preview {
  text-align: center;
}

.preview-frame {
  display: flex;
  justify-content: center;
  margin-bottom: 1.1rem;
}

.qr-preview-wrap {
  width: 100%;
  max-width: 280px;
}

/* An actual <img>, not the live canvas — mobile browsers only offer the
   native long-press "save/share image" context menu for <img> elements,
   not <canvas>. The library's canvas render stays offscreen as a work
   surface (see #qr-preview-host in index.html); this is a PNG snapshot of
   it, kept in sync in app.js's refresh(). */
/* --code-field, not --surface: this is the ground the code is drawn against,
   and #173's rule is that dark mode never touches it. The PNG is opaque today
   (bgColor is always a solid from a colour input), so nothing shows through --
   which is exactly why this would be an easy thing to get wrong later. The
   day a transparent background is offered, a themed value here would put a
   user's code on a near-black field and break it for a large share of
   scanners, and it would break only for people in dark mode. */
.qr-preview {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 0.5rem 0.5rem 0 0;
  background: var(--code-field);
}

/* Mirrors the caption baked below the QR in the exported PNG (see
   drawWatermarkCaption() in app.js) so the on-screen preview always shows
   the same watermark as the downloaded file. Colors are set inline from
   the user's chosen dot/background colors (see refresh() in app.js).
   Radius is only on the bottom, and margin-top is 0, so this sits flush
   against the canvas above and the two read as one continuous shape. */
.qr-watermark {
  margin: 0 0 1rem;
  padding: 0.3rem 0.55rem;
  text-align: right;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 0 0 0.5rem 0.5rem;
}

.cta {
  display: inline-block;
  border: none;
  padding: 0.7rem 1.3rem;
  border-radius: 0.4rem;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.9rem;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s;
}

.cta:hover {
  background: var(--accent-soft);
}

.cta:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* .cta's own `display: inline-block` above otherwise wins over the browser's
   built-in `[hidden] { display: none }` rule at equal specificity (author
   styles beat the user-agent stylesheet) -- same fix as .field[hidden] and
   .logo-preview-wrap[hidden] elsewhere in this file. Without this, a hidden
   .cta button (e.g. #qr-save before entitlements resolve) still renders. */
.cta[hidden] {
  display: none;
}

.preview-note {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0.75rem 0 0;
}

.tool-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* ── Download format dropdown ─────────────────────────────
   Purpose-built rather than reused from .subnav-dropdown (nav.js's
   click/hover text-link menu, also reused by the header account
   dropdown) -- this needs a button trigger and richer two-line menu items
   (format name + a one-line "why you'd pick this" note) that the nav
   menu's plain link rows weren't designed for. Interaction is hand-wired
   in app.js to match: click-toggle and close-on-outside-click, but
   deliberately no hover-to-open -- a primary CTA popping a menu just
   because the pointer passed over it would be surprising here. */
.download-menu {
  position: relative;
  display: inline-block;
}

.download-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

.download-menu-caret {
  transition: transform 0.15s;
}

.download-menu-trigger[aria-expanded="true"] .download-menu-caret {
  transform: rotate(180deg);
}

.download-menu-list {
  display: none;
  flex-direction: column;
  position: absolute;
  top: 100%;
  /* Anchored to the trigger's left edge, not centered under it -- centering
     (left: 50%; transform: translateX(-50%)) pushed the menu off-screen on
     narrow viewports whenever the trigger sat near the left edge of its
     panel, since half the menu's width extends further left than there was
     room for. Left-anchoring only ever extends rightward, which the width
     cap below keeps inside the viewport. */
  left: 0;
  margin-top: 0.5rem;
  width: min(280px, calc(100vw - 2.5rem));
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 0.4rem;
  box-shadow: 0 8px 24px var(--shadow);
  z-index: 200;
}

.download-menu-list.open {
  display: flex;
}

.download-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  background: none;
  font-family: inherit;
  padding: 0.5rem 0.6rem;
  border-radius: 0.5rem;
  cursor: pointer;
}

.download-menu-item:hover,
.download-menu-item:focus-visible {
  background: var(--bg);
}

.download-menu-item:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.download-menu-item-name {
  display: block;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--ink);
}

.download-menu-item-desc {
  display: block;
  font-size: 0.75rem;
  color: var(--muted);
  margin-top: 0.1rem;
  line-height: 1.35;
}

/* ── Contact page ────────────────────────────────────── */

.contact {
  padding-bottom: 4rem;
}

.contact-grid {
  display: grid;
  max-width: 480px;
}

.field-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.form-status {
  min-height: 1.2em;
  margin: 0.75rem 0 0;
  font-size: 0.875rem;
}

.form-status-success { color: var(--success-fg); }
.form-status-error { color: var(--danger-ink); }

/* ── Auth pages (login, signup, verify, forgot/reset password) ─────── */

.auth {
  padding-bottom: 4rem;
}

.auth-grid {
  display: grid;
  max-width: 480px;
}

/* Login-only (GitHub #50) -- a modifier on .auth-grid rather than a change
   to the base rule, since signup/verify/forgot-password/reset-password all
   share .auth-grid too and weren't asked to grow a second column. Only
   login.html adds the matching .auth-brand-panel markup. */
.auth-grid--branded {
  max-width: 800px;
  gap: 1.5rem;
}

@media (min-width: 640px) {
  .auth-grid--branded {
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }
}

.auth-brand-panel {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.75rem;
  padding: 2rem;
  border-radius: 0.75rem;
  /* The fixed brand indigo, not --accent, and it stays that in dark mode.
     This panel is a saturated block with cream text and its own inverted
     brand mark (see login.html) -- a closed system. Letting --accent through
     would lighten the ground to #818CF8 under text chosen for #4F46E5 and
     drop it to about 2:1, which is the whole reason the constant exists. */
  background: var(--brand-indigo);
  /* Faint repeating dot grid, evocative of a QR code's module pattern
     without resembling a real scannable one -- decorative texture only. */
  background-image: radial-gradient(color-mix(in srgb, var(--brand-cream) 18%, transparent) 1.5px, transparent 1.5px);
  background-size: 14px 14px;
}

/* Below 640px this panel would just push the form down the page under a
   big graphic -- hidden on narrow viewports rather than stacked, same
   reasoning as why it's not shown at all until there's room beside the
   form. */
@media (min-width: 640px) {
  .auth-brand-panel {
    display: flex;
  }
}

.auth-brand-mark {
  width: 64px;
  height: 64px;
}

.auth-brand-mark svg {
  width: 100%;
  height: 100%;
}

.auth-brand-text {
  font-family: "Sora", sans-serif;
  font-weight: 700;
  font-size: 1.5rem;
  letter-spacing: -0.3px;
  margin: 0;
}

/* Overrides the shared header .w-primary/.w-accent colors (--ink/--accent,
   both dark, meant for a light background) -- illegible against this
   panel's solid indigo background otherwise. */
.auth-brand-panel .w-primary,
.auth-brand-panel .w-accent {
  color: var(--brand-cream);
}

.auth-brand-tagline {
  color: color-mix(in srgb, var(--brand-cream) 85%, transparent);
  font-size: 0.9rem;
  max-width: 26ch;
  margin: 0;
}

.field-note {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0.35rem 0 0;
}

/* GitHub #96 -- the webhook list on /profile.html. Deliberately plain: this
   is an account-settings list, not a dashboard, and it reuses .field-note and
   the existing button classes rather than introducing a second visual
   vocabulary for the same kind of row. */
/* GitHub #180 -- where the Turnstile widget draws. In `interaction-only`
   appearance it is usually zero-height (Cloudflare decides nobody needs to do
   anything), so this reserves no space of its own and only separates itself
   from the button when it actually has something to show. */
.challenge-slot:not(:empty) {
  margin-bottom: 1rem;
}

.webhook-list {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
}

.webhook-row {
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  padding: 0.75rem 0.9rem;
  margin-bottom: 0.75rem;
}

.webhook-row code {
  /* A webhook URL is long, arbitrary, and user-supplied. Wrapping anywhere is
     what keeps it inside the panel instead of pushing the page sideways --
     the same overflow class of bug the 390px e2e assertion exists to catch. */
  display: block;
  overflow-wrap: anywhere;
  margin-bottom: 0.35rem;
}

.webhook-row .field-note {
  margin-bottom: 0.6rem;
}

.webhook-row button + button {
  margin-left: 0.5rem;
}

.form-links {
  margin: 1rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.form-links a {
  margin-left: 0.25rem;
}

/* ── Account page ────────────────────────────────────── */

.account-grid {
  display: grid;
  gap: 1.25rem;
  max-width: 720px;
}

/* Same fix as .cta[hidden]/.content-grid[hidden] elsewhere in this file. */
.account-grid[hidden] {
  display: none;
}

.tier-badge {
  display: inline-block;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.feature-list {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
  font-size: 0.85rem;
}

.feature-list li[data-enabled="false"] {
  color: var(--muted);
  text-decoration: line-through;
}

.stat-tile-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.stat-tile {
  min-width: 120px;
}

.stat-tile-value {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0;
}

.stat-tile-label {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0.15rem 0 0;
}

/* Wraps every hand-rolled SVG chart in dynqr-analytics.js -- horizontal
   scroll rather than clipping/squashing on a narrow viewport, since the
   column chart's width scales with the number of days plotted and can
   exceed the panel width. */
.dynqr-chart {
  overflow-x: auto;
}

.qr-library-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: grid;
  gap: 0.75rem;
}

.qr-library-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
}

/* A real render of the item's own saved url + style (see
   renderThumbnail() in library.js), not a generic icon -- sized fixed so
   a dense QR/long payload can't push the row taller, with its own
   background since a saved item's bgColor could be anything, including a
   value close to the panel's own background. */
.qr-library-item-thumb {
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The library renders its SVG at a fixed internal resolution well above
   this box (see THUMB_RENDER_SIZE in library.js) so dense/maximum-length
   payloads never hit the library's own "canvas is too small" floor --
   width/height here scale that down to the actual display size, lossless
   since the output stays vector at any size. */
.qr-library-item-thumb svg {
  display: block;
  width: 100%;
  height: 100%;
}

.qr-library-item-meta {
  min-width: 0;
  flex: 1 1 auto;
}

.qr-library-item-name {
  font-weight: 600;
  font-size: 0.9rem;
  margin: 0 0 0.15rem;
}

.qr-library-item-url {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.qr-library-item-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}

/* Dynamic items have a third action (View analytics) alongside
   pause/resume + delete -- stacked rather than forcing three buttons into
   the same row width the static two-button case was sized for. */
.qr-library-item--dynamic .qr-library-item-actions {
  flex-direction: column;
}

/* Segmented All/Static/Dynamic filter above the merged list -- only shown
   (via JS toggling [hidden]) for users who actually have dynamic_qr
   entitlement, since the Dynamic option is meaningless otherwise. */
.qr-library-filter {
  display: flex;
  gap: 0.4rem;
  margin: 0 0 0.85rem;
}

.qr-library-filter[hidden] {
  display: none;
}

.qr-library-filter-btn {
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  font-size: 0.8rem;
  font-family: inherit;
  cursor: pointer;
}

.qr-library-filter-btn[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* A subtle tint (not a loud color change) so dynamic codes read as a
   distinct category at a glance in a list otherwise identical-looking to
   static codes -- per the explicit ask that they "stand out". */
.qr-library-item--dynamic {
  background: color-mix(in srgb, var(--accent-soft) 10%, var(--surface));
  border-color: var(--accent-soft);
}

.qr-library-item-badge {
  display: inline-block;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  margin-right: 0.4rem;
  vertical-align: middle;
}

.qr-library-item-status {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 600;
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  margin-left: 0.4rem;
  vertical-align: middle;
  background: var(--success-bg);
  color: var(--success-fg);
}

.qr-library-item-status--paused {
  background: var(--warn-bg);
  color: var(--warn-fg);
}

.qr-library-item-dest-row {
  display: flex;
  gap: 0.4rem;
  margin: 0.4rem 0 0.15rem;
}

.qr-library-item-dest-row input[type="url"] {
  font-size: 0.8rem;
  padding: 0.35rem 0.5rem;
}

/* Library toolbar and bulk bar (GitHub #97). A flat list of 10 is fine; a flat
   list of 200 is not, and the paid tiers are already unlimited. */
.qr-library-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 0.75rem;
}

.qr-library-tools input,
.qr-library-tools select {
  font-size: 0.85rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
}

.qr-library-search {
  flex: 1 1 14rem;
  display: flex;
}

.qr-library-search input {
  width: 100%;
}

/* Sticky, because a bulk selection made at the bottom of a long list is
   useless if its actions are at the top. */
.qr-library-bulk {
  position: sticky;
  top: 0.5rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.75rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--accent);
  border-radius: 0.45rem;
  background: var(--surface);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
}

.qr-library-item-select {
  align-self: flex-start;
  margin: 0.15rem 0.35rem 0 0;
  width: 1rem;
  height: 1rem;
  flex: none;
}

.qr-library-item-folder {
  align-self: flex-start;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.15rem 0.4rem;
  border-radius: 0.3rem;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--muted);
}

/* Routing and scheduling panel (GitHub #93, #95).

   A <details> rather than an always-open block: this is the second thing an
   owner does with a dynamic code, not the first, and an open rule editor on
   every row would bury the destination field that most visits are actually
   for. Every colour here is a token, so the panel follows dark mode with the
   rest of the chrome. */
.dynqr-conditions {
  margin: 0.5rem 0 0.15rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--bg);
}

.dynqr-conditions > summary {
  cursor: pointer;
  padding: 0.4rem 0.6rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text);
}

.dynqr-conditions-body {
  padding: 0.2rem 0.6rem 0.6rem;
  display: grid;
  gap: 0.5rem;
}

.dynqr-password {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.dynqr-password label,
.dynqr-expiry label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text);
}

.dynqr-password input {
  font-size: 0.8rem;
  padding: 0.3rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 0.35rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
}

.dynqr-password .form-hint {
  flex-basis: 100%;
}

.dynqr-rules {
  display: grid;
  gap: 0.4rem;
}

/* Wraps rather than scrolls: a rule is four controls, and on a narrow screen
   stacking them is readable where a horizontal scroller inside a list row is
   not. */
.dynqr-rule {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
}

.dynqr-rule select,
.dynqr-rule input {
  font-size: 0.8rem;
  padding: 0.3rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 0.35rem;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
}

/* The URL is the part that needs room; the condition selects are short and
   fixed. */
.dynqr-rule input[type="url"] {
  flex: 1 1 12rem;
  min-width: 0;
}

.dynqr-rule-value {
  display: inline-flex;
  gap: 0.3rem;
}

.form-hint {
  font-size: 0.75rem;
  color: var(--muted);
  margin: 0;
  line-height: 1.4;
}

.btn-secondary {
  display: inline-block;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.4rem 0.7rem;
  border-radius: 0.4rem;
  cursor: pointer;
  text-decoration: none;
}

/* Same fix as .cta[hidden] above -- this class's own `display: inline-block`
   otherwise wins over the browser's built-in `[hidden]` rule, so a hidden
   .btn-secondary (e.g. #qr-save-locked before entitlements resolve) would
   still render. */
.btn-secondary[hidden] {
  display: none;
}

.btn-secondary:hover {
  border-color: var(--accent-soft);
  color: var(--accent);
}

.btn-danger {
  border: 1px solid var(--danger-line);
  background: var(--surface);
  color: var(--danger-ink);
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.4rem 0.7rem;
  border-radius: 0.4rem;
  cursor: pointer;
}

.btn-danger:hover {
  background: var(--danger-bg);
}

.btn-danger:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.empty-note {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0;
}

/* Business QR library (GitHub #23) -- the shoutbox/comment thread nested
   inside each shared-library item's card. */
.business-qr-comments {
  display: grid;
  gap: 0.3rem;
  margin: 0.5rem 0;
}

.business-qr-comments:empty {
  display: none;
}

.button-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.75rem;
}

/* ── Admin dashboard ─────────────────────────────────── */

/* Wide tabular content scrolls inside its own container rather than
   overflowing the page horizontally on narrow viewports. */
.admin-table-wrap {
  overflow-x: auto;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.admin-table th,
.admin-table td {
  padding: 0.55rem 0.7rem;
  text-align: left;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.admin-table th {
  color: var(--muted);
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.admin-table tbody tr:hover {
  background: var(--bg);
}

/* Screen-reader-only content -- e.g. a table column header that's visually
   redundant (the tier name already labels its column) but still needs a
   real accessible name for the row-label cells to make sense when read
   linearly. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.pricing-table-wrap {
  overflow-x: auto;
  margin-top: 1.5rem;
}

.pricing-table {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
}

.pricing-table th,
.pricing-table td {
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--line);
  text-align: center;
}

.pricing-table thead th {
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink);
}

.pricing-table tbody th[scope="row"] {
  text-align: left;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.pricing-table td[data-enabled="false"] {
  color: var(--muted);
}

.pricing-table-cta-row td {
  padding-top: 1.25rem;
}

.pricing-table-note {
  margin-top: 1rem;
  color: var(--muted);
  font-size: 0.85rem;
  max-width: 720px;
}

/* ── Footer ──────────────────────────────────────────── */

.site-footer {
  border-top: 1px solid var(--line);
  padding: 1.5rem 0;
  color: var(--muted);
  font-size: 0.8rem;
}

.footer-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
}

/* Type-generator links (GitHub #92). Spans the full width as its own row
   above the existing three-column footer row, so it doesn't consume one of
   those columns. */
.footer-generators {
  grid-column: 1 / -1;
  justify-self: stretch;
  padding-bottom: 1rem;
  margin-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
}

.footer-generators h2 {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
}

.footer-generators ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-generators a {
  color: var(--muted);
  text-decoration: none;
}

.footer-generators a:hover { color: var(--accent); }

.footer-nav {
  justify-self: start;
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.footer-nav a {
  color: var(--muted);
  text-decoration: none;
}

.footer-nav a:hover { color: var(--accent); }

.footer-attribution {
  justify-self: center;
  text-align: center;
}

.footer-attribution a {
  color: var(--muted);
}

.footer-attribution a:hover { color: var(--accent); }

.footer-copyright {
  justify-self: end;
  text-align: right;
}

.link-button {
  font: inherit;
  font-size: inherit;
  color: var(--muted);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.link-button:hover { color: var(--accent); }

/* ── Cookie banner ───────────────────────────────────── */

.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  background: var(--banner-bg);
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12);
}

.cookie-banner-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1rem 0;
}

.cookie-banner-inner p {
  margin: 0;
  font-size: 0.85rem;
  color: var(--banner-fg);
  max-width: 60ch;
}

.cookie-banner-actions {
  display: flex;
  gap: 0.6rem;
  flex-shrink: 0;
}

.btn-cookie {
  font: inherit;
  font-weight: 600;
  font-size: 0.85rem;
  border-radius: 0.5rem;
  padding: 0.55rem 1.1rem;
  cursor: pointer;
  border: 1px solid transparent;
}

.btn-cookie-accept {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.btn-cookie-accept:hover { background: var(--accent-soft); border-color: var(--accent-soft); }

.btn-cookie-reject {
  color: var(--banner-fg);
  background: transparent;
  border-color: var(--muted);
}

.btn-cookie-reject:hover { border-color: var(--banner-fg); }

@media (max-width: 760px) {
  .cookie-banner-inner {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ── Responsive ──────────────────────────────────────── */

@media (max-width: 720px) {
  .tool-grid {
    grid-template-columns: 1fr;
  }

  /* Scoped to the main QR-type selector specifically, not .type-toggle in
     general — the 2-item logo source toggle should keep its even 50/50
     split, not a 3-column grid with two empty cells. Grid (not flex-wrap)
     because it needs a fixed count per row regardless of label length —
     flex-wrap alone would fit a different number per row depending on
     whether a type's name is "Text" or something longer, which isn't the
     predictable "N per row" this is meant to guarantee as more types get
     added. Any row short of 3 just leaves trailing grid cells empty rather
     than stretching to fill — the normal, expected behavior for a fixed
     column count. */
  #qr-type-toggle {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
  }

  /* Tooltips normally center on their own button, but centered on a button
     in the grid's outer columns they'd overflow the viewport edge — nth-
     child reliably identifies "rightmost"/"leftmost column" here because
     the column count is fixed at 3 above, not because of anything about
     the buttons themselves. Middle column (3n+2) is left centered. */
  #qr-type-toggle .type-toggle-item:nth-child(3n) .tooltip {
    left: auto;
    right: 0;
    transform: none;
  }

  #qr-type-toggle .type-toggle-item:nth-child(3n+1) .tooltip {
    left: 0;
    transform: none;
  }

  /* Superseded by the mobile-chrome block at the end of this file, which
     takes .site-nav out of the flow entirely instead of wrapping it. */
  .site-header .container {
    gap: 0.6rem;
  }

  /* The thumbnail (added alongside name/url/actions in one row) was
     enough extra fixed-width content to push .qr-library-item past the
     viewport on narrow screens, even with meta's own text able to
     shrink/ellipsis -- actions (two buttons, flex-shrink: 0 so they stay
     tappable) had nowhere left to go. Wrapping actions onto their own
     full-width row below thumb+meta keeps every row within the
     viewport instead of overflowing it. */
  .qr-library-item {
    flex-wrap: wrap;
  }

  .qr-library-item-actions {
    flex-basis: 100%;
    justify-content: flex-end;
  }

  .footer-inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: 0.5rem;
  }

  .footer-nav,
  .footer-attribution,
  .footer-copyright {
    justify-self: center;
  }

  .footer-generators ul {
    justify-content: center;
  }
}


/* ── Mobile chrome ───────────────────────────────────────
   Below this breakpoint the header nav no longer fits: the six inline links
   plus the account dropdown need ~405px, and the brand takes another ~180px,
   so the row overflowed the viewport on every phone narrower than ~620px.
   That clipped the Account / Log In control off-screen entirely at 390px and
   360px -- the two most common phone widths -- and Contact as well at 320px.
   Since this chrome sits on every page including the whole content hub, and
   the clipped control is the sign-up entry point, it's fixed here rather than
   left to each page. */

@media (max-width: 720px) {
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }

  /* The panel is positioned against .site-header (already a containing block,
     being sticky) rather than .container, so it spans the full width instead
     of the centred column. */
  .site-nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.35rem 0 0.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 10px 24px var(--shadow);
    max-height: 70vh;
    overflow-y: auto;
  }

  .site-nav.open {
    display: flex;
  }

  .site-nav > a,
  .site-nav .subnav-dropdown-label {
    display: block;
    padding: 0.75rem clamp(1rem, 5vw, 2rem);
    font-size: 0.95rem;
    opacity: 1;
  }

  .nav-auth-slot {
    border-top: 1px solid var(--line);
    margin-top: 0.35rem;
    padding-top: 0.35rem;
  }

  /* Inside the panel the account dropdown expands inline rather than floating
     -- an absolutely positioned menu anchored to the right edge would hang off
     a panel that is already full-width. */
  .site-nav .subnav-dropdown {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }

  .site-nav .subnav-dropdown-toggle {
    position: absolute;
    right: clamp(1rem, 5vw, 2rem);
    margin-top: 0.6rem;
    padding: 0.3rem 0.5rem;
  }

  .site-nav .nav-account-dropdown .subnav-dropdown-menu {
    position: static;
    padding-top: 0;
    min-width: 0;
  }

  .site-nav .nav-account-dropdown .subnav-dropdown-menu-inner {
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    background: var(--bg);
  }

  .site-nav .nav-account-dropdown .subnav-dropdown-menu a,
  .site-nav .nav-account-dropdown .subnav-dropdown-menu button {
    padding: 0.6rem clamp(1.75rem, 8vw, 3rem);
    font-size: 0.9rem;
  }

  /* Hover-to-open is meaningless on touch and fires unpredictably; inside the
     panel the menu opens only from its own toggle.
     The :not(.open) is load-bearing, not defensive. `:hover` counts toward
     specificity, so a plain `.site-nav .subnav-dropdown:hover .menu` (0,4,0)
     outranks `.site-nav .menu.open` (0,3,0) no matter which comes later --
     and since a tap leaves the emulated hover state stuck on the element,
     that made the account menu impossible to open on touch at all. Excluding
     open menus from the hover rule removes the conflict rather than trying to
     out-specify it. */
  .site-nav .subnav-dropdown:hover .subnav-dropdown-menu:not(.open) {
    display: none;
  }

  .site-nav .subnav-dropdown-menu.open {
    display: flex;
  }

  /* Content-hub subnav: five dropdowns wrapped onto three rows at 390px,
     pushing each article's <h1> well down the first screen. One scrollable
     row instead. The caret menus are dropped here rather than reworked --
     every pillar page already lists its own articles prominently, so the
     category link is a complete route to them on a phone. */
  .subnav-inner {
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 1.15rem;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .subnav-inner::-webkit-scrollbar {
    display: none;
  }

  .subnav .subnav-dropdown {
    flex-shrink: 0;
  }

  .subnav .subnav-dropdown-toggle,
  .subnav .subnav-dropdown-menu {
    display: none;
  }
}

/* Consent banner: fixed to the bottom, it was taking roughly a third of a
   phone screen on first visit -- over exactly the content a search visitor
   arrived to read. Same information, tighter box, full-width tap targets.
   Matched to the 760px breakpoint where it already switches to a column, so
   the two don't disagree between 721px and 760px. */
@media (max-width: 760px) {
  .cookie-banner-inner {
    gap: 0.6rem;
    padding: 0.75rem 0;
  }

  .cookie-banner-inner p {
    font-size: 0.8rem;
    line-height: 1.45;
  }

  .cookie-banner-actions {
    width: 100%;
  }

  .cookie-banner-actions .btn-cookie {
    flex: 1;
    padding: 0.6rem 0.5rem;
  }
}
