/* Figtree (https://fonts.google.com/specimen/Figtree, OFL): the whole UI and
   the QR frame lettering (see FRAME_FONT_FAMILY in js/qr-render.js). Bundled so
   canvas and SVG downloads can use it too. Latin subset, variable weight. */
@font-face {
  font-family: "Figtree";
  src: url("fonts/figtree.woff2") format("woff2");
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  color-scheme: light;

  /* Surfaces */
  --page: #f3f3f5;
  --surface: #ffffff;
  --surface-2: #efeff2;
  --surface-3: #e6e6ea;

  /* Text */
  --text-primary: #16160f;
  --text-secondary: #58564c;
  --text-muted: #8f8c7f;

  /* Lines */
  --gridline: #e7e5da;
  --baseline: #c8c6b6;
  --border: rgba(13, 13, 8, 0.1);
  --border-strong: rgba(13, 13, 8, 0.16);

  /* Accent: Sellwood red (#ed1c24), with yellow (#ffda00) second for charts. */
  --series-1: #ed1c24;
  --series-1-light: #f2555b;
  --series-2: #ffda00;
  --accent-soft: color-mix(in srgb, var(--series-1) 11%, transparent);
  --accent-soft-strong: color-mix(in srgb, var(--series-1) 20%, transparent);
  --good: #12a150;
  --critical: #e23d3d;

  /* Radius scale -- larger and softer than a typical utility app */
  --radius-xs: 6px;
  --radius-sm: 10px;
  --radius: 18px;
  --radius-lg: 24px;

  /* Spacing scale */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;

  /* Shadows: soft and layered rather than one hard drop */
  --shadow-xs: 0 1px 2px rgba(20, 16, 60, 0.05);
  --shadow-sm: 0 1px 2px rgba(20, 16, 60, 0.04), 0 4px 10px -6px rgba(20, 16, 60, 0.14);
  --shadow-md: 0 16px 36px -14px rgba(20, 16, 60, 0.22);
  --shadow-pop: 0 26px 60px -16px rgba(20, 16, 60, 0.35);
  --shadow-accent: 0 10px 24px -8px color-mix(in srgb, var(--series-1) 48%, transparent);

  --header-h: 60px;
  --font-sans: 'Figtree', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
}

@media (min-width: 860px) { :root { --header-h: 68px; } }

/* Dark: on Auto (no data-theme) when the device is dark, or when Dark is
   picked in the page menu (see wireTheme() in js/generator/). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --page: #0b0b0a;
    --surface: #171614;
    --surface-2: #201f1c;
    --surface-3: #2a2926;
    --text-primary: #f5f4f0;
    --text-secondary: #c2bfb2;
    --text-muted: #8f8c7f;
    --gridline: #2a2926;
    --baseline: #3c3b35;
    --border: rgba(255, 255, 255, 0.1);
    --border-strong: rgba(255, 255, 255, 0.16);
    --series-1: #f2555b;
    --series-1-light: #f4777c;
    --series-2: #ffda00;
    --accent-soft: color-mix(in srgb, var(--series-1) 16%, transparent);
    --accent-soft-strong: color-mix(in srgb, var(--series-1) 26%, transparent);
    --critical: #ea6f6f;
    --good: #3ecf7e;
    --shadow-xs: none;
    --shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
    --shadow-md: 0 14px 32px -12px rgba(0, 0, 0, 0.55);
    --shadow-pop: 0 24px 56px -16px rgba(0, 0, 0, 0.65);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --page: #0b0b0a;
  --surface: #171614;
  --surface-2: #201f1c;
  --surface-3: #2a2926;
  --text-primary: #f5f4f0;
  --text-secondary: #c2bfb2;
  --text-muted: #8f8c7f;
  --gridline: #2a2926;
  --baseline: #3c3b35;
  --border: rgba(255, 255, 255, 0.1);
  --border-strong: rgba(255, 255, 255, 0.16);
  --series-1: #f2555b;
  --series-1-light: #f4777c;
  --series-2: #ffda00;
  --accent-soft: color-mix(in srgb, var(--series-1) 16%, transparent);
  --accent-soft-strong: color-mix(in srgb, var(--series-1) 26%, transparent);
  --critical: #ea6f6f;
  --good: #3ecf7e;
  --shadow-xs: none;
  --shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
  --shadow-md: 0 14px 32px -12px rgba(0, 0, 0, 0.55);
  --shadow-pop: 0 24px 56px -16px rgba(0, 0, 0, 0.65);
}
/* Light picked while the device is dark. */
:root[data-theme="light"] { color-scheme: light; }

* { box-sizing: border-box; }

/* No default blue/grey flash when tapping links, buttons and icons on a phone. */
* { -webkit-tap-highlight-color: transparent; }

/* Always reserve the scrollbar's space. Otherwise a short page (the dashboard
   before its data loads) is wider than a long one, and the centred header
   shifts sideways when the scrollbar appears or when moving between pages. */
html { overflow-y: scroll; -webkit-text-size-adjust: 100%; }

html, body {
  margin: 0;
  padding: 0;
  /* A soft, colour-washed background instead of a flat tint -- layered over
     --page so anything assuming a solid colour underneath still works. */
  background-color: var(--page);
  background-image:
    radial-gradient(1100px circle at 8% -12%, color-mix(in srgb, var(--series-1) 10%, transparent), transparent 55%),
    radial-gradient(900px circle at 108% 0%, color-mix(in srgb, var(--series-2) 9%, transparent), transparent 50%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]), :root:not([data-theme="light"]) body {
    background-image:
      radial-gradient(1100px circle at 8% -12%, color-mix(in srgb, var(--series-1) 18%, transparent), transparent 55%),
      radial-gradient(900px circle at 108% 0%, color-mix(in srgb, var(--series-2) 13%, transparent), transparent 50%);
  }
}
:root[data-theme="dark"], :root[data-theme="dark"] body {
  background-image:
    radial-gradient(1100px circle at 8% -12%, color-mix(in srgb, var(--series-1) 18%, transparent), transparent 55%),
    radial-gradient(900px circle at 108% 0%, color-mix(in srgb, var(--series-2) 13%, transparent), transparent 50%);
}

body { min-height: 100vh; }

a { color: var(--series-1); }

:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }

h1 { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; margin: 0; }
h2 { font-size: 1rem; font-weight: 700; letter-spacing: -0.005em; margin: 0; }
code { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.85em; }

.subtitle { color: var(--text-secondary); margin: 4px 0 0; font-size: 0.88rem; }
.hint { font-size: 0.79rem; color: var(--text-muted); margin-top: 5px; line-height: 1.45; }
.muted { color: var(--text-muted); }

/* ---- Icons (Lucide, see js/icons.js) ---- */

.icon { width: 16px; height: 16px; flex: none; }
.icon.sm { width: 14px; height: 14px; }
.icon.lg { width: 20px; height: 20px; }
/* Holds the icon's space until js/icons.js swaps in the SVG. */
i[data-icon] { display: inline-block; width: 16px; height: 16px; flex: none; }

/* ==========================================================================
   Layout
   ========================================================================== */

.container {
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--sp-2) var(--sp-4) 56px; /* a tight gap under the header */
}

@media (min-width: 720px) { .container { padding-left: var(--sp-5); padding-right: var(--sp-5); padding-top: var(--sp-3); } }

header.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--page) 82%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  border-bottom: 1px solid var(--border);
}

.header-inner {
  max-width: 1080px;
  min-height: var(--header-h);
  margin: 0 auto;
  padding: 0 var(--sp-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

@media (min-width: 720px) { .header-inner { padding: 0 var(--sp-5); } }

.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  font-weight: 800;
  font-size: 1.06rem;
  letter-spacing: -0.015em;
  text-decoration: none;
  color: var(--text-primary);
  min-width: 0;
}

.brand .mark {
  width: 33px;
  height: 33px;
  border-radius: var(--radius-xs);
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1) 55%, color-mix(in srgb, var(--series-1) 65%, #7a1810));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  flex: none;
  box-shadow: var(--shadow-accent);
}

nav.tabs {
  display: flex;
  gap: 2px;
  background: var(--surface-2);
  border-radius: 999px;
  padding: 4px;
  border: 1px solid var(--border);
}

nav.tabs a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border-radius: 999px;
  text-decoration: none;
  color: var(--text-secondary);
  font-size: 0.85rem;
  font-weight: 650;
  transition: color 0.15s var(--ease);
}

nav.tabs a:hover { color: var(--text-primary); }
nav.tabs a.active {
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1));
  color: #fff;
  box-shadow: var(--shadow-accent);
}
nav.tabs a.active .icon { color: #fff; }

@media (max-width: 440px) {
  /* Icon only, not the label text: three tabs' full labels plus the brand
     mark and sign-out button no longer fit a narrow phone on one line, and
     since the header's a flex row (not wrapping), text that didn't fit used
     to wrap onto a second line and spill past the header's own box, sitting
     on top of whatever came right after it in the page. Each link still has
     its label as title/aria-label, so this doesn't lose the accessible name. */
  nav.tabs a { padding: 8px 10px; }
  nav.tabs a span { display: none; }
}

/* ==========================================================================
   Cards & panels
   ========================================================================== */

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

@media (min-width: 720px) { .card { padding: var(--sp-5); } }

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

.card-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.card-head .icon { color: var(--text-muted); }
.card-head h2 { flex: 1; min-width: 0; }

/* Small uppercase caption for a group of controls. */
.section-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--sp-3);
}

.signin-card { max-width: 380px; margin: 40px auto 0; }
.signin-card .signin-icon {
  width: 44px; height: 44px; border-radius: var(--radius-xs);
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1));
  color: #fff;
  box-shadow: var(--shadow-accent);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: var(--sp-4);
}
.signin-card .signin-icon .icon { width: 20px; height: 20px; }

/* ==========================================================================
   Forms
   ========================================================================== */

label {
  display: block;
  font-size: 0.79rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.field { margin-bottom: var(--sp-3); }

input[type="text"], input[type="url"], input[type="email"], input[type="tel"], input[type="search"],
input[type="number"], input[type="password"], input[type="datetime-local"], input[type="date"], input[type="time"],
textarea, select {
  width: 100%;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--page);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 0.9rem;
  line-height: 1.3;
  transition: border-color 0.12s var(--ease), box-shadow 0.12s var(--ease), background 0.12s var(--ease);
}

input::placeholder, textarea::placeholder { color: var(--text-muted); }

/* iOS zooms the page on focus into any field under 16px. */
@media (max-width: 720px) {
  input[type="text"], input[type="url"], input[type="email"], input[type="tel"], input[type="search"],
  input[type="number"], input[type="password"], input[type="datetime-local"], input[type="date"], input[type="time"],
  textarea, select { font-size: 16px; }
  /* Not an input itself, but reads as one: same text size as the rest. */
  .input-action.date-field { font-size: 16px; }
}

/* Block, not the default inline: an inline textarea leaves a few pixels of
   line-height under it, so the gap after it is bigger than after an input. */
textarea { display: block; resize: vertical; min-height: 64px; }

select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 32px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238f8c7f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  cursor: pointer;
}

/* A colour picker shows as a field like the others: a small dot of the colour
   and its hex code (built by wireColorFields() in js/generator/). The real
   input is laid invisibly over the whole field, so a click or tap anywhere
   opens the browser's own picker. */
.color-field {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 38px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page);
  cursor: pointer;
}
.color-field:focus-within {
  border-color: var(--series-1);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* The faint ring keeps a white (or page-coloured) dot visible. */
.color-dot {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: var(--radius-xs);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.color-hex {
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.color-field input[type="color"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}
/* Its own ring would be invisible; the field's :focus-within ring shows instead. */
.color-field input[type="color"]:focus-visible { outline: none; }

/* The touch-screen colour picker (js/color-picker.js): a sheet pinned to the
   bottom of the screen, in place of the phone's own picker. */
dialog.color-sheet {
  width: 100%;
  max-width: 440px;
  margin: auto auto 0;
  padding: var(--sp-4) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
  border: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
dialog.color-sheet[open] { animation: color-sheet-up 0.22s var(--ease, ease); }
@keyframes color-sheet-up { from { transform: translateY(100%); } }
.color-sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-3); }
.color-sheet-title { margin: 0; font-size: 1rem; }
.color-sheet-done { color: var(--series-1); }

/* Saturation left to right, brightness top to bottom, over the current hue. */
.color-sv {
  position: relative;
  height: 180px;
  border-radius: var(--radius-sm);
  background:
    linear-gradient(to top, #000, transparent),
    linear-gradient(to right, #fff, transparent),
    var(--hue, red);
  touch-action: none;
}
.color-hue {
  position: relative;
  height: 16px;
  margin: var(--sp-4) 0;
  border-radius: 999px;
  background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
  touch-action: none;
}
.color-sv-thumb, .color-hue-thumb {
  position: absolute;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 3px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.color-hue-thumb { top: 50%; }

.color-sheet-row { display: flex; align-items: center; gap: var(--sp-3); }
.color-sheet-preview {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.color-sheet-hex { flex: 1; min-width: 0; font-variant-numeric: tabular-nums; text-transform: uppercase; }

.color-presets {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 10px;
  margin-top: var(--sp-4);
}
.color-preset {
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
  cursor: pointer;
}
.color-preset.active { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 0 2px var(--surface), 0 0 0 4px var(--series-1); }

input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--series-1); vertical-align: middle; margin: 0; }
input[type="file"] { font-size: 0.82rem; color: var(--text-secondary); min-width: 0; max-width: 100%; }

/* Visually hidden but still focusable/clickable -- used to keep a native
   input (whose own appearance can't be fully styled cross-browser) wired to
   a normal-looking label acting as its button. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Centre logo: an upload button (styled to match the rest of the app, unlike
   a native file input) plus the chosen filename, and a second row to load an
   image from a link instead (e.g. an icon from lucide.dev). */
.logo-upload { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.logo-upload .btn { flex: none; }
.logo-filename { font-size: 0.82rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--series-1);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Toggle switch: a checkbox drawn as a pill. */
input[type="checkbox"].switch {
  appearance: none;
  -webkit-appearance: none;
  width: 36px;
  height: 21px;
  border-radius: 999px;
  background: var(--baseline);
  position: relative;
  cursor: pointer;
  flex: none;
  transition: background 0.15s var(--ease);
}
input[type="checkbox"].switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s var(--ease);
}
input[type="checkbox"].switch:checked { background: var(--series-1); }
input[type="checkbox"].switch:checked::after { transform: translateX(15px); }
input[type="checkbox"].switch:disabled { opacity: 0.45; cursor: not-allowed; }
/* For setting a switch from script as the page opens, so it starts in its final
   position instead of sliding there (see startEdit in js/generator/). */
input[type="checkbox"].switch.no-anim,
input[type="checkbox"].switch.no-anim::after { transition: none; }

.checkbox-row { display: flex; align-items: center; gap: 8px; }
.checkbox-row label { display: inline; margin: 0; font-weight: 500; font-size: 0.85rem; }

.row { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.row > * { flex: 1; min-width: 120px; }

/* Paired fields (name + password, first + last name, ...): always two even
   columns, one on a phone, so rows line up instead of reflowing into a
   ragged number of columns depending on the card's width. */
.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--sp-3);
}
@media (max-width: 480px) { .field-grid { grid-template-columns: minmax(0, 1fr); } }

/* Settings lists (the Dynamic and Style cards): one setting per row -- label
   on the left, control on the right in a fixed-width column so every
   control lines up -- grouped under accent-coloured headings. Every row is
   the same height and every control in it the same height, whatever kind
   of control it is. */
.settings-group + .settings-group { margin-top: var(--sp-5); }
.settings-title {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 0.74rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--series-1);
  padding-bottom: var(--sp-2);
  border-bottom: 2px solid var(--accent-soft-strong);
}
.settings-title .muted { text-transform: none; letter-spacing: 0; font-weight: 500; }
.setting {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 240px);
  align-items: center;
  gap: var(--sp-3);
  height: 44px;
  border-bottom: 1px solid var(--border);
}
.setting > label, .setting .checkbox-row label {
  margin: 0;
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.setting :is(input:not([type="checkbox"]), select),
.setting .btn {
  height: 32px;
  padding-top: 0;
  padding-bottom: 0;
}
.setting > .switch { justify-self: end; }
.setting .color-field { height: 32px; }
.setting .date-field { height: 32px; padding-right: 34px; }
.setting .logo-upload { flex-wrap: nowrap; gap: 6px; }
.setting .logo-filename { flex: 1; }
.setting .logo-upload .icon-btn { width: 28px; height: 28px; flex: none; }
.setting .input-action .icon-btn { width: 26px; height: 26px; right: 3px; }
@media (max-width: 480px) {
  .setting { grid-template-columns: minmax(0, 1fr) minmax(0, 58%); }
}
.dyn-fields .dyn-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-5); }
.dyn-actions > .btn { flex: 1; text-decoration: none; }
.dyn-actions > .btn[hidden] { display: none; }
/* Smart redirect rules (see redirect-rules.js): their own card under the
   content card, one row per rule -- its number, when it applies over what it
   shows, and buttons to try, move and remove it -- that opens to set it up,
   with the content form moved inside. */
.gen-rules[hidden] { display: none; }
.rule-list { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); }
.rule-list:empty { display: none; }
.rule { border: 1px solid var(--border); border-radius: var(--radius-sm); }
.rule.open { border-color: color-mix(in srgb, var(--series-1) 45%, var(--border)); }
.rule-head { display: flex; align-items: center; gap: 2px; padding: 4px 6px 4px 0; }
.rule-toggle {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px;
  padding: 6px 4px 6px 10px; background: none; border: 0; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.rule-num {
  flex: none; width: 24px; height: 24px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text-secondary); font-size: 0.78rem; font-weight: 650;
}
.rule.open .rule-num { background: var(--accent-soft); color: var(--series-1); }
.rule-summary { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rule-summary-when, .rule-summary-show { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rule-summary-when { font-size: 0.86rem; font-weight: 600; }
.rule-summary-show { font-size: 0.78rem; color: var(--text-muted); }
.rule-summary-show:empty { display: none; }
.rule-chev { flex: none; color: var(--text-muted); }
/* A paused rule: dimmed, its power button lit. */
.rule.paused .rule-summary, .rule.paused .rule-num { opacity: 0.5; }
.rule-actions .rule-pause[aria-pressed="true"] { background: none; border-color: transparent; color: var(--series-1); }
/* A rule that never applies (a rule above catches its scans first): a
   small mark by its summary, the reason on hover. */
.rule-shadow { flex: none; display: inline-flex; color: var(--text-muted); }
.rule-shadow[hidden] { display: none; }
.rule.open .rule-chev { display: none; }
.rule-actions { flex: none; display: flex; gap: 2px; }
.rule-actions .icon-btn { width: 28px; height: 28px; }
.rule-actions .icon-btn[hidden] { display: none; }
/* Moving a rule is for when it's open: a closed one keeps just Try and Remove. */
.rule:not(.open) :is(.rule-up, .rule-down) { display: none; }
.rule-body { display: none; padding: 2px var(--sp-3) var(--sp-3); }
.rule.open .rule-body { display: block; }
.rule-part + .rule-part { margin-top: var(--sp-4); }
.rule-label { display: block; font-size: 0.74rem; font-weight: 500; color: var(--text-muted); margin-bottom: 6px; }
/* A rule's conditions, one line each: "When" / "and", what it goes by, its
   value, and a remove button once there's more than one. A device, country
   or language condition's list floats under its search box, and its picks
   are pills under the line (wrapped, even on a phone, where .chips is one
   scrolling line). On a phone the "When" / "and" goes above. */
.rule-conds { display: flex; flex-direction: column; gap: var(--sp-2); }
.rule-cond {
  display: grid; grid-template-columns: 38px minmax(0, 2fr) minmax(0, 3fr) 28px;
  align-items: center; gap: 6px var(--sp-2);
}
.rule-cond > .rule-label { margin: 0; }
.rule-cond .rule-picker { display: contents; }
.rule-search-field { position: relative; min-width: 0; }
.rule-cond .rule-picked { order: 1; grid-column: 2 / -1; margin: 0; padding: 0; flex-wrap: wrap; overflow: visible; }
.rule-cond-remove { width: 28px; height: 28px; }
@media (max-width: 560px) {
  .rule-cond { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 28px; }
  .rule-cond > .rule-label { grid-column: 1 / -1; margin-bottom: -2px; }
  .rule-cond .rule-picked { grid-column: 1 / -1; }
}
.rule-add-cond {
  display: inline-flex; align-items: center; gap: 4px; margin-top: var(--sp-2); padding: 4px 0;
  background: none; border: 0; color: var(--series-1); font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer;
}
.rule-add-cond:hover { text-decoration: underline; }
.rule-add-cond[hidden] { display: none; }
.rule-picked[hidden] { display: none; }
.rule-picked .chip { gap: 4px; }
.rule-options {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 30;
  max-height: 260px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 4px;
  box-shadow: 0 8px 24px rgba(20, 20, 40, 0.12);
}
.rule-options[hidden] { display: none; }
.rule-option {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: 7px 10px; border-radius: 6px; font-size: 0.86rem; cursor: pointer;
}
.rule-option:hover, .rule-option.active { background: var(--surface-2); }
.rule-option-code { flex: none; font-size: 0.74rem; color: var(--text-muted); text-transform: uppercase; }
.rule-option-none { padding: 7px 10px; font-size: 0.82rem; color: var(--text-muted); }
.rule-times { display: flex; align-items: center; gap: 6px; }
.rule-times input { flex: 1; min-width: 0; }
.rule-dash { flex: none; color: var(--text-muted); }
/* In the content card while its form is open in a rule. */
.content-away { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); font-size: 0.86rem; color: var(--text-secondary); }
.content-away[hidden] { display: none; }
.field-grid .field.wide { grid-column: 1 / -1; }
/* A short label next to a long URL. */
.field-grid.label-and-url { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
@media (max-width: 560px) { .field-grid.label-and-url { grid-template-columns: minmax(0, 1fr); } }

.unit-input { display: flex; gap: 6px; }
.unit-input input { flex: 1; min-width: 0; }
.unit-input select { flex: 0 0 auto; width: auto; }

/* A text input with a small button set inside its right edge (the link slug's
   "new slug" button). */
.input-action { position: relative; }
.input-action input { padding-right: 40px; }
.input-action .icon-btn {
  position: absolute;
  top: 50%;
  right: 4px;
  width: 29px;
  height: 29px;
  transform: translateY(-50%);
}
.icon-btn.spin .icon { animation: spin-once 0.5s ease; }

/* A date shown as plain text over an invisible datetime-local (see
   enhanceDateField() in js/expiry.js): fits any width, no browser arrow or
   calendar icon, and says "None" when empty. The clear button sits above the
   input so it stays tappable. */
.date-field {
  display: flex;
  align-items: center;
  width: 100%;
  height: 38px;
  padding: 0 40px 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page);
  font-size: 0.9rem;
  cursor: pointer;
  transition: border-color 0.12s var(--ease), box-shadow 0.12s var(--ease), background 0.12s var(--ease);
}
.date-field:focus-within {
  border-color: var(--series-1);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.date-display { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); }
.date-display.empty { color: var(--text-muted); }
.date-field input[type="datetime-local"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}
.date-field input[type="datetime-local"]:focus-visible { outline: none; }
.date-field .icon-btn { z-index: 1; }
@keyframes spin-once { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .icon-btn.spin .icon { animation: none; } }

.color-pair { display: flex; gap: 8px; }
.color-pair > div { flex: 1; min-width: 0; }

.error-text { color: var(--critical); font-size: 0.82rem; margin-top: var(--sp-2); }

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1) 65%);
  color: #fff;
  font-family: inherit;
  font-size: 0.88rem;
  font-weight: 650;
  line-height: 1.3;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: var(--shadow-accent);
  transition: filter 0.12s var(--ease), background 0.12s var(--ease), box-shadow 0.12s var(--ease), transform 0.08s var(--ease);
}

.btn:hover { filter: brightness(1.08); }
.btn:active { transform: translateY(1px); }
.btn.secondary { background: var(--surface); color: var(--text-primary); border-color: var(--border); box-shadow: var(--shadow-xs); }
.btn.secondary:hover { filter: none; background: var(--surface-2); }
.btn.danger { background: linear-gradient(155deg, color-mix(in srgb, var(--critical) 80%, #fff), var(--critical) 60%); color: #fff; box-shadow: 0 10px 24px -8px color-mix(in srgb, var(--critical) 45%, transparent); }
.btn.sm { padding: 7px 11px; font-size: 0.8rem; }
.btn.block { width: 100%; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; filter: none; transform: none; }

.btn-row { display: flex; gap: 8px; width: 100%; }
.btn-row .btn { flex: 1; }

/* Square, icon-only button. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  flex: none;
  transition: background 0.12s var(--ease), color 0.12s var(--ease), border-color 0.12s var(--ease);
}
.icon-btn:hover { background: var(--surface-2); color: var(--text-primary); }
.icon-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.icon-btn:disabled:hover { background: transparent; color: var(--text-secondary); }
.icon-btn.bordered:disabled:hover { background: var(--surface); }
.icon-btn.bordered { border-color: var(--border); background: var(--surface); box-shadow: var(--shadow-xs); }
.icon-btn.danger:hover { color: var(--critical); }

/* Buttons that keep their label on a wide screen and shrink to just the icon
   on a phone. */
@media (max-width: 560px) {
  .btn.collapse-sm { padding: 7px; width: 34px; height: 34px; }
  .btn.collapse-sm .btn-text { display: none; }
}

/* ==========================================================================
   Chips
   ========================================================================== */

.chips { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: var(--sp-3); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: var(--surface-2);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 0.74rem;
  font-weight: 550;
  line-height: 1.4;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s var(--ease), color 0.12s var(--ease), border-color 0.12s var(--ease);
}
.chip:hover { background: color-mix(in srgb, var(--text-primary) 8%, var(--surface-2)); }
.chip.active { background: var(--accent-soft); color: var(--series-1); }
/* A code's scans by type (renderTypeTabs()): link chips, each with its count,
   along the top of the stats card -- like Scans over time's range chips. */
.type-tabs { margin: 0 0 var(--sp-3); }
.type-tabs[hidden] { display: none; } /* .chips' display: flex would otherwise keep it on screen, empty */
.type-tabs .chip { text-decoration: none; }
.type-tabs .tab-count { font-variant-numeric: tabular-nums; opacity: 0.7; }
/* The type the code is now: a small dot on the tab's top-right corner. */
.type-tabs .chip { position: relative; overflow: visible; }
.type-tabs .tab-now {
  position: absolute; top: -2px; right: -2px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--good); box-shadow: 0 0 0 2px var(--surface);
}
.chip .icon { width: 13px; height: 13px; }
.chip:disabled { opacity: 0.4; cursor: not-allowed; }
/* Icon only (the content-type and map-mode pickers): a round button, the name
   shows as a tooltip. Kept at the original, larger size. */
#type-tabs, #geo-mode-tabs { gap: 6px; margin-bottom: var(--sp-4); }
.chip.icon-only {
  padding: 0;
  width: 35px;
  height: 35px;
  flex: none;
  justify-content: center;
  border-color: var(--border);
  background: var(--surface);
  font-size: 0.82rem;
  font-weight: 600;
}
.chip.icon-only .icon { width: 16px; height: 16px; }
.chip.icon-only:hover { background: var(--surface-2); }
.chip.icon-only.active {
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1));
  border-color: transparent;
  color: #fff;
  box-shadow: var(--shadow-accent);
}

/* The type picker with the Reset button pinned at its right, so on a phone the
   chips scroll beside it rather than carrying it off screen. */
.type-row { display: flex; align-items: flex-start; gap: 8px; margin-bottom: var(--sp-3); }
.type-row .chips { flex: 1; min-width: 0; margin-bottom: 0; }
.type-row .icon-btn { width: 35px; height: 35px; border-radius: 999px; border-color: var(--border); background: var(--surface); }

/* One scrolling line on a phone rather than three wrapped ones. */
@media (max-width: 600px) {
  .chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    margin-inline: -16px;
    padding-inline: 16px;
    scrollbar-width: none;
  }
  .chips::-webkit-scrollbar { display: none; }
  /* Rows that fade at their edges (.scroll-row) stay inside the card's padding. */
  .type-row .chips, .chips.scroll-row { margin-inline: 0; padding-inline: 0; }
}

/* One-line rows that scroll sideways at any width -- the type icons and the
   Styles (wireScrollRow() in js/generator/). They fade out at whichever
   edge there's more to scroll to rather than being cut off hard, and a mouse
   can drag them along. */
.scroll-row {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  --fade-l: 0px;
  --fade-r: 0px;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent);
}
.scroll-row::-webkit-scrollbar { display: none; }
.scroll-row.more-left { --fade-l: 28px; }
.scroll-row.more-right { --fade-r: 28px; }
.scroll-row.dragging { cursor: grabbing; user-select: none; }
.scroll-row.dragging * { cursor: grabbing; }
/* Room for the "now" dot on a tab's corner, which the scrolling would clip. */
.type-tabs.scroll-row { padding: 3px 3px 0; margin-inline: -3px; margin-top: -3px; }

/* ==========================================================================
   Badges
   ========================================================================== */

.badge {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 650;
  background: var(--surface-2);
  color: var(--text-secondary);
  vertical-align: 1px;
}
.badge.inactive { color: var(--critical); background: color-mix(in srgb, var(--critical) 10%, var(--surface-2)); }
.badge .icon { width: 11px; height: 11px; margin-right: 3px; vertical-align: -1px; }
.meta-line .expired { color: var(--critical); }

.status-toggle { display: inline-flex; align-items: center; gap: 7px; margin: 0; cursor: pointer; font-size: 0.78rem; font-weight: 600; }
.status-toggle .on { color: var(--good); }
.status-toggle .off { color: var(--text-muted); }

.chips.list-filter { margin-bottom: var(--sp-2); }
.chip .count { font-weight: 500; opacity: 0.7; font-variant-numeric: tabular-nums; }

/* Bin */
/* Database usage under the list: one muted line with a thin bar, opening to a
   few figures. Kept small so it never competes with the codes. */
.usage { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.usage > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.usage > summary::-webkit-details-marker { display: none; }
.usage > summary:hover { color: var(--text-secondary); }
.usage-bar { flex: 0 1 120px; min-width: 48px; height: 4px; border-radius: 2px; background: var(--surface-3); overflow: hidden; margin-left: auto; }
.usage-bar > span { display: block; height: 100%; border-radius: inherit; background: var(--text-muted); }
.usage-bar > span.warn { background: var(--series-2); }
.usage-bar > span.critical { background: var(--critical); }
.usage > summary .chev { flex: none; transition: transform 0.15s var(--ease); }
.usage[open] > summary .chev { transform: rotate(180deg); }
.usage-rows { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px var(--sp-4); margin: var(--sp-3) 0 0; font-size: 0.75rem; }
.usage-rows dt { color: var(--text-muted); }
.usage-rows dd { margin: 0; color: var(--text-secondary); }

.bin-note { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 var(--sp-2); }
.note-row { display: flex; align-items: center; gap: 8px; font-size: 0.78rem; color: var(--text-muted); }
.note-text { display: flex; align-items: flex-start; gap: 8px; flex: 1; min-width: 0; }
.note-row .icon { margin-top: 2px; }
.note-row .btn { margin-left: auto; flex: none; }
/* On a phone the text gets its own line and the button drops beneath it. */
@media (max-width: 560px) {
  .note-row { flex-wrap: wrap; }
  .note-text { flex-basis: 100%; }
  .note-row .btn { margin-left: 0; }
}

/* Selecting several codes */

/* The select button reads as "on" while selecting. */
.icon-btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--series-1); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); }

/* Ticks appear on every row while selecting. The box only shows the state; the
   whole row is the click target. */
.row-check { display: flex; align-items: center; flex: none; }
.row-check input { pointer-events: none; }
.select-mode .code-list-item { cursor: pointer; }
.code-list-item.selected,
.code-list-item.selected:hover { background: var(--accent-soft); }
.code-list-item.selected .status-dot { border-color: var(--accent-soft); }

.bulk-bar {
  position: sticky;
  top: calc(var(--header-h) + 8px);
  z-index: 5;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: var(--sp-3);
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: var(--shadow-md);
}
.bulk-bar .bulk-count { margin-right: auto; font-size: 0.82rem; font-weight: 600; white-space: nowrap; }
.bulk-bar .bulk-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.bulk-bar .btn:disabled { opacity: 0.45; cursor: not-allowed; }
.bulk-bar #bulk-all[aria-pressed="true"] { background: var(--accent-soft); color: var(--series-1); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); }
/* On a phone everything stays on one line: the actions shrink to icons (see
   .collapse-sm), and the count gives way with an ellipsis rather than wrapping. */
@media (max-width: 560px) {
  .bulk-bar { flex-wrap: nowrap; gap: 6px; padding: 8px; }
  .bulk-bar .bulk-count { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; padding-left: 4px; }
  .bulk-bar .bulk-actions { flex: none; flex-wrap: nowrap; gap: 4px; }
}

/* Schedule: the start / end dates, hidden behind a button until asked for. */
.schedule { padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
.schedule .schedule-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: var(--sp-2); }
.schedule .section-label { margin-bottom: 0; }
.schedule .schedule-head .icon-btn { width: 28px; height: 28px; }
.schedule .field { margin-bottom: 0; }
.schedule .hint { margin-top: var(--sp-2); }
.schedule .hint.bad { color: var(--critical); }

/* Notes: a small marker on a list row, and the section on a code's page. */
.code-list-item .meta .note-flag { display: inline-flex; margin-left: 8px; vertical-align: -2px; color: var(--text-muted); }
.notes { margin-top: var(--sp-4); }
.notes .notes-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: var(--sp-2); }
.notes .section-label { margin-bottom: 0; }
.notes .notes-head .icon-btn { width: 28px; height: 28px; }
.notes-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.88rem;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 11px 13px;
}
.notes-empty {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 10px 13px;
  border: 1px dashed var(--baseline);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.12s var(--ease), color 0.12s var(--ease), border-color 0.12s var(--ease);
}
.notes-empty:hover { background: var(--surface-2); color: var(--text-secondary); border-color: var(--text-muted); }
.notes-actions { display: flex; gap: 8px; margin-top: var(--sp-2); }

.row-actions { display: flex; gap: 2px; flex: none; }
.bin-item .kind-icon, .bin-item .info strong { opacity: 0.6; }
.icon-btn.danger { color: color-mix(in srgb, var(--critical) 70%, var(--text-muted)); }
.icon-btn.danger:hover { color: var(--critical); background: color-mix(in srgb, var(--critical) 10%, transparent); }

/* Not working when scanned (switched off or expired): quieter than the rest. */
.code-list-item.is-off .kind-icon svg,
.code-list-item.is-off .info strong,
.code-list-item.is-off .scans { opacity: 0.55; }

.tag { display: inline-block; padding: 0 6px; border-radius: var(--radius-xs); font-size: 0.68rem; font-weight: 650; }
.tag.new { background: var(--accent-soft); color: var(--series-1); }
.tag.repeat { background: var(--surface-2); color: var(--text-muted); }

/* ==========================================================================
   Generator
   ========================================================================== */

/* Every card is a direct child, and on mobile the preview is last -- a
   floating thumbnail (.qr-float, wired up in wireFloatPreview() in
   js/generator/) stands in for it everywhere above, right up until this,
   the real full-size card, scrolls into view. */
.gen-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "content" "dynamic" "style" "preview";
  gap: var(--sp-3);
  align-items: start;
}
.gen-grid > .card, .gen-main > .card { margin: 0; }
/* The content card with the Rules card under it, as one column. */
.gen-main { grid-area: content; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.gen-preview { grid-area: preview; }
.gen-dynamic { grid-area: dynamic; }
.gen-style { grid-area: style; }

@media (min-width: 860px) {
  .gen-grid {
    grid-template-columns: minmax(0, 1fr) 320px;
    grid-template-areas: "content preview" "dynamic preview" "style preview";
    gap: var(--sp-4) var(--sp-5);
  }
}


.preview-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  /* Without an explicit width this flex column shrink-wraps to its widest
     child instead of filling the grid column -- and that widest child
     changes (the 280px frame, then the wider "Model / Version / Mask" line
     once a code exists), so the whole card visibly resized itself depending
     on whether anything had been generated yet. Fill the column always. */
  width: 100%;
}
@media (min-width: 860px) {
  .preview-box { position: sticky; top: calc(var(--header-h) + var(--sp-4)); }
}

.preview-frame {
  width: 100%;
  max-width: 280px;
  aspect-ratio: 1;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.preview-frame { position: relative; }
.preview-frame canvas { position: relative; max-width: 100%; max-height: 100%; }
/* Shows through while the canvas is empty; a drawn code covers it. */
.preview-frame .placeholder { position: absolute; width: 48%; height: 48%; color: #d7d5ca; stroke-width: 1.5; }

.preview-hint { display: flex; align-items: flex-start; gap: 6px; font-size: 0.78rem; color: var(--text-muted); }
/* Takes no space (and adds no extra gap) until there's something to say. */
.preview-hint:empty { display: none; }
.preview-hint.warn { color: var(--series-2); }

/* The floating stand-in for the preview card (mobile only -- the sticky
   sidebar already keeps it in view at 860px+, see .preview-box above). Fixed
   to the corner, out of the page's flow entirely, so it sits over whatever's
   scrolled underneath it. wireFloatPreview() in js/generator/ toggles the
   .visible class and moves the real canvas in and out of .qr-float-canvas as
   that scrolls in and out of view; it starts [hidden] so it can't flash empty
   before that first runs. Tapping it opens .qr-peek-dialog below, rather than
   jumping the page down to the real card -- it's meant for a quick look
   without losing your place in the form. */
/* Holds the thumbnail and its hide button, fixed to the corner. It takes no
   clicks itself -- the buttons inside decide that for themselves. */
.qr-float-wrap {
  position: fixed;
  left: var(--sp-4);
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  /* ~46% of the viewport width, same idea as vw, but clamped so it can't
     shrink into an unreadable speck on a small phone or, since this rule's
     shared with tablet-width screens right up to 859px, balloon into
     something absurd there. */
  width: clamp(140px, 46vw, 240px);
  z-index: 30;
  pointer-events: none;
  /* A swipe drags this whole wrapper, hide button included; this eases it
     back once let go (the drag itself sets transition: none inline). */
  transition: transform 0.18s ease, opacity 0.18s ease;
}
.qr-float {
  display: block;
  width: 100%;
  /* Shrinks into the corner, where the bubble takes over, when hidden. */
  transform-origin: bottom left;
  /* Horizontal drags are a swipe to hide it and a press-and-hold shows the
     hide button (see wireFloatPreview()), so only vertical drags scroll the
     page, and neither gesture should select text or open the browser's own
     long-press menu. */
  touch-action: pan-y;
  user-select: none;
  -webkit-touch-callout: none;
  /* Height follows the content (this padding, plus the canvas at its own
     real proportions) instead of being forced to match width. A Frame
     banner makes the code taller than it is wide; forcing a square made it
     fill that square by height with zero gap top/bottom while the leftover
     width became gaps on the sides instead -- lopsided padding. This keeps
     the gap even on every side, frame or not. */
  height: auto;
  /* No padding by default -- like .preview-frame, a code with no margin set
     should reach the widget's own edge exactly, not sit behind an arbitrary
     gap. .has-frame (below) adds it back only once there's a Frame: that's
     what actually needs the gap, to keep the code's own baked-in border from
     touching this widget's border (see the comment on .has-frame). */
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(10px) scale(0.94);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s;
}
/* The code's own Frame already draws a border right at its own edge (see
   FRAME_BORDER_MODULES in js/qr-render.js) -- a second one immediately
   outside it, from this widget's own chrome, looked like a doubled border.
   render() in js/generator/ toggles this, since it's the one place that
   already knows whether the current style has a frame. border-color, not
   border: none, so the border-box size (and the padding around it) doesn't
   shift by a pixel when this toggles. */
.qr-float.has-frame { border-color: transparent; padding: var(--sp-3); }
.qr-float.visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.qr-float.visible:active { transform: scale(0.96); }
.qr-float-wrap.collapsed .qr-float:not(.visible) { transform: scale(0.2); }
/* Their display values would otherwise override [hidden]. */
.qr-float[hidden], .qr-float-bubble[hidden] { display: none; }

/* Shows on hover, on keyboard focus, or -- on touch screens, which have no
   hover -- after a press-and-hold (.show-hide, set by wireFloatPreview()). */
.qr-float-hide {
  position: absolute;
  /* Straddles the thumbnail's corner rather than sitting inside it. */
  top: -10px;
  right: -10px;
  width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--series-1);
  color: #fff;
  box-shadow: var(--shadow-pop);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.8);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.qr-float:not(.visible) ~ .qr-float-hide { visibility: hidden; }
.qr-float-wrap.show-hide .qr-float.visible ~ .qr-float-hide,
.qr-float.visible ~ .qr-float-hide:focus-visible {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
@media (hover: hover) {
  .qr-float-wrap:hover .qr-float.visible ~ .qr-float-hide { opacity: 1; pointer-events: auto; transform: none; }
}

.qr-float-bubble {
  position: fixed;
  left: var(--sp-4);
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  width: 48px;
  height: 48px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--series-1);
  color: #fff;
  box-shadow: var(--shadow-pop);
  z-index: 30;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: scale(0.5);
  transform-origin: bottom left;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s;
}
.qr-float-bubble.visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition: opacity 0.18s ease 0.08s, transform 0.18s ease 0.08s;
}
.qr-float-bubble.visible:active { transform: scale(0.92); }
.qr-float-bubble .icon { width: 26px; height: 26px; }
.qr-float-canvas {
  /* No padding *inside* this box -- like .preview-frame, the only whitespace
     around the modules should be the quiet zone actually baked into the
     drawn canvas (the Margin style setting), not an extra fixed gap on top
     of it. The gap on every side is .qr-float's own padding above, which
     now comes out even because both size to the content's real aspect
     ratio instead of a forced square. min-height is only a floor, so a
     placeholder icon (nothing drawn yet -- see below) has something to
     centre against instead of collapsing to nothing. */
  position: relative;
  width: 100%;
  min-height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
}
.qr-float-canvas canvas { position: relative; display: block; width: 100%; height: auto; }
.qr-float-canvas .placeholder { position: absolute; width: 48%; height: 48%; color: #d7d5ca; stroke-width: 1.5; }
@media (min-width: 860px) { .qr-float-wrap, .qr-float-bubble { display: none !important; } }

/* The floating thumbnail's popup: a closer look at the same canvas (moved in
   while open, moved back to wherever it belongs on close -- see
   wireFloatPreview()) plus the same Copy/PNG/SVG actions as the real card
   (see wireDownloadTrio()). Centred by the browser's own <dialog> behaviour
   (position: fixed + auto margins, from the UA stylesheet) -- deliberately
   not overridden with a position of our own here, which would break that. */
.qr-peek-dialog { max-width: 340px; padding: var(--sp-4); }
.qr-peek-canvas {
  /* Same no-extra-padding rule as .qr-float-canvas above. */
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin-bottom: var(--sp-3);
}
.qr-peek-canvas canvas { display: block; width: 100%; height: auto; }
.preview-hint.error { color: var(--critical); }
.preview-hint .icon { margin-top: 2px; }

/* Model / version / mask: reference facts, so kept small and quiet. */
.qr-info {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px 12px;
  margin: 0;
  font-size: 0.72rem;
  color: var(--text-muted);
}
.qr-info:empty { display: none; }
.qr-info div { display: inline-flex; gap: 4px; }
.qr-info dt { font-weight: 500; }
.qr-info dt::after { content: ":"; }
.qr-info dd { margin: 0; color: var(--text-secondary); font-variant-numeric: tabular-nums; }

.edit-banner {
  display: flex;
  gap: 10px;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--series-1) 30%, transparent);
  border-radius: var(--radius-sm);
  padding: 11px 13px;
  margin-bottom: var(--sp-4);
  font-size: 0.88rem;
}
.edit-banner { align-items: center; }
.edit-banner > .icon { color: var(--series-1); flex: none; }
/* What's being edited and the way back on one line; a note, when there is
   one, goes underneath. */
.edit-banner > div { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--sp-3); }
.edit-banner .hint { order: 2; flex-basis: 100%; margin-top: 1px; }
/* One line however long the slug: it shortens, the link keeps its place. */
#edit-title { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.edit-banner .hint:empty { display: none; }
.edit-banner a { display: inline-flex; flex: none; align-items: center; gap: 4px; margin-left: auto; font-size: 0.8rem; text-decoration: none; white-space: nowrap; }
.edit-banner a:hover { text-decoration: underline; }

/* The Dynamic card: a header row with the switch, and fields that unfold. */
.dyn-head { display: flex; align-items: center; gap: var(--sp-3); }
/* Only has text for a type that can't be dynamic (WiFi); empty, its margin
   would sit under the header and push it off-centre in the card. */
#dyn_hint:empty { display: none; }
.dyn-head .dyn-icon {
  width: 36px; height: 36px; border-radius: var(--radius-xs); flex: none;
  background: linear-gradient(155deg, var(--series-1-light), var(--series-1));
  color: #fff;
  box-shadow: var(--shadow-accent);
  display: flex; align-items: center; justify-content: center;
}
.dyn-head .dyn-text { flex: 1; min-width: 0; }
.dyn-head .dyn-title { font-weight: 600; font-size: 0.92rem; }
.dyn-fields, .gen-style .dyn-head + .settings-group { margin-top: var(--sp-5); }

.link-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 4px 4px 11px;
  margin: var(--sp-2) 0;
}
.link-row code { flex: 1; min-width: 0; word-break: break-all; }
.link-more { display: inline-flex; align-items: center; gap: 5px; font-size: 0.82rem; text-decoration: none; }
.link-more:hover { text-decoration: underline; }

/* Collapsible block. */
details.fold > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 0;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-secondary);
  user-select: none;
}
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary:hover { color: var(--text-primary); }
details.fold > summary .chev { margin-left: auto; transition: transform 0.15s var(--ease); color: var(--text-muted); }
details.fold[open] > summary .chev { transform: rotate(180deg); }
details.fold > .fold-body { padding: 8px 0 2px; }

.swatch {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 3px;
  border: 1px solid var(--border);
  margin-right: 5px;
  vertical-align: -1px;
}


/* ==========================================================================
   Dashboard: code list
   ========================================================================== */

.list-head h1 { white-space: nowrap; }
.list-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.list-head .actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* Sort: a secondary button naming the order in use, opening a menu like the
   app's others (see wireSort()), lined up with its right edge. */
.sort-menu { flex: none; }
/* The same colour as the icon buttons beside it, darkening the same way on hover. */
.sort-btn { height: 34px; gap: 6px; padding: 0 10px; font-size: 0.82rem; white-space: nowrap; }
.btn.secondary.sort-btn { color: var(--text-secondary); }
.btn.secondary.sort-btn:hover, .btn.secondary.sort-btn[aria-expanded="true"] { color: var(--text-primary); }
.sort-btn .sort-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: left; }
.sort-btn .sort-chev { color: var(--text-muted); transition: transform 0.15s var(--ease); }
.sort-btn[aria-expanded="true"] .sort-chev { transform: rotate(180deg); }
.sort-menu .menu { left: auto; right: 0; top: calc(100% + 6px); min-width: 180px; z-index: 20; }
.sort-menu .menu .sort-check { visibility: hidden; color: var(--series-1); }
.sort-menu .menu [aria-checked="true"] { color: var(--series-1); background: var(--accent-soft); font-weight: 600; }
.sort-menu .menu [aria-checked="true"] .sort-check { visibility: visible; }
/* Five controls next to the heading no longer fit on one line on a phone or
   small tablet -- stack the heading above them instead of letting either get
   squeezed (a wrapped title, or the New button dropping onto its own line). */
@media (max-width: 700px) {
  .list-head { flex-direction: column; align-items: stretch; }
  /* One row: the sort button takes whatever width the icon buttons leave, so
     its label ("Newest first"...) is never cut off. */
  .list-head .actions { flex-wrap: nowrap; justify-content: flex-end; }
  .sort-menu { flex: 1 1 auto; }
  .sort-btn { width: 100%; }
  /* The sort button sits at the left of the row here, so its menu opens
     rightwards from it instead of off the left edge of the screen. */
  .sort-menu .menu { left: 0; right: auto; max-width: calc(100vw - 2 * var(--sp-4)); }
}

/* The list's search box: a magnifier on the left, and a clear button on the
   right once something has been typed. */
.search-box { position: relative; margin-bottom: var(--sp-3); }
.search-box .search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  width: 15px;
  height: 15px;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}
.search-box input { padding-left: 36px; padding-right: 36px; }
.search-box .icon-btn {
  position: absolute;
  top: 50%;
  right: 4px;
  width: 28px;
  height: 28px;
  transform: translateY(-50%);
}

.code-list { margin: 0 calc(var(--sp-2) * -1); }

.code-list-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 10px var(--sp-2);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: inherit;
  transition: background 0.1s var(--ease);
}
.code-list-item:hover { background: var(--surface-2); }

.kind-icon {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-xs);
  background: var(--accent-soft);
  color: var(--series-1);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

/* Status dot on the icon's corner: green = active, grey = switched off, red =
   expired. The ring is the row's own background so it looks cut out. */
.kind-icon { position: relative; }
.status-dot {
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid var(--surface);
  background: var(--baseline);
}
.status-dot.live { background: var(--good); }
.status-dot.expired { background: var(--critical); }
.status-dot.scheduled { background: var(--series-2); }
.code-list-item:hover .status-dot { border-color: var(--surface-2); }

.code-list-item .info { flex: 1; min-width: 0; }
.code-list-item .title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.code-list-item .title strong { flex: 0 1 auto; min-width: 0; max-width: 45%; font-size: 0.92rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-list-item .title .payload { flex: 1 1 0; min-width: 0; color: var(--text-muted); font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-list-item .meta { color: var(--text-muted); font-size: 0.76rem; margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-list-item .meta .badge { margin-left: 8px; }
.code-list-item .scans { text-align: right; white-space: nowrap; line-height: 1.2; }
.code-list-item .scans .big { font-size: 1rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.code-list-item .scans .small { font-size: 0.7rem; color: var(--text-muted); }
.code-list-item .go { color: var(--text-muted); flex: none; }

@media (max-width: 700px) {
  .code-list-item .meta .when { display: none; }
  .code-list-item .go { display: none; }
  .code-list-item .scans .small { display: none; }
}
/* Phones only: keep each row to two clean lines. The destination text is the
   noisy part, so it goes (it's on the code's page) and the label gets all the
   room the scan count leaves it. Tablet widths have room for it, cut off with
   an ellipsis as needed. */
@media (max-width: 520px) {
  .code-list-item .title .payload { display: none; }
  .code-list-item .title strong { max-width: none; }
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  padding: 30px 16px;
  color: var(--text-muted);
  font-size: 0.85rem;
  margin: 0;
}
.empty-state .icon { width: 22px; height: 22px; }
.empty-state.inline { padding: 14px 0; }

/* ==========================================================================
   Dashboard: detail
   ========================================================================== */

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  color: var(--text-secondary);
  font-size: 0.82rem;
  font-weight: 550;
  margin-bottom: var(--sp-3);
}
.back-link:hover { color: var(--text-primary); }

.overview {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-4) var(--sp-5);
  align-items: start;
}
.ov-main { min-width: 0; display: flex; flex-direction: column; gap: 0; }
.ov-main > .error-text { margin-top: var(--sp-2); }
.ov-fold { grid-column: 1 / -1; border-top: 1px solid var(--gridline); padding-top: 4px; }

@media (max-width: 600px) {
  .overview { grid-template-columns: minmax(0, 1fr); }
  .ov-main { order: -1; }
}

.ov-head h1 { overflow-wrap: anywhere; }
/* One uniform layout for the whole top of the page: the title and its two
   buttons on the first line, then every detail as an identical full-width row
   (small label left, value right-of-it, hairline between) -- the short link,
   what it opens, its status, dates and so on all the same shape. */
.ov-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 0 var(--sp-3); }
.ov-head .titles { display: contents; }
.ov-head h1 { grid-column: 1; grid-row: 1; }
.ov-head .actions { grid-column: 2; grid-row: 1; }
.ov-head .dest { grid-column: 1 / -1; margin-top: var(--sp-3); }
.ov-head .meta-line { grid-column: 1 / -1; margin-top: 0; }
.dest, .meta-line { display: flex; flex-direction: column; gap: 0; font-size: 0.82rem; }
.info-row { display: flex; align-items: center; gap: var(--sp-3); height: 32px; min-width: 0; padding: 0; border-top: 1px solid var(--gridline); }
.info-label { flex: none; width: 84px; font-size: 0.7rem; font-weight: 650; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted); }
.info-value { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 5px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.info-row.expired .info-value { color: var(--critical); }
.info-row .status-toggle { font-size: 0.82rem; }
.ov-head .actions { display: flex; gap: 6px; flex: none; }


.ov-qr { display: flex; flex-direction: column; gap: var(--sp-2); align-items: stretch; width: 188px; }
.ov-qr .btn-row { gap: 6px; }
.ov-qr .btn-row .btn { padding-inline: 4px; }
.qr-canvas-wrap { background: #fff; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px; }
/* No forced aspect-ratio: the canvas's own width/height attributes (set when
   it's drawn) are its real intrinsic ratio -- square normally, taller when a
   style has a frame banner -- and this lets that ratio through instead of
   stretching the image to fit a hardcoded square. */
.qr-canvas-wrap canvas { display: block; width: 100%; height: auto; }

@media (max-width: 600px) {
  /* Phones: the code centred above four equal buttons in a 2 x 2 grid, all
     full width -- one tidy column, like every other section. */
  .ov-qr { flex-direction: column; align-items: stretch; width: auto; }
  .ov-qr .qr-canvas-wrap { width: 180px; align-self: center; }
}

.btn-col { display: flex; flex-direction: column; gap: 6px; }
.btn-col .btn-row { flex-wrap: wrap; }
@media (max-width: 600px) {
  .btn-col .btn-row { display: grid; grid-template-columns: 1fr 1fr; }
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--sp-2);
}

.stat-tile {
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  min-width: 0;
  transition: border-color 0.15s var(--ease);
}
.stat-tile .label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.7rem;
  font-weight: 550;
  color: var(--text-muted);
  white-space: nowrap;
}
.stat-tile .label span { overflow: hidden; text-overflow: ellipsis; }
.stat-tile .value { font-size: 1.34rem; font-weight: 700; letter-spacing: -0.01em; line-height: 1.2; margin-top: 3px; font-variant-numeric: tabular-nums; }

@media (max-width: 400px) {
  .stat-tile { padding: 8px; }
  .stat-tile .label .icon { display: none; }
}
.panel-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}
/* Two across once there's room; an odd card at the end spans the row. */
@media (min-width: 640px) {
  .panel-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .panel-grid > .card:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
.panel-grid > .card { margin: 0; padding: var(--sp-4) var(--sp-5); }
.panel-grid .card-head { margin-bottom: var(--sp-2); }
.card.panel { padding: var(--sp-4) var(--sp-5); margin-top: var(--sp-3); }
.card.panel .card-head { margin-bottom: var(--sp-3); }

/* Bar breakdowns */
.bar-row { display: flex; align-items: center; gap: var(--sp-3); padding: 5px 0; font-size: 0.82rem; }
.bar-row .bar-label { width: clamp(84px, 34%, 132px); flex-shrink: 0; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-row .bar-track { flex: 1; background: var(--surface-2); border-radius: 5px; height: 8px; position: relative; overflow: hidden; }
.bar-row .bar-fill { background: linear-gradient(90deg, var(--series-1), color-mix(in srgb, var(--series-1) 75%, #7aa3ef)); border-radius: 5px; height: 8px; }
/* Link clicks: one line per link -- an icon for what it is (Button, Title
   link...; the word is its tooltip), then its name, in the same width as the
   other breakdowns so the bars line up with theirs. A long name slides to
   show the rest, like all cut-off text (see "Cut-off text" below). Links no
   longer on the page greyed; tracking off dims the lot. */
.click-row .bar-label { display: flex; align-items: center; gap: 6px; }
.click-row .click-name { flex: 0 1 auto; min-width: 0; white-space: nowrap; color: var(--text-primary); }

/* Cut-off text, anywhere on the site: one line that ends in "…" and slides
   sideways to show the rest -- a swipe, a trackpad, Shift + wheel, or
   dragged with the mouse (see wireRevealText() in page-ui.js, whose
   REVEAL_TEXT lists the same elements). Once slid, the "…" would stay where
   it was and hide the end, so the text is clipped instead, fading at
   whichever edge has more to show. */
:is(.logo-filename, .history-title, .history-meta, .ov-head h1, .info-value:not(.editing, .status-value),
  .code-list-item .title strong, .code-list-item .title .payload, .code-list-item .meta,
  .bar-row:not(.click-row) .bar-label, .click-row .click-name) {
  overflow-x: auto; overflow-y: hidden; text-overflow: ellipsis; white-space: nowrap;
  scrollbar-width: none; overscroll-behavior-x: contain;
}
:is(.logo-filename, .history-title, .history-meta, .ov-head h1, .info-value, .code-list-item .title strong,
  .code-list-item .title .payload, .code-list-item .meta, .bar-row .bar-label, .click-row .click-name)::-webkit-scrollbar { display: none; }
.reveal-more { cursor: grab; }
.reveal-dragging, .reveal-dragging * { cursor: grabbing; user-select: none; }
.reveal-scrolled {
  text-overflow: clip !important; /* over the rule above, which is more specific */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 14px, #000 calc(100% - 14px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 14px, #000 calc(100% - 14px), transparent);
}
.reveal-scrolled.reveal-end {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 14px);
  mask-image: linear-gradient(to right, transparent, #000 14px);
}
.click-row .click-kind-icon { display: inline-flex; flex: none; color: var(--text-muted); }
.click-row .click-kind-icon .icon { width: 13px; height: 13px; display: block; }
.click-row.old .click-name { color: var(--text-muted); }
.click-row.old .bar-fill { background: var(--text-muted); opacity: 0.5; }
.click-row.old .bar-value { color: var(--text-muted); font-weight: 400; }
/* A thin rule between the page's current links and the old ones below. */
.click-row:not(.old) + .click-row.old { margin-top: var(--sp-2); padding-top: calc(5px + var(--sp-2)); border-top: 1px solid var(--border); }
/* A rule's conditions take more room than a link's name. */
#redirect-chart .bar-label { width: clamp(120px, 52%, 280px); }
#clicks-panel.tracking-off #click-chart { opacity: 0.55; }
.bar-row .bar-value { width: 32px; text-align: right; flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--text-secondary); font-weight: 550; }


.scan-map { display: block; width: 100%; height: auto; border-radius: var(--radius-sm); overflow: hidden; }
.map-bg { fill: var(--surface-2); }
.map-land { fill: var(--border); stroke: none; }
.map-grid line { stroke: var(--gridline); stroke-width: 0.5; stroke-opacity: 0.6; }
.map-dot { fill: var(--series-1); fill-opacity: 0.7; stroke: var(--series-1); stroke-width: 0.4; cursor: default; }
.map-dot:hover { fill-opacity: 0.9; }

#time-chart svg { touch-action: pan-y; }

/* Recent scans */
.table-wrap { margin: 0 calc(var(--sp-5) * -1) -6px; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
th, td { text-align: left; padding: 9px var(--sp-5); border-top: 1px solid var(--gridline); vertical-align: top; }
th { color: var(--text-muted); font-weight: 650; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; border-top: none; padding-top: 2px; padding-bottom: 7px; }
td .sub { display: block; color: var(--text-muted); font-size: 0.73rem; margin-top: 1px; }
td.nowrap { white-space: nowrap; }
td.empty { padding: 20px var(--sp-5); text-align: center; color: var(--text-muted); }

/* The delete button in Recent scans' last column: as little width as the
   column can take, so a fourth column doesn't push the other three (already
   sized to just fit a phone) into wrapping the table sideways. */
#recent-table th:last-child, #recent-table td:last-child { width: 1%; padding-left: 0; padding-right: var(--sp-2); }
#recent-table td:last-child .icon-btn { width: 28px; height: 28px; }

/* ==========================================================================
   Dialog
   ========================================================================== */

dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-pop);
  padding: var(--sp-5);
  width: calc(100% - 32px);
  max-width: 380px;
}
dialog::backdrop { background: rgba(10, 10, 8, 0.5); backdrop-filter: blur(2px); }
dialog h2 { font-size: 1.05rem; }
/* Last in the markup so opening the dialog doesn't focus it first. */
.dialog-close { position: absolute; top: 12px; right: 12px; }

.dialog-actions { display: flex; gap: 8px; }

/* With a Frame on, the code draws its own border and rounded corners, so the
   popup's grey box around it (and its white fill, which would show in those
   rounded corners) go away -- same idea as .qr-float.has-frame above. Toggled
   by render() in js/generator/. */
.qr-peek-dialog.has-frame .qr-peek-canvas { border-color: transparent; background: transparent; }
.qr-float.has-frame .qr-float-canvas { background: transparent; }

/* Mobile: the full-size preview card at the bottom stays out of sight while
   the floating thumbnail is standing in for it, then eases in (fade + grow)
   once the thumbnail has gone -- see wireFloatPreview() in js/generator/,
   which toggles .float-hidden. It keeps its space in the layout meanwhile, so
   nothing jumps when it appears. */
@media (max-width: 859px) {
  .gen-preview {
    transition: opacity 0.35s ease, transform 0.35s var(--ease), visibility 0s;
    transform-origin: bottom left;
  }
  .gen-preview.float-hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(12px) scale(0.92);
    transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
  }
}

/* Same for the full-size preview card's own frame (its grey border and white
   fill) -- see .qr-peek-dialog.has-frame above. */
.preview-frame.has-frame { border-color: transparent; background: transparent; }

/* The page's cards start at the container's own side padding (see .container),
   which widens at 720px -- keep the corner thumbnail's left edge on it. */
@media (min-width: 720px) { .qr-float-wrap, .qr-float-bubble { left: var(--sp-5); } }

/* Notes as one more row of the details list, and in-place editing. */
.notes { margin-top: 0; }
.notes-row { height: auto; min-height: 32px; padding: 3px 0; }
.notes-row .info-label { padding-top: 0; }
.notes-row .info-value { white-space: pre-wrap; overflow: visible; overflow-wrap: anywhere; display: block; align-self: center; font-size: 0.82rem; line-height: 1.5; }
.notes-row .icon-btn { flex: none; width: 24px; height: 24px; }
.info-value.editing { overflow: visible; }

/* In-place inputs: no grey fill, and short enough to sit inside a normal
   detail row (min-height 38px) so switching to edit never changes the layout. */
.inline-edit { flex: 1; min-width: 0; width: 100%; height: 24px; padding: 0 8px; font-size: 0.82rem; font-family: inherit; border-radius: var(--radius-xs); background: transparent; }
.info-value.editing { display: flex; align-items: center; gap: 6px; }
.info-value.editing .icon-btn { flex: none; width: 24px; height: 24px; }
.inline-edit-title { grid-column: 1; grid-row: 1; height: 36px; font-size: 1.2rem; font-weight: 800; letter-spacing: -0.02em; padding: 0 10px; }
.notes textarea { background: transparent; margin-top: 2px; }
.info-row a { color: var(--series-1); text-decoration: none; }
.info-row a:hover { text-decoration: underline; }
/* The password's show/hide eye sits inside its input, on the right. */
.pw-wrap { position: relative; flex: 1; min-width: 0; display: block; }
.pw-wrap .inline-edit { padding-right: 28px; }
.info-value.editing .pw-wrap .icon-btn { position: absolute; right: 0; top: 0; width: 24px; height: 24px; }

/* Nothing in the header may change height when Edit is pressed: the first row
   has a fixed minimum, every button in it is the same height, and the swap of
   Edit / Delete for Save / Cancel keeps that. */
.ov-head { grid-template-rows: minmax(36px, auto); }
.ov-head .actions { align-self: center; }
.ov-head .actions .btn, .ov-head .actions .icon-btn { height: 34px; box-sizing: border-box; }
.ov-head h1 { line-height: 36px; }

/* Scans-over-time legend, and matching alignment of the two in-place icons:
   the date picker's calendar and the password's eye share a size, box and
   right-hand offset. */
.chart-legend { display: inline-flex; gap: var(--sp-3); font-size: 0.75rem; color: var(--text-secondary); }
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
.chart-legend i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.inline-edit[type="datetime-local"] { padding-right: 0; }
.inline-edit[type="datetime-local"]::-webkit-calendar-picker-indicator {
  width: 24px; height: 24px; margin: 0; padding: 0; cursor: pointer;
  background-size: 14px; background-position: center; background-repeat: no-repeat;
}

/* The mobile "16px so iOS doesn't zoom" rule above is more specific than
   .inline-edit, so on a phone it made these inputs bigger than the text they
   replace. Same size as the row text on every screen instead. */
.info-value .inline-edit[type], .notes textarea { font-size: 0.82rem; }
.ov-head .inline-edit-title[type] { font-size: 1.2rem; }

/* Download button + its format menu. */
.menu-wrap { position: relative; flex: 1; min-width: 0; }
.menu-wrap > .btn { width: 100%; gap: 6px; }
/* The dashboard's QR details card sits in its button row the same way. */
.btn-col .btn-row > .qr-info-pop { flex: 1; min-width: 0; }
.btn-col .btn-row > .qr-info-pop > .btn { width: 100%; }
.menu {
  position: absolute; left: 0; top: calc(100% + 4px); z-index: 15; width: max-content; min-width: 160px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md); padding: 4px;
}
.menu[hidden] { display: none; }
.menu button {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--radius-xs);
  background: transparent; color: var(--text-primary); font: inherit; font-size: 0.82rem; text-align: left; cursor: pointer;
}
.menu button:hover { background: var(--surface-2); }
/* The Recent scans card's Export menu: its own width, opening leftwards from
   the card's right edge. */
.card-head .export-menu { flex: none; }
.card-head .export-menu .menu { left: auto; right: 0; }
/* On a phone it's icon-only (.collapse-sm): keep it a circle rather than the
   full width .menu-wrap gives its button. */
@media (max-width: 560px) {
  .card-head .export-menu > .btn.collapse-sm { width: 34px; height: 34px; flex: none; }
}

/* One text size for every value in the details list. The Notes and Design rows
   sit outside the .dest / .meta-line boxes that set it, so it is set on the
   row itself, and the status text and links follow it too. */
.info-row { font-size: 0.82rem; line-height: 1.4; }
.info-row .info-value, .info-row .status-toggle, .info-row a { font-size: inherit; font-weight: 400; }
.info-row .status-toggle { font-weight: 600; }

/* The popup's "nothing generated yet" icon, centred behind the (empty) canvas
   like the one in the real preview frame and the corner thumbnail. */
.qr-peek-canvas { position: relative; min-height: 120px; }
.qr-peek-canvas .placeholder { position: absolute; top: 50%; left: 50%; width: 48%; height: 48%; transform: translate(-50%, -50%); color: #d7d5ca; stroke-width: 1.5; }
.qr-peek-canvas canvas { position: relative; }

/* A code's title on its page: smaller so most labels fit on one line, and any
   that still don't are cut off with an ellipsis instead of wrapping (the full
   text is in the edit box). */
.ov-head h1 { font-size: 1.2rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Long values (a destination link, a note) end in an ellipsis. It needs a block
   box: text-overflow does nothing on the inline-flex box these values were, so
   the text was just cut off at the edge. Icons in a value sit inline with it. */
.info-row .info-value { display: block; }
.info-row .info-value .icon { vertical-align: -2px; margin-right: 5px; }
.info-row .info-value.editing { display: flex; }
/* As a block, the switch sat on the text baseline, a little below the row's
   middle; as a flex row it's centred like the other values. */
.info-row .info-value.status-value { display: flex; align-items: center; }

/* Touching and holding a chart to read its numbers shouldn't start selecting
   the page's text (or open the browser's copy / image menu). Vertical swipes
   still scroll the page; a horizontal drag scrubs the chart. */
#time-chart, #time-chart *, #location-map, #location-map * {
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
#time-chart svg, #location-map svg { touch-action: pan-y; }
.range-chips { margin-bottom: var(--sp-2); }
/* Range chips: even gaps, never squeezed. */
.range-chips { gap: 4px var(--sp-2); }
.range-chips .chip { flex: none; }
/* Custom range: From and To dates and a Show button, in a row that wraps on a phone. */
.range-custom { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.range-custom[hidden] { display: none; }
/* margin 0: a label's usual space underneath would lift the dates above Show. */
.range-custom label { display: flex; flex-direction: column; gap: 4px; margin: 0; font-size: 0.8rem; color: var(--text-secondary); }
.range-custom input[type="date"] { width: auto; min-width: 9.5rem; }
/* The dates and Show the same height, so their bottoms line up whatever text
   size each has (the dates' grows to 16px on a phone). */
.range-custom input[type="date"], .range-custom .btn { box-sizing: border-box; height: 40px; margin: 0; }
.range-custom .btn { padding-block: 0; }

/* Scans over time: three tidy left-aligned rows -- the title (a button that
   flips between the line and bar graph), the range chips, then the legend. */
.time-head { margin-bottom: var(--sp-2); }
.time-title { display: flex; align-items: center; gap: var(--sp-2); padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.time-title #time-icon { display: inline-flex; color: var(--text-muted); }
.time-title:hover #time-icon { color: var(--text-secondary); }
.time-title h2 { white-space: nowrap; }
.range-chips { margin-bottom: var(--sp-2); }
.chart-legend-row { margin-bottom: var(--sp-2); }

/* ==========================================================================
   Clean look: flat colour instead of gradients and glows, borderless white
   cards on a plain grey page, pill buttons, soft grey fields. Kept last so it
   overrides the earlier component rules without rewriting each one.
   ========================================================================== */

html, body { background-image: none; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]), :root:not([data-theme="light"]) body { background-image: none; } }
:root[data-theme="dark"], :root[data-theme="dark"] body { background-image: none; }

.card { border: 0; box-shadow: none; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .card { border: 1px solid var(--border); } }
:root[data-theme="dark"] .card { border: 1px solid var(--border); }

.btn,
nav.tabs a.active,
.chip.icon-only.active,
.brand .mark,
.dyn-head .dyn-icon,
.signin-card .signin-icon { background: var(--series-1); box-shadow: none; }
.btn { border-radius: 999px; }
.btn.secondary { background: var(--surface-2); border-color: transparent; box-shadow: none; }
.btn.secondary:hover { background: var(--surface-3); }
.btn.danger { background: var(--critical); box-shadow: none; }
.icon-btn.bordered { border-color: transparent; background: var(--surface-2); box-shadow: none; }
nav.tabs { border: 0; }
header.site-header { border-bottom-color: transparent; }

/* Fields: filled grey, no outline until focused. */
input[type="text"], input[type="url"], input[type="email"], input[type="tel"], input[type="search"],
input[type="number"], input[type="password"], input[type="datetime-local"], input[type="date"], input[type="time"],
textarea, select, .color-field, .date-field, .link-row {
  background-color: var(--surface-2);
  border-color: transparent;
}
.chip.icon-only { border-color: transparent; background: var(--surface-2); }
.type-row .icon-btn { border-color: transparent; background: var(--surface-2); }

/* Settings rows: no hairlines, quieter group titles. */
.setting { border-bottom: 0; height: 42px; }
.setting > label { display: flex; align-items: center; gap: 8px; }
.setting > label .icon { color: var(--text-muted); }
.settings-title {
  font-size: 0.8rem; font-weight: 650; letter-spacing: 0; text-transform: none;
  color: var(--text-primary); border-bottom: 0; padding-bottom: 2px;
}

/* ---- Generator layout: controls on the left, the preview on the right ---- */
.gen-style > .card + .card { margin-top: var(--sp-3); }
@media (min-width: 860px) {
  /* The last row is empty space, standing in for the page's bottom padding:
     the sticky preview can only stay put while its grid area is on screen,
     so ending the grid at the page's end stops it being nudged up when
     scrolled right to the bottom (44px + the 12px row gap = the 56px the
     padding gave). */
  .container:has(> .gen-grid) { padding-bottom: 0; }
  .gen-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "content preview" "dynamic preview" "style preview" ". preview";
    grid-template-rows: auto auto auto 44px;
    gap: var(--sp-3) var(--sp-6);
  }
}
/* An even split between the controls and the preview. */

.preview-box { gap: var(--sp-4); padding: 0; background: none; border: 0; }
.preview-frame {
  max-width: none;
  /* No padding: the code's own margin (quiet zone) is the only white space
     around it, so what you see is exactly what downloads. */
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 2px rgba(20, 20, 40, 0.04);
}
.preview-frame.has-frame { padding: var(--sp-5); background: #fff; }
.preview-box .btn-row { align-items: center; justify-content: center; gap: 8px; width: auto; }
.preview-box .btn-row .btn { flex: none; }
.btn.pill { padding: 9px 16px; font-weight: 600; }
.icon-btn.round { width: 38px; height: 38px; border-radius: 999px; background: var(--surface-2); }
.icon-btn.round:hover { background: var(--surface-3); }
.preview-hint, .qr-info { justify-content: center; text-align: center; }

/* ---- Compact style cards ---- */
.card.compact { padding: var(--sp-4); }
.card-title {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 var(--sp-3); font-size: 0.85rem; font-weight: 650; color: var(--text-primary);
}
.card-title .icon { color: var(--text-muted); }
.card-title-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.card-title-row .card-title { margin: 0; }
.card-title-row + #s_frame_fields { margin-top: var(--sp-3); }

.opt-label { font-size: 0.74rem; font-weight: 500; color: var(--text-muted); margin: var(--sp-3) 0 6px; }
.card-title + .opt-label, .card-title + .scroll-group > .opt-label:first-child { margin-top: 0; }
/* A set of icon rows that scroll sideways as one, from the rows or the space
   between them (see linkShapeRowScrolls() in js/generator/): a sideways swipe
   there is handled by the page, an up-and-down one still scrolls it. */
.scroll-group { touch-action: pan-y; }
.scroll-group.dragging { cursor: grabbing; user-select: none; }

/* Icon tiles: grey squares, the chosen one ringed in the accent colour. */
.opts { display: flex; flex-wrap: wrap; gap: 8px; }
.opts.inline { gap: 4px; justify-content: flex-end; flex-wrap: nowrap; }
.opts[hidden] { display: none; } /* a direction row shows only the buttons for its shape's kind */
.opt {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  border: 2px solid transparent; border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text-primary);
  cursor: pointer; transition: background 0.12s var(--ease), border-color 0.12s var(--ease);
}
.opt:hover { background: var(--surface-3); }
.opt.active { border-color: var(--series-1); background: var(--surface); }
.opt .icon { width: 28px; height: 28px; }
.opt:disabled { opacity: 0.35; cursor: not-allowed; }
.opt:disabled:hover { background: var(--surface-2); }
.opt.text { width: 32px; height: 32px; font: inherit; font-size: 0.8rem; font-weight: 650; }
/* The Shape and Frame cards' icon rows each stay on one line and
   scroll sideways when they don't fit (they're .scroll-row, see above), so
   the buttons keep their full size and more options never wrap. */
.opts.shape-opts { flex-wrap: nowrap; gap: 6px; padding: 4px; margin: -4px; } /* room for the focus ring, which scrolling would clip */
.opts.shape-opts .opt { flex: none; width: 44px; height: 44px; }
/* Evens up a shorter row's scroll so the three rows scroll together (see
   linkShapeRowScrolls() in js/generator/). */
.shape-opts-spacer { flex: none; }
.opts.shape-opts .opt .icon { width: 64%; height: auto; aspect-ratio: 1; }
/* An eye centre is drawn small in its icon (it sits inside the frame), so
   the Eye row's shapes are shown a size up to read as clearly as the rest. */
.opts.shape-opts[data-for="s_eye_ball"] .opt:not([data-value="inherit"]) .icon { width: 82%; }

/* Colour dots, then the hex field for anything else. */
.swatches { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.swatch-btn {
  width: 28px; height: 28px; padding: 0; flex: none;
  border: 0; border-radius: 50%; cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
  transition: transform 0.1s var(--ease);
}
/* Only with a real mouse: on a touch screen a tap leaves :hover stuck, and
   the dot stayed enlarged (and clipped by its scrolling row). */
@media (hover: hover) and (pointer: fine) { .swatch-btn:hover { transform: scale(1.08); } }
.swatch-btn.active { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 0 2px var(--surface), 0 0 0 4px var(--series-1); }
.swatches .color-field { width: 116px; height: 32px; flex: none; margin-left: 4px; }

/* Logo / Advanced: collapsed cards that open in place. */
details.fold-card > summary { list-style: none; cursor: pointer; user-select: none; }
/* The card's padding moves inside its title row, so a collapsed card is one
   big button -- a tap anywhere on it opens it, not just on the words. Open,
   it looks the same as before: the body carries the same padding. */
details.fold-card.card.compact { padding: 0; }
details.fold-card > summary { padding: var(--sp-4); border-radius: inherit; }
details.fold-card[open] > summary { padding-bottom: 0; }
details.fold-card > summary:focus-visible { outline: 2px solid var(--series-1); outline-offset: -2px; }
details.fold-card > .fold-card-body { padding: 0 var(--sp-4) var(--sp-4); }
details.fold-card > summary::-webkit-details-marker { display: none; }
details.fold-card .chev { color: var(--text-muted); transition: transform 0.15s var(--ease); }
details.fold-card[open] .chev { transform: rotate(180deg); }
.fold-card-body { margin-top: var(--sp-3); }
.fold-card-body .error-text { margin: 0 0 var(--sp-2); }

/* Colours card: one row per colour -- its name, then the presets and picker. */
.colour-row { display: flex; align-items: center; gap: var(--sp-3); min-height: 40px; }
.colour-row + .colour-row { margin-top: 4px; }
.colour-row .opt-label { width: 84px; flex: none; margin: 0; }
.colour-row .swatches { flex: 1; min-width: 0; }

/* The frame's text, under its style tiles. */
.frame-text { position: relative; margin-top: var(--sp-3); }
/* The icon sits by the first line, where it stays when Enter adds a second. */
.frame-text .icon { position: absolute; left: 12px; top: 10px; color: var(--text-muted); pointer-events: none; }
.frame-text textarea { padding-left: 36px; min-height: 0; resize: none; overflow: hidden; }

/* The on-screen code is its SVG in an <img> (see showQrSvg() in
   js/qr-render.js), so it stays sharp when zoomed. */
.preview-frame img, .qr-float-canvas img, .qr-peek-canvas img, .qr-canvas-wrap img { position: relative; display: block; width: 100%; height: auto; }
.preview-frame img { height: 100%; object-fit: contain; }
img#preview-view[hidden] { display: none; }

/* QR details: an info button under the preview that opens a small card. */
.qr-info-pop { position: relative; }
.qr-info-pop > summary { list-style: none; cursor: pointer; }
.qr-info-pop > summary::-webkit-details-marker { display: none; }
.qr-info-pop[open] > summary { background: var(--surface-3); color: var(--text-primary); }
.qr-info-pop .qr-info {
  position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 15;
  transform: translateX(var(--pop-shift, 0px)); overflow-y: auto;
  flex-direction: column; align-items: flex-start; gap: 6px;
  width: max-content; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md); font-size: 0.78rem;
}
.qr-info-pop .qr-info div { gap: 8px; }
/* Nothing to show until a code is drawn. */
.qr-info-pop:has(.qr-info:empty) { display: none; }

/* Nothing drawn yet: the floating thumbnail and its popup hold a square, the
   shape a code will be, rather than shrinking to a short strip. */
.qr-float-canvas:not(:has(img:not([hidden]))),
.qr-peek-canvas:not(:has(img:not([hidden]))) { aspect-ratio: 1; min-height: 0; }

/* Colours rows stay on one line at any width: name, the preset dots, then the
   hex field taking whatever room is left (and no more than it needs). */
.colour-row .swatches { flex-wrap: nowrap; }
.colour-row .swatches .color-field { flex: 1 1 auto; width: auto; min-width: 0; max-width: 132px; }
@media (max-width: 480px) {
  .colour-row { gap: var(--sp-2); }
  .colour-row .opt-label { width: 70px; }
  .colour-row .swatches { gap: 6px; }
  .colour-row .swatch-btn { width: 26px; height: 26px; }
}

/* ==========================================================================
   Dashboard, in the Generate tab's clean look: icon + word labels instead of
   uppercase captions, no hairlines, grey tiles, softer headings.
   ========================================================================== */

/* List rows: short icon-led meta instead of a sentence. */
.code-list-item .meta { display: flex; align-items: center; gap: 8px; }
.code-list-item .meta .meta-item { display: inline-flex; align-items: center; gap: 4px; }
.code-list-item .meta .note-flag { margin-left: 0; }
.code-list-item .meta .badge { margin-left: 0; }
.code-list-item .scans .small { display: inline-flex; align-items: center; gap: 3px; justify-content: flex-end; }
.code-list-item .scans .small .icon { width: 11px; height: 11px; }
.kind-icon { border-radius: var(--radius-sm); }
.code-list-item { border-radius: var(--radius); }

/* Detail rows: icon and word, like the Generate settings rows. */
.info-row { border-top: 0; height: 34px; }
.info-label {
  display: inline-flex; align-items: center; gap: 8px;
  width: 112px; font-size: 0.8rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; color: var(--text-muted);
}
.info-label .icon { flex: none; }
.ov-fold { border-top: 0; }

/* Stats: borderless grey tiles that stay in one or two tidy rows. */
.stat-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); }
.stat-tile { border: 0; background: var(--surface-2); border-radius: var(--radius-sm); }
.stat-tile .label { font-size: 0.74rem; }

/* Headings in the same weight and size as the Generate cards'. */
.card-head h2 { font-size: 0.9rem; font-weight: 650; }
.list-head h1 { font-size: 1.25rem; }

/* Table and settings captions in plain case. */
th { text-transform: none; letter-spacing: 0; font-size: 0.74rem; font-weight: 600; }

/* Charts' readout: what's under the pointer or finger, at the right of the
   card's title row -- never off the edge of the screen or under a finger. */
.chart-readout {
  margin-left: auto; min-width: 0;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.78rem; color: var(--text-secondary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.chart-readout .ro-label { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; margin-right: 3px; }
.chart-readout i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; margin-left: 4px; }
.chart-readout strong { font-weight: 650; color: var(--text-primary); }
.time-head { display: flex; align-items: center; gap: var(--sp-2); }
.map-dot.active { fill-opacity: 1; stroke: var(--surface); stroke-width: 1; }

/* The floating preview's first appearance on a phone: it springs up out of
   the corner bubble (see onCodeShown in js/generator/). */
@keyframes qr-float-pop {
  0% { opacity: 0; transform: translateY(16px) scale(0.3); }
  60% { opacity: 1; transform: translateY(-4px) scale(1.04); }
  100% { opacity: 1; transform: none; }
}
.qr-float.visible.pop { animation: qr-float-pop 0.55s var(--ease) both; }
@media (prefers-reduced-motion: reduce) { .qr-float.visible.pop { animation: none; } }

/* Styles: one-tap preset looks, each a small live thumbnail and its name. On
   a phone the row scrolls sideways rather than wrapping. */
.presets { display: flex; gap: 10px; overflow-x: auto; scrollbar-width: none; padding: 2px; margin: -2px; }
.presets::-webkit-scrollbar { display: none; }
.preset {
  flex: none; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 0; border: 0; background: none; cursor: pointer; font: inherit;
  color: var(--text-muted);
}
/* 94px: 90 inside the border, so the tallest thumbnail (30 modules) gets a
   whole 3px per module (see snapThumbToPixels). The image is placed by hand,
   on whole screen pixels. */
.preset-thumb {
  position: relative; width: 94px; height: 94px; display: block;
  padding: 4px; border-radius: var(--radius-sm); background: var(--surface-2);
  border: 2px solid transparent; transition: border-color 0.12s var(--ease), background 0.12s var(--ease);
}
.preset-thumb img { display: block; position: absolute; max-width: 100%; max-height: 100%; }
.preset:hover .preset-thumb { background: var(--surface-3); }
.preset.active .preset-thumb { border-color: var(--series-1); background: var(--surface); }
.preset-name { font-size: 0.74rem; font-weight: 500; }
.preset.active .preset-name { color: var(--text-primary); font-weight: 600; }

/* Very narrow phones: four preset dots leave no room to read a hex code, so the
   custom colour field shrinks to just its colour square (still tap to pick). */
@media (max-width: 400px) {
  .colour-row .swatches .color-field { flex: none; width: 34px; padding: 0 6px; justify-content: center; }
  .colour-row .color-hex { display: none; }
}

/* Colour rows: six preset dots, then the custom colour as a grey "+" circle.
   Picking a colour of your own fills it with that colour and rings it (the
   .custom class, set by syncPickers() in js/generator/). */
.colour-row .swatches { flex-wrap: nowrap; gap: 6px; }
.colour-row .swatches .color-field {
  flex: none; width: 28px; max-width: none; min-width: 0; height: 28px;
  padding: 0; margin-left: 0; border: 0; border-radius: 50%;
  justify-content: center;
}
.colour-row .swatches .color-field .color-hex { display: none; }
.colour-row .swatches .color-field.custom { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--series-1); }
/* Seven circles and a name on one line down to a 360px phone. The circles
   shrink rather than the gaps: a chosen one's ring reaches 4px out, so the
   gaps stay 8px and it never touches its neighbour. */
@media (max-width: 480px) {
  .colour-row .swatch-btn, .colour-row .swatches .color-field { width: 26px; height: 26px; }
}
@media (max-width: 400px) {
  .colour-row .opt-label { width: 66px; }
  .colour-row .swatch-btn, .colour-row .swatches .color-field { width: 24px; height: 24px; }
}

/* The Dynamic card's settings run as one list now, with no group headings. */
.dyn-fields .settings-group + .settings-group { margin-top: 0; }

/* Logo colour: Original, the six preset dots, then the "+" custom circle --
   the same look as the Colours card's rows, on one line down to 360px. */
.logo-colours { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; }
.logo-colours .swatch-btn.original {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text-secondary);
}
.logo-colours .color-field {
  flex: none; width: 28px; max-width: none; min-width: 0; height: 28px;
  padding: 0; margin-left: 0; border: 0; border-radius: 50%; justify-content: center;
}
.logo-colours .color-field .color-hex { display: none; }
.logo-colours .color-field.custom { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--series-1); }
#logo-colour-row[hidden] { display: none; }

/* Fields in focus: a thin line in the theme red, not a soft glow (which
   looked muddy against the grey fields). */
input:focus, textarea:focus, select:focus,
.color-field:focus-within, .date-field:focus-within {
  border-color: var(--series-1);
  box-shadow: none;
  background-color: var(--surface);
}
/* The custom-colour circles keep their own look. */
.colour-row .swatches .color-field:focus-within, .logo-colours .color-field:focus-within { background-color: var(--surface-2); }
.colour-row .swatches .color-field.custom:focus-within, .logo-colours .color-field.custom:focus-within {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--series-1);
}

/* A code's "Active" status, in the theme red rather than green. */
.status-toggle .on { color: var(--series-1); }

/* The custom-colour circle: grey with a "+" like the other tiles, until a
   colour of your own is picked -- then that colour fills the whole circle. */
.colour-row .swatches .color-field, .logo-colours .color-field {
  background-color: var(--surface-2);
  color: var(--text-secondary);
}
.colour-row .swatches .color-field::before, .logo-colours .color-field::before {
  content: ""; width: 12px; height: 12px;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 2px no-repeat,
    linear-gradient(currentColor, currentColor) center / 2px 100% no-repeat;
  border-radius: 1px;
}
.colour-row .swatches .color-field:hover, .logo-colours .color-field:hover { color: var(--text); }
.colour-row .swatches .color-field .color-dot, .logo-colours .color-field .color-dot {
  position: absolute; inset: 0; width: auto; height: auto; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
  opacity: 0; transition: opacity 0.12s var(--ease);
}
.colour-row .swatches .color-field.custom .color-dot, .logo-colours .color-field.custom .color-dot { opacity: 1; }
/* Chosen: the "+" stays, on top of the colour (--plus is set by syncPickers()). */
.colour-row .swatches .color-field::before, .logo-colours .color-field::before { position: relative; z-index: 1; pointer-events: none; } /* clicks go through to the picker */
.colour-row .swatches .color-field.custom, .logo-colours .color-field.custom { color: var(--plus, #fff); }

/* Colour dots: roomy like the icon tiles wherever the row has space (so a
   chosen dot's ring never crowds its neighbours), tightening only where it
   doesn't -- the narrow desktop column just past the breakpoint, and phones. */
.colour-row .swatches, .logo-colours { gap: 12px; }
@media (min-width: 860px) and (max-width: 979px) {
  .colour-row .swatches, .logo-colours { gap: 8px; }
}
@media (max-width: 480px) {
  .colour-row .swatches, .logo-colours { gap: 8px; } /* never under the ring's 4px + a gap */
  .logo-colours .swatch-btn, .logo-colours .color-field { width: 26px; height: 26px; }
}
@media (max-width: 400px) {
  .logo-colours .swatch-btn, .logo-colours .color-field { width: 24px; height: 24px; }
}

/* Preview tilt (wireTilt() in js/generator/): follows the mouse quickly
   while it's over the code, eases back flat once it leaves. */
.tilt { --tilt-x: 0deg; --tilt-y: 0deg; }
.preview-frame.tilt {
  transform: perspective(var(--tilt-depth, 900px)) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 0.5s var(--ease);
}
.preview-frame.tilt.tilting { transition: transform 0.1s linear; }
@media (prefers-reduced-motion: reduce) { .preview-frame.tilt { transform: none; } }

/* Detail rows, tight: read-only now (editing is in the generator), so the
   rows only need to fit a line of text, and the label column only the
   longest label ("Redirects"). */
.info-row { height: 24px; }
.notes-row { min-height: 24px; padding: 2px 0; }
.info-label { width: 92px; }

@media (max-width: 600px) {
  .btn-col .btn-row { grid-template-columns: repeat(3, 1fr); }
}

/* Tiles in a settings row (Error correction's L M Q H) span the same width as
   the fields above and below, split evenly. */
.setting .opts.inline { width: 100%; }
.setting .opts.inline .opt { flex: 1; width: auto; }

/* Gradient tiles, sized to sit in a Colours row beside the colour dots. */
#s_fg_end_row[hidden] { display: none; }

/* The Colours card's header: its title, then "Gradient" in grey beside it,
   which opens the gradient menu underneath. */
.colours-head { justify-content: flex-start; gap: 10px; margin-bottom: var(--sp-3); }
.gradient-menu { flex: none; }
.gradient-btn {
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 0.85rem; font-weight: 500; color: var(--text-muted);
  transition: color 0.12s var(--ease);
}
.gradient-btn:hover, .gradient-btn[aria-expanded="true"] { color: var(--text-primary); }
.gradient-btn.on { color: var(--series-1); }
/* The menu: just a row of direction icons; the one in use is lit up red. */
.gradient-menu .menu { left: -6px; top: calc(100% + 8px); min-width: 0; display: flex; gap: 2px; }
.gradient-menu .menu[hidden] { display: none; }
.gradient-menu .menu button { width: 34px; height: 34px; padding: 0; justify-content: center; color: var(--text-secondary); }
.gradient-menu .menu button .icon { width: 18px; height: 18px; }
.gradient-menu .menu button:hover { color: var(--text-primary); }
.gradient-menu .menu [aria-checked="true"] { color: var(--series-1); background: var(--accent-soft); }
/* The eye switch, set off by a thin divider, only once there's a gradient. */
.gradient-menu .menu-divider { width: 1px; margin: 6px 3px; background: var(--border); }
.gradient-menu .menu:not(.has-gradient) :is(.menu-divider, #gradient-eyes-btn) { display: none; }
.colour-row[hidden] { display: none; }

/* Sign-out and the page menu, together at the header's right. */
.header-actions { display: flex; align-items: center; gap: 4px; }

/* The page menu (the three dots): Undo and Redo side by side, then History.
   Undo / Redo leave it open, so several steps can be taken in a row. */
.page-menu { flex: none; }
.page-menu .menu { left: auto; right: 0; top: calc(100% + 6px); z-index: 25; min-width: 200px; padding: 6px; box-shadow: var(--shadow-pop); }
.page-menu .menu button { font-size: 0.88rem; padding: 10px 12px; }
.page-menu .menu button .icon { color: var(--text-secondary); }
.page-menu .menu button:disabled { opacity: 0.4; cursor: default; background: transparent; }
.page-menu-steps { display: flex; gap: 4px; }
.page-menu-steps button { justify-content: center; }
.page-menu-divider { display: block; height: 1px; margin: 6px 4px; background: var(--border); }
#page-menu-btn[aria-expanded="true"] { background: var(--surface-2); color: var(--text-primary); }

/* A colour row's dots scroll sideways in one line (swipe, or drag with a
   mouse -- wireScrollRow()), with the "+" picker staying put after them. The
   padding keeps a chosen dot's ring from being clipped by the scroll box. */
.swatch-scroll {
  display: flex; align-items: center; gap: inherit;
  flex: 0 1 auto; min-width: 0;
  padding: 6px; margin: -6px 0 -6px -6px;
}

/* History: a list of the codes kept in this browser. */
.history-dialog { max-width: 440px; padding: var(--sp-4); }
.history-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.history-head h2 { margin: 0; }
.history-list { list-style: none; margin: 0; padding: 0; max-height: min(60vh, 520px); overflow-y: auto; }
.history-row { display: flex; align-items: center; gap: 4px; }
.history-row + .history-row { margin-top: 2px; }
.history-item {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px;
  padding: 6px; border: 0; border-radius: 10px; background: transparent;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.history-item:hover, .history-item:focus-visible { background: var(--surface-2); }
.history-thumb {
  flex: none; width: 56px; height: 56px; border-radius: 8px; overflow: hidden;
  display: flex; align-items: center; justify-content: center; background: var(--surface-2);
}
.history-thumb img { display: block; max-width: 100%; max-height: 100%; }
.history-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.history-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.history-meta { font-size: 0.78rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.history-delete { flex: none; width: 32px; height: 32px; }
.history-head { margin-bottom: var(--sp-3); }
/* Nothing kept yet: just a faint clock. */
.history-empty { display: flex; justify-content: center; padding: var(--sp-5) 0 var(--sp-4); color: var(--text-muted); opacity: 0.5; }
.history-empty .icon { width: 36px; height: 36px; stroke-width: 1.5; }
.history-empty[hidden] { display: none; }
/* While editing a saved code, picking one only takes its look. */
.history-hint { margin: 0 0 var(--sp-3); font-size: 0.8rem; color: var(--text-muted); }
.history-hint[hidden] { display: none; }

/* The header as three columns -- the logo, the tabs, the buttons -- with the
   outer two sharing the space evenly, so the tabs sit in the exact middle
   whatever is either side of them. */
.header-inner { display: grid; grid-template-columns: 1fr auto 1fr; }
.header-inner > .brand { justify-self: start; }
.header-inner > nav.tabs { justify-self: center; }
.header-inner > :last-child { justify-self: end; }

/* Sign out, last in the page menu, with a line above it (drawn here so it
   comes and goes with the button, which only shows while signed in). */
.page-menu #signout-btn { position: relative; margin-top: 13px; }
.page-menu #signout-btn::before {
  content: ""; position: absolute; left: 4px; right: 4px; top: -7px;
  height: 1px; background: var(--border); pointer-events: none;
}

/* Copy, PNG, SVG, PDF, Info: the same row under the preview and in the peek
   dialog. Tighter in the dialog, so all five fit on one line on a phone. */
.qr-actions { align-items: center; justify-content: center; gap: 8px; width: auto; }
.qr-actions .btn { flex: none; }
.qr-peek-dialog .qr-actions { gap: 6px; }
.qr-peek-dialog .qr-actions .btn.pill { padding: 9px 12px; }
@media (max-width: 380px) {
  .qr-actions { gap: 4px; }
  .qr-actions .icon-btn.round { width: 34px; height: 34px; }
  .qr-actions .btn.pill { padding: 8px 10px; }
}

/* Theme: Auto, Light, Dark side by side in the page menu. */
.page-menu-theme { display: flex; gap: 2px; padding: 3px; border-radius: var(--radius-xs); background: var(--surface-2); }
.page-menu .page-menu-theme button { flex: 1; justify-content: center; gap: 6px; padding: 7px 8px; font-size: 0.8rem; color: var(--text-secondary); }
.page-menu .page-menu-theme button:hover { background: transparent; color: var(--text-primary); }
.page-menu .page-menu-theme button[aria-checked="true"] { background: var(--surface); color: var(--text-primary); font-weight: 600; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); }
.page-menu .page-menu-theme button[aria-checked="true"] .icon { color: var(--series-1); }

/* The scan score: a ring with the number in it, the same size as the round
   buttons beside it, coloured by how it did (the slot's data-status). Its
   card opens upward from the button row, over the preview. */
.scan-score-slot { --score: var(--good); display: flex; }
.scan-score-slot[hidden] { display: none; }
.scan-score-slot[data-status="warn"] { --score: var(--series-2); }
.scan-score-slot[data-status="bad"] { --score: var(--critical); }
.scan-score-pop > summary { list-style: none; }
.scan-score-pop > summary::-webkit-details-marker { display: none; }
.scan-score-pill {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 999px; cursor: pointer; user-select: none;
}
.scan-score-pill:hover, .scan-score-pop[open] > .scan-score-pill { background: var(--surface-2); }
/* Working out a new score: the last one fades until it's ready. */
.scan-score-slot.busy .scan-score-pill { opacity: 0.45; transition: opacity 0.2s; }
.scan-score-pill svg { position: absolute; inset: 2px; width: 34px; height: 34px; transform: rotate(-90deg); }
.scan-score-pill circle { fill: none; stroke-width: 3; }
.scan-score-pill .track { stroke: var(--border); }
.scan-score-pill .value { stroke: var(--score); stroke-linecap: round; transition: stroke-dasharray 0.3s var(--ease); }
/* line-height 1 and a half-pixel nudge down: Figtree's digits otherwise sit
   a touch high of the ring's centre, and a whole pixel looked too low. */
.scan-score-num { position: relative; top: 0.5px; line-height: 1; font-size: 0.76rem; font-weight: 700; color: var(--text-primary); }
@media (max-width: 380px) {
  .scan-score-pill { width: 34px; height: 34px; }
  .scan-score-pill svg { inset: 1px; width: 32px; height: 32px; }
}

/* The card: anchored to the whole button row (the slot and its details
   don't position it), centred, opening upward. In the peek dialog it sits
   over the code inside the dialog, scrolling if it's tall. */
.qr-actions { position: relative; }
.scan-score-card {
  position: absolute; left: 50%; bottom: calc(100% + 8px); z-index: 16; transform: translateX(calc(-50% + var(--pop-shift, 0px)));
  width: min(340px, calc(100vw - 32px)); padding: 14px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md); text-align: left;
}
/* No room above it on screen: it opens downward instead (see fitPopCard()
   in js/generator/, which also caps its height and nudges it sideways). */
.scan-score-card.below, .qr-info-pop .qr-info.below { bottom: auto; top: calc(100% + 8px); }
/* The dashboard's card opens downward, level with its Download menu, and
   only goes up (.above) when there's more room there -- see fitPopCard(). */
.qr-info-pop[data-open="below"] .qr-info { bottom: auto; top: calc(100% + 4px); }
.qr-info-pop[data-open="below"] .qr-info.above { top: auto; bottom: calc(100% + 4px); }
.qr-peek-dialog .qr-actions { position: static; }
.qr-peek-dialog .scan-score-card {
  left: var(--sp-4); right: var(--sp-4); bottom: calc(var(--sp-4) + 46px); transform: none; width: auto;
  max-height: calc(100% - 2 * var(--sp-4) - 54px); overflow-y: auto;
}
.scan-score-head { display: flex; align-items: baseline; gap: 4px; margin-bottom: 10px; }
.scan-score-big { font-size: 1.6rem; line-height: 1; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.scan-score-of { color: var(--text-muted); font-size: 0.85rem; }
.scan-score-head .scan-score-label { margin-left: auto; font-weight: 650; color: var(--score); }
.scan-score-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.scan-score-checks li { display: flex; gap: 10px; align-items: flex-start; }
.scan-score-checks li > .icon { flex: none; margin-top: 2px; }
.scan-score-checks li.good > .icon { color: var(--good); }
.scan-score-checks li.warn > .icon { color: var(--series-2); }
.scan-score-checks li.bad > .icon { color: var(--critical); }
.scan-score-checks strong { display: block; font-size: 0.85rem; font-weight: 600; color: var(--text-primary); }
.scan-score-checks span { display: block; font-size: 0.78rem; color: var(--text-secondary); line-height: 1.4; }
.scan-score-foot { margin: 12px 0 0; font-size: 0.72rem; color: var(--text-muted); }

/* The Logo card: Icon / Image / Link as three equal buttons, then the chosen
   one's panel at full width, then the logo in use. */
.logo-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: var(--sp-3); }
@media (max-width: 600px) { .chips.logo-tabs { margin-inline: 0; padding-inline: 0; overflow: visible; } }
.logo-tabs .chip { justify-content: center; gap: 6px; padding: 8px 12px; font-size: 0.82rem; }
.logo-tabs .chip .icon { width: 15px; height: 15px; }
.logo-panel { display: flex; flex-direction: column; gap: var(--sp-2); }
.logo-panel[hidden] { display: none; }
.logo-panel input[type="url"] { width: 100%; }
.logo-panel .btn.block { width: 100%; justify-content: center; gap: 6px; }
.logo-panel .error-text { margin: 0; }
.logo-current { display: flex; align-items: center; gap: 6px; min-height: 32px; margin: var(--sp-2) 0 var(--sp-3); }
.logo-current[hidden], #logo-bg-row[hidden] { display: none; }
.logo-current .logo-filename { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85rem; color: var(--text-secondary); }
.logo-current .icon-btn { width: 28px; height: 28px; flex: none; }

/* The icon picker: a search box over a scrolling grid of every Lucide icon.
   The box is a normal field (see the shared input styles), with room for the
   magnifier on the left and our own clear button on the right. */
.logo-panel[data-panel="icon"] { gap: var(--sp-3); }
.icon-search { position: relative; }
.icon-search > .icon { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; }
.icon-search input { width: 100%; padding: 10px 40px 10px 38px; -webkit-appearance: none; appearance: none; }
.icon-search input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.icon-search .icon-search-clear { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); width: 30px; height: 30px; }
.icon-search .icon-search-clear[hidden] { display: none; }
.icon-grid {
  /* Fixed-size cells, spread across the row, and room kept for the scrollbar
     whether it's there or not: the icons stay the same size however many a
     search finds. */
  display: grid; grid-template-columns: repeat(auto-fill, 44px); justify-content: space-between; gap: 4px; align-content: start;
  scrollbar-gutter: stable;
  max-height: 244px; overflow-y: auto; overscroll-behavior: contain; /* shrinks to fit a short list of results */
  /* A subtle frame, so the scrolling icons read as their own section. */
  padding: 6px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface);
}
.icon-choice {
  aspect-ratio: 1; max-width: 100%; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--radius-xs); background: transparent; color: var(--text-primary); cursor: pointer; padding: 0;
}
.icon-choice svg { width: 22px; height: 22px; }
.icon-choice[hidden] { display: none; }
.icon-grid[hidden] { display: none; } /* no results (or still loading): just the message, no empty frame */ /* its display: flex would otherwise keep a filtered-out icon showing */
.icon-choice:hover { background: var(--surface-2); }
.icon-choice:focus-visible { outline: 2px solid var(--series-1); outline-offset: 0; }
.icon-choice[aria-selected="true"] { background: var(--accent-soft); color: var(--series-1); }
.icon-status { margin: 0; text-align: center; color: var(--text-muted); font-size: 0.9rem; padding: var(--sp-3) 0; }
.icon-credit { margin: 0; font-size: 0.72rem; color: var(--text-muted); }
.icon-credit a { color: inherit; }

/* About QR codes: a long read in the page menu. The head stays put while the
   body scrolls, and the contents row jumps within it (see wireGuide()). */
.guide-dialog {
  max-width: 640px; padding: 0; overflow: hidden;
  display: none; flex-direction: column; max-height: min(88vh, 860px);
  --a-finder: var(--series-1);
  --a-timing: #2f7de1;
  --a-align: var(--series-2);
  --a-format: #8b5cf6;
  --a-data: var(--text-primary);
}
.guide-dialog[open] { display: flex; }
.guide-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: var(--sp-4) var(--sp-4) var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border);
}
.guide-head h2 { margin: 0; }
.guide-body {
  overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-3) var(--sp-5) var(--sp-6);
  line-height: 1.6; color: var(--text-secondary); font-size: 0.93rem;
}
.guide-body section { padding-top: var(--sp-3); }
.guide-body section + section { margin-top: var(--sp-4); border-top: 1px solid var(--border); padding-top: var(--sp-5); }
.guide-body h3 { margin: 0 0 var(--sp-2); font-size: 1.02rem; color: var(--text-primary); }
.guide-body p { margin: 0 0 var(--sp-3); }
.guide-body strong, .guide-body b { color: var(--text-primary); }
.guide-body ul { margin: 0 0 var(--sp-3); padding-left: 1.2em; }
.guide-body li + li { margin-top: 4px; }
.guide-body code { background: var(--surface-2); padding: 1px 5px; border-radius: 5px; color: var(--text-primary); word-break: break-word; }
.guide-body .icon.inline { vertical-align: -2px; }

.guide-toc { display: flex; flex-wrap: wrap; gap: 6px; }
.guide-toc a {
  font-size: 0.8rem; padding: 4px 10px; border-radius: 999px; text-decoration: none;
  background: var(--surface-2); color: var(--text-secondary); white-space: nowrap;
}
.guide-toc a:hover, .guide-toc a:focus-visible { background: var(--accent-soft-strong); color: var(--text-primary); }

.guide-note {
  background: var(--accent-soft); border-left: 3px solid var(--series-1);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding: var(--sp-2) var(--sp-3);
}

.guide-terms { margin: 0 0 var(--sp-3); }
.guide-terms dt { font-weight: 700; color: var(--text-primary); margin-top: var(--sp-3); }
.guide-terms dd { margin: 2px 0 0; }

.guide-body ul.guide-types { list-style: none; padding: 0; }
.guide-types li { display: flex; gap: 10px; align-items: baseline; }
.guide-types li .icon { position: relative; top: 2px; color: var(--series-1); }

.guide-table { width: 100%; border-collapse: collapse; margin: 0 0 var(--sp-3); font-size: 0.86rem; }
.guide-table th { text-align: left; font-weight: 600; color: var(--text-muted); font-size: 0.78rem; }
.guide-table th, .guide-table td { padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.guide-table th:first-child, .guide-table td:first-child { padding-left: 0; white-space: nowrap; color: var(--text-primary); }

/* The anatomy diagram and its key. */
.guide-figure { margin: 0 0 var(--sp-3); display: flex; gap: var(--sp-5); align-items: center; }
.guide-figure svg { flex: none; width: 190px; height: 190px; background: #fff; border-radius: var(--radius-sm); shape-rendering: crispEdges; }
.guide-figure figcaption { display: grid; gap: 6px; font-size: 0.84rem; }
.guide-figure figcaption span { display: flex; align-items: center; gap: 8px; }
.guide-figure .key { width: 12px; height: 12px; border-radius: 3px; flex: none; }
.guide-figure .k-finder { background: var(--a-finder); }
.guide-figure .k-timing { background: var(--a-timing); }
.guide-figure .k-align { background: var(--a-align); }
.guide-figure .k-format { background: var(--a-format); }
.guide-figure .k-data { background: #16160f; }
.guide-figure .k-quiet { border: 1.5px dashed var(--baseline); }
.guide-figure .a-quiet { fill: none; stroke: #c8c6b6; stroke-width: 0.25; stroke-dasharray: 0.8 0.6; }
.guide-figure .a-finder { fill: var(--a-finder); }
.guide-figure .a-timing { fill: var(--a-timing); }
.guide-figure .a-align { fill: var(--a-align); }
.guide-figure .a-format { fill: var(--a-format); }
.guide-figure .a-data { fill: #16160f; }
.guide-figure .light { opacity: 0.16; }
@media (max-width: 520px) {
  .guide-figure { flex-direction: column; align-items: stretch; }
  .guide-figure svg { width: min(240px, 100%); height: auto; aspect-ratio: 1; align-self: center; }
  .guide-figure figcaption { grid-template-columns: 1fr 1fr; }
}

/* Version sizes to scale: each square's side is its module count. */
.guide-scale { display: flex; align-items: flex-end; gap: var(--sp-4); margin: 0 0 var(--sp-3); flex-wrap: wrap; }
.guide-scale div { display: flex; flex-direction: column; align-items: center; gap: 2px; font-size: 0.78rem; }
.guide-scale span {
  width: calc(var(--n) * 0.9px); height: calc(var(--n) * 0.9px); border-radius: 3px;
  background: var(--surface-2); border: 1px solid var(--border-strong);
  background-image: linear-gradient(var(--border-strong) 1px, transparent 1px), linear-gradient(90deg, var(--border-strong) 1px, transparent 1px);
  background-size: calc(100% / var(--n) * 4) calc(100% / var(--n) * 4);
}
.guide-scale b { color: var(--text-primary); margin-top: 4px; }
.guide-scale small { color: var(--text-muted); }

/* About QR codes, last in the page menu: quieter than the actions above it. */
.page-menu .page-menu-about { color: var(--text-muted); font-size: 0.76rem; }
.page-menu .page-menu-about:hover { color: var(--text-secondary); }

/* The landing page type's form: a logo line and an image line, then each button as a label, the
   link it opens and a remove "x" on one line (label above link on a phone).
   Only the filled-in rows and one empty one show (see syncPageUi()). */
/* The logo and image rows: picture (click it to add or change), link and
   remove on one line. */
.page-logo-row { display: flex; align-items: center; gap: var(--sp-2); }
/* A row's kind: a small icon at the start of its label line, opening the
   app's own menu. Both label lines in a row are the icon's height, so the
   two inputs under them line up. */
.page-btn-head { display: flex; align-items: center; gap: 4px; height: 22px; margin-bottom: 6px; }
.page-btn-head label { margin: 0 0 0 2px; }
.page-btn-row .field > label { display: flex; align-items: center; height: 22px; }
/* Flex, not inline: an inline box would add a line's worth of descender
   space under the icon, making this label line taller than the other. */
.page-btn-style { flex: none; display: flex; }
/* The drag handle: grab a row to move it (see wirePageButtonDrag()). */
.page-btn-grip {
  position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 22px; padding: 0;
  margin-left: -4px; border: 0; border-radius: 4px; background: transparent; color: var(--text-muted);
  cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none;
}
/* Only on rows that can move (see syncPageUi): otherwise the label starts
   in line with the fields above. */
.page-btn-grip[hidden] { display: none; }
/* A finger-sized target round the small icon. */
.page-btn-grip::before { content: ""; position: absolute; inset: -11px -10px; }
.page-btn-grip .icon { width: 14px; height: 14px; }
.page-btn-grip:hover { color: var(--text-primary); }
.page-btn-grip:focus-visible { outline: 2px solid var(--series-1); outline-offset: 1px; }
/* While a row is dragged: the others slide to open a gap for it, it follows
   the pointer lifted above them, and on letting go it settles into the gap. */
.reordering { -webkit-user-select: none; user-select: none; }
.reordering, .reordering * { cursor: grabbing !important; }
.reordering .page-btn-row { transition: transform 0.18s var(--ease); }
.page-btn-row.dragging {
  z-index: 5; transition: none; background: var(--surface); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md), 0 0 0 6px var(--surface);
}
.page-btn-row.dragging.settling { transition: transform 0.18s var(--ease); }
/* A row moved with the arrow keys flashes where it lands. */
.page-btn-row.moved { animation: page-row-moved 0.6s var(--ease); }
@keyframes page-row-moved {
  from { background: var(--accent-soft); box-shadow: 0 0 0 6px var(--accent-soft); border-radius: var(--radius-sm); }
}
@media (prefers-reduced-motion: reduce) {
  .reordering .page-btn-row, .page-btn-row.dragging.settling { transition: none; }
  .page-btn-row.moved { animation: none; }
}
.page-btn-style-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 6px; background: var(--surface-2); color: var(--text-secondary); cursor: pointer;
  transition: background 0.12s var(--ease), color 0.12s var(--ease);
}
.page-btn-style-btn .icon { width: 13px; height: 13px; }
.page-btn-style-btn:hover { color: var(--text-primary); }
.page-btn-style-btn[aria-expanded="true"] { background: var(--accent-soft); color: var(--series-1); }
.page-btn-style-btn:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.page-btn-style .menu { top: calc(100% + 6px); min-width: 150px; z-index: 20; }
.page-btn-style .menu button .icon { width: 15px; height: 15px; color: var(--text-secondary); }
.page-btn-style .menu [aria-checked="true"] { color: var(--series-1); background: var(--accent-soft); font-weight: 600; }
.page-btn-style .menu [aria-checked="true"] .icon { color: var(--series-1); }
/* The picture and the link box beside it share one explicit height -- the
   inputs' own (0.9rem text, 16px on a phone, x 1.3 line height, plus 9px
   padding and 1px border each side) -- so they line up exactly, whatever a
   browser would make of an input on its own. The picture looks like the
   inputs too: the same grey fill, with an outline only on hover. */
.page-logo-thumb, .page-logo-row > .page-pic-link { font-size: 0.9rem; line-height: 1.3; height: calc(1.3em + 20px); }
@media (max-width: 720px) { .page-logo-thumb, .page-logo-row > .page-pic-link { font-size: 16px; } }
.page-logo-thumb {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: calc(1.3em + 20px); border-radius: var(--radius-sm); overflow: hidden; cursor: pointer;
  margin: 0; /* it's a <label> (so a click opens the file picker): no label spacing under it */
  background: var(--surface-2); border: 1px solid transparent; color: var(--text-muted);
  transition: border-color 0.12s var(--ease), color 0.12s var(--ease);
}
.page-logo-thumb:hover { border-color: var(--border); color: var(--text-primary); }
/* The file picker is hidden, so show its keyboard focus on the picture. */
.page-logo-row:has(input[type="file"]:focus-visible) .page-logo-thumb { outline: 2px solid var(--series-1); outline-offset: 2px; }
.page-logo-row > .page-pic-link { flex: 1; min-width: 0; }
.page-logo-row > .page-pic-link:disabled { opacity: 0.6; cursor: not-allowed; }
.page-logo-row > .icon-btn { flex: none; }
.page-logo-thumb img { width: 100%; height: 100%; object-fit: contain; }
#page-logo-clear[hidden], #page-image-clear[hidden], .page-btn-row[hidden], .page-btn-clear[hidden], .page-btn-beside[hidden] { display: none; }
.page-btn-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0 var(--sp-2);
}
/* The row's two small controls -- side by side, and remove -- sit up on the
   label line at the right, so the label and the link each get half the width. */
.page-btn-clear, .page-btn-beside { position: absolute; top: -5px; width: 26px; height: 26px; z-index: 1; }
.page-btn-clear { right: 0; }
.page-btn-beside { right: 30px; }
/* The side-by-side toggle, on (a checkbox inside it) like a pressed icon-btn. */
.page-btn-beside:has(input:checked) { background: var(--accent-soft); color: var(--series-1); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); }
.page-btn-beside:has(input:focus-visible) { outline: 2px solid var(--series-1); outline-offset: 2px; }
/* Two buttons side by side: one line down the left of both rows, from the
   first's label to the bottom of the second's inputs, so they read as a group. */
.page-btn-row { position: relative; }
.page-btn-row.pair-start, .page-btn-row.pair-end { padding-left: var(--sp-3); }
.page-btn-row.pair-start::before, .page-btn-row.pair-end::before {
  content: ""; position: absolute; left: 0; width: 2px;
  background: color-mix(in srgb, var(--series-1) 55%, transparent);
}
.page-btn-row.pair-start::before { top: 0; bottom: 0; border-radius: 2px 2px 0 0; }
.page-btn-row.pair-end::before { top: 0; bottom: var(--sp-3); border-radius: 0 0 2px 2px; }
/* Only a picture that couldn't be used shows a message here; it brings its own gap. */
#page-hint { margin-top: var(--sp-2); }

/* The landing page preview: the eye button at the form's top right, on the
   Logo label's line, and the page in a phone-shaped popup in the middle of
   the screen (see wirePagePreview()). */
.type-fields[data-type="page"], .type-fields[data-type="markdown"] { position: relative; }
/* The Markdown form: its text in a code font, coloured as Markdown, then the
   style and Upload on one line. The colours are drawn by .md-hl, a copy of
   the text under the (see-through) textarea; the two share every measurement
   that places a letter, so the caret sits on the coloured text. Token colours
   are the catalogue app's (see syncMarkdownHighlight()). */
.md-editor {
  position: relative; border-radius: var(--radius-sm); background: var(--surface-2);
  --mdh-marker: #8a8a8a; --mdh-heading: #0550ae; --mdh-code: #a31515; --mdh-url: #a31515;
  --mdh-quote: #0a7d3c; --mdh-comment: #7a7a7a;
  --mdh-col-0: #0550ae; --mdh-col-1: #0a7d3c; --mdh-col-2: #953800; --mdh-col-3: #8250df; --mdh-col-4: #9a6700; --mdh-col-5: #1b7c83;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .md-editor {
    --mdh-marker: #8b949e; --mdh-heading: #569cd6; --mdh-code: #ce9178; --mdh-url: #ce9178;
    --mdh-quote: #6a9955; --mdh-comment: #8b949e;
    --mdh-col-0: #79c0ff; --mdh-col-1: #7ee787; --mdh-col-2: #ffa657; --mdh-col-3: #d2a8ff; --mdh-col-4: #e3b341; --mdh-col-5: #56d4dd;
  }
}
:root[data-theme="dark"] .md-editor {
  --mdh-marker: #8b949e; --mdh-heading: #569cd6; --mdh-code: #ce9178; --mdh-url: #ce9178;
  --mdh-quote: #6a9955; --mdh-comment: #8b949e;
  --mdh-col-0: #79c0ff; --mdh-col-1: #7ee787; --mdh-col-2: #ffa657; --mdh-col-3: #d2a8ff; --mdh-col-4: #e3b341; --mdh-col-5: #56d4dd;
}
.md-input, .md-hl {
  margin: 0; padding: 9px 12px; border: 1px solid transparent;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; line-height: 1.5;
  letter-spacing: normal; tab-size: 4; white-space: pre-wrap; overflow-wrap: break-word; word-break: normal;
  overflow-x: hidden; overflow-y: auto;
}
@media (max-width: 720px) { .md-input, .md-hl { font-size: 16px; } }
.md-input {
  position: relative; z-index: 1; min-height: 240px;
  background: transparent !important; color: transparent; -webkit-text-fill-color: transparent; caret-color: var(--text-primary);
}
.md-input::placeholder { -webkit-text-fill-color: var(--text-muted); }
.md-input::selection { background: rgba(120, 160, 255, 0.35); -webkit-text-fill-color: transparent; }
.md-hl {
  position: absolute; inset: 0; pointer-events: none; color: var(--text-primary);
  scrollbar-color: transparent transparent; /* keeps the bar's width, so the lines wrap the same */
}
.md-hl::-webkit-scrollbar-thumb, .md-hl::-webkit-scrollbar-track { background: transparent; }
.md-hl .mdh-marker, .md-hl .mdh-fence, .md-hl .mdh-punct, .md-hl .mdh-del, .md-hl .mdh-tsep { color: var(--mdh-marker); }
.md-hl .mdh-heading, .md-hl .mdh-strong, .md-hl .mdh-em, .md-hl .mdh-bi, .md-hl .mdh-link, .md-hl .mdh-tag,
.md-hl .mdh-list, .md-hl .mdh-hr, .md-hl .mdh-task, .md-hl .mdh-emoji { color: var(--mdh-heading); }
.md-hl .mdh-code, .md-hl .mdh-url, .md-hl .mdh-string { color: var(--mdh-code); }
.md-hl .mdh-quote, .md-hl .mdh-quote-text { color: var(--mdh-quote); }
.md-hl .mdh-comment { color: var(--mdh-comment); }
.md-hl .mdh-col-0 { color: var(--mdh-col-0); } .md-hl .mdh-col-1 { color: var(--mdh-col-1); } .md-hl .mdh-col-2 { color: var(--mdh-col-2); }
.md-hl .mdh-col-3 { color: var(--mdh-col-3); } .md-hl .mdh-col-4 { color: var(--mdh-col-4); } .md-hl .mdh-col-5 { color: var(--mdh-col-5); }
.md-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.chips.md-style { margin: 0; padding: 0; overflow: visible; }
.page-preview-btn { position: absolute; top: -5px; right: 0; width: 26px; height: 26px; z-index: 1; }
.page-preview-dialog {
  width: min(390px, calc(100vw - 32px)); height: min(780px, calc(100dvh - 48px)); max-width: none;
  padding: 0; overflow: hidden; border-radius: 24px; background: #f3f3f5;
}
.page-preview-dialog iframe { display: block; width: 100%; height: 100%; border: 0; }
.page-preview-dialog iframe[hidden] { display: none; }
.page-preview-message { margin: 0; height: 100%; display: flex; align-items: center; justify-content: center; padding: 32px; text-align: center; font-size: 0.9rem; color: #58564c; }
.page-preview-message[hidden] { display: none; }
.page-preview-dialog .dialog-close { background: rgba(255, 255, 255, 0.9); box-shadow: var(--shadow-sm); color: #16160f; }
#page-hint:empty { display: none; }

/* GS1: the fields its key doesn't take are hidden (see syncGs1Ui()). */
.type-fields .field[hidden] { display: none; }
/* Every type's form ends flush, like URL's last field (margin-bottom:0): a
   grid of fields that ends a form spaces its rows with a gap instead, so
   its last row adds no margin (in one column or two), and the Location
   form's hint takes no room while it has nothing to say. */
.type-fields > .field-grid:last-child { row-gap: var(--sp-3); }
.type-fields > .field-grid:last-child > .field { margin-bottom: 0; }
#geo-hint:empty { display: none; }
/* GS1 print mode's switch. */
.gs1-print { margin-top: var(--sp-3); }
.gs1-print + .gs1-print { margin-top: var(--sp-2); }
/* What the chosen Print size comes to (see syncGs1PrintSize()): a label and
   its size per row, the sizes lined up on the right. */
.gs1-sizes { margin: var(--sp-2) 0 0; font-size: 0.79rem; line-height: 1.45; color: var(--text-muted); }
.gs1-sizes:empty { display: none; }
.gs1-sizes div { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 2px 0; }
.gs1-sizes div + div { border-top: 1px solid var(--border); }
.gs1-sizes dd { margin: 0; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }
#gs1-opens-name { color: var(--text-primary); font-weight: 600; }
/* The form of the type a dynamic GS1 code opens, moved in under its GS1
   fields (see placeGs1Opened()). */
.gs1-dynamic { margin-top: var(--sp-4); }
.gs1-dynamic[hidden], .type-fields [data-gs1-static][hidden] { display: none; }
.gs1-opens-slot > .type-fields { margin-top: var(--sp-3); }

/* GS1 More options: everything but the barcode number, folded away. */
.gs1-more { margin: 0 0 var(--sp-2); }
.gs1-more[hidden] { display: none; } /* a dynamic code doesn't show it */
.gs1-more > summary {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none;
  font-size: 0.85rem; font-weight: 600; color: var(--text-primary); padding: 4px 0;
}
.gs1-more > summary::-webkit-details-marker { display: none; }
.gs1-more > summary .chev { transition: transform 0.15s var(--ease); }
.gs1-more[open] > summary .chev { transform: rotate(180deg); }
.gs1-more > .field-grid { margin-top: var(--sp-3); }
/* What More options' fields are for, above them. */
.gs1-more > .gs1-more-hint { margin-top: var(--sp-2); }
/* What a dynamic GS1 code opens: the type buttons' own round icon chips
   (.chip.icon-only), spaced like them and on one scrolling line. */
.gs1-dynamic > .field { margin-bottom: var(--sp-3); }
.gs1-dynamic .opt-label { margin-top: 0; }
.chips.gs1-opens { gap: 6px; margin: 0; }
@media (max-width: 600px) { .chips.gs1-opens { padding-inline: 0; } }
/* A GS1 code's link in the Dynamic card: shown, not changed (see syncDynamicUi()). */
#dyn_slug_box[hidden], #dyn_gs1_link[hidden] { display: none; }
#dyn_gs1_link { color: var(--text-muted); cursor: default; }

/* Dashboard confirmations (see showToast()): a pill in the theme colour,
   floating at the bottom centre over the page, so nothing in the layout
   moves. Fades and rises in, then out again. */
.toast {
  position: fixed; left: 50%; bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); z-index: 50;
  display: inline-flex; align-items: center; gap: 8px; max-width: calc(100vw - 32px);
  padding: 10px 16px; border-radius: 999px;
  background: var(--series-1); color: #fff; box-shadow: var(--shadow-accent);
  font-size: 0.85rem; font-weight: 600; line-height: 1.35;
  opacity: 0; transform: translate(-50%, 8px); pointer-events: none;
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast .icon { flex: none; }
@media (prefers-reduced-motion: reduce) { .toast { transition: opacity 0.2s; transform: translate(-50%, 0); } }
