/* ── Top nav ── */
.topnav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--slate-900);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  height: 52px;
  box-shadow: 0 2px 8px rgba(0,0,0,.25);
}

.topnav-brand {
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: -.3px;
  color: #fff;
  text-decoration: none;
}

.topnav-brand span { color: var(--accent); }

.topnav-actions { display: flex; gap: 8px; align-items: center; }

/* ── Builder layout ── */
/* App-shell host for the builder (invoice-edit): edge to edge, and on desktop the
   two panels scroll themselves. This used to be an inline style="overflow:hidden"
   on <main> plus an inline height on .builder-wrap, which beat the phone rules
   below: on a phone the form was clipped at one screen height and the Save / PDF /
   Send bar could not be reached. The ≤860px rule lets the page scroll normally. */
.dash-main.dash-main--builder { padding: 0; overflow: hidden; }
@media (max-width: 860px) {
  .dash-main.dash-main--builder {
    overflow: visible;
    /* clear the Feedback launcher, which sits above the 58px mobile nav */
    padding-bottom: max(16px, calc(var(--pf-launcher-clearance, 0px) - 58px + 12px));
  }
}
.builder-wrap {
  display: grid;
  grid-template-columns: var(--form-panel-w, 420px) 6px 1fr;
  height: calc(100vh - 52px);
  overflow: hidden;
}

/* Draggable splitter between form and preview panels */
.builder-splitter {
  cursor: col-resize;
  background: var(--slate-200);
  border-left: 1px solid var(--slate-300);
  border-right: 1px solid var(--slate-300);
  transition: background 120ms ease;
  touch-action: none;
}
.builder-splitter:hover,
.builder-splitter:active {
  background: var(--accent, #c9a05a);
}

/* ── Form panel ── */
.form-panel {
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  background: var(--slate-100);
  border-right: 1px solid var(--slate-200);
  /* Isolate the panel's tall scrollable content from the root scroller. Without
     this, Chrome leaks this grid-item scroll container's overflow up to the
     document, producing thousands of px of dead blank scroll below the fixed-
     height desktop app (overflow:hidden on .builder-wrap does NOT stop it). On
     mobile the panel is auto-height so there is nothing to clip. */
  contain: layout paint;
}

/* Native date inputs carry a wide intrinsic min-content size; in a 1fr/1fr
   .grid-2 that forces the two tracks past the card edge on narrow phones (the
   DUE DATE input bled to the viewport edge). min-width:0 lets the grid tracks
   shrink, and the date control fills its (now shrinkable) track. */
.form-panel .field { min-width: 0; }
.form-panel input[type="date"] { width: 100%; min-width: 0; }

/* ── Template picker — collapsed disclosure (invoice-edit.html, APP-4; and
   create.html since the 2026-09 Danish fold fix) ──
   Opt-in via `.template-picker` on the card. On the public builder the first
   field comes first and the gallery waits behind one "Template: Clean ·
   change" row (plan BUILDER_DA_FOLD_FIX_PLAN_2026-09.md P2.2); create.js opens
   it on first paint only for a ?tpl=<pro> deep link, so the locked teaser
   still shows. */
.template-picker { padding: 12px 16px; }

.template-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.template-toggle .card-title { margin-bottom: 2px; }

.template-toggle-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.template-current {
  font-size: .9rem;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.template-toggle-action {
  flex: none;
  font-size: .78rem;
  font-weight: 600;
  color: var(--accent);
  border: 1px solid var(--slate-200);
  border-radius: 999px;
  padding: 5px 12px;
}

.template-toggle:hover .template-toggle-action,
.template-toggle:focus-visible .template-toggle-action { border-color: var(--accent); }

.template-picker-body { margin-top: 12px; }
.template-picker-body[hidden] { display: none; }

/* ── Builder steps — four numbered cards (create.html, plan P2.3) ──
   Step 1 opens by default; the others are collapsed headers, and opening
   one never closes another. The header is a real <button> inside an <h2>,
   so the steps read as headings and toggle from the keyboard. */
.card.step { padding: 0; }

.step-heading { margin: 0; font: inherit; }

.step-head {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  margin: 0;
  padding: 16px 20px;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: .98rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.step-num {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--rule);
  color: var(--ink-2);
  font-size: .8rem;
  font-weight: 600;
}

.step-head[aria-expanded="true"] .step-num {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.step-title { flex: 1; min-width: 0; }

.step-head::after {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  margin-right: 4px;
  transition: transform 150ms ease;
}

.step-head[aria-expanded="true"]::after { transform: rotate(-135deg); }

.step-head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.step-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 20px 20px;
}

.step-body[hidden] { display: none; }

.step-subtitle { margin: 16px 0 4px; }

@media (prefers-reduced-motion: reduce) {
  .step-head::after { transition: none; }
}

/* ── Mobile peek row — "see what it will look like" (plan P6) ──
   Phones only (the mobile tab bar's breakpoint); a tap switches to the
   existing preview tab. */
.create-peek { display: none; }

@media (max-width: 860px) {
  .create-peek {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    margin: 0;
    padding: 10px 14px;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--paper-hi);
    color: var(--ink);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }

  .create-peek-paper {
    flex: none;
    width: 30px;
    height: 40px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    background:
      linear-gradient(var(--rule), var(--rule)) 5px 8px / 14px 2px no-repeat,
      linear-gradient(var(--rule), var(--rule)) 5px 15px / 20px 2px no-repeat,
      linear-gradient(var(--rule), var(--rule)) 5px 20px / 20px 2px no-repeat,
      linear-gradient(var(--accent), var(--accent)) 13px 31px / 12px 3px no-repeat,
      var(--paper);
  }

  .create-peek-text {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    font-size: .8rem;
    color: var(--ink-2);
  }

  .create-peek-text strong { font-size: .9rem; color: var(--ink); }

  .create-peek-go { flex: none; font-size: 1.1rem; color: var(--accent); }
}

/* ── Editor action bar — hierarchy + "More actions" disclosure (APP-5) ── */
.editor-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 4px;
}

.editor-actions-primary {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.editor-actions-primary .btn {
  flex: 1;
  justify-content: center;
  padding: 13px;
}

/* Send is the only control here that leaves the building — give it the extra
   width and weight the roast asked for so it never reads as one of a row of
   equals. */
.editor-actions-primary .btn-send {
  flex: 1.5;
  font-weight: 700;
}

.editor-more-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 6px 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
}

.editor-more-toggle:hover { color: var(--ink); }

.editor-more-menu {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper-hi);
}

.editor-more-menu[hidden] { display: none; }

.editor-more-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.editor-more-item:hover { background: var(--slate-100); }

.editor-more-label {
  font-size: .86rem;
  font-weight: 600;
  color: var(--ink);
}

.editor-more-desc {
  font-size: .76rem;
  font-weight: 400;
  line-height: 1.35;
  color: var(--muted);
  white-space: normal;
}

/* ── Template picker ── */
.template-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
  margin-top: 4px;
}

/* Group headings ("Free · 4" / "Pro · 12") — direct grid children spanning
   both columns, styled like the page's own .card-title small-caps label
   (main.css) so the grouping reads as part of the same design system. */
.tpl-group-heading {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--slate-400);
  margin-top: 6px;
}

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

/* ── Reused gold "PRO" pill — the PennaDye badge (create.html), the Pro group
   heading, and each locked template card's badge all render this same class
   so "Pro" reads identically everywhere on the page. */
.pro-pill {
  display: inline-block;
  font-size: .7rem;
  font-weight: 700;
  background: var(--accent, #c9a05a);
  color: var(--on-gold, #1b1a17);
  padding: 2px 7px;
  border-radius: 10px;
  letter-spacing: .02em;
  vertical-align: middle;
}

.tpl-card {
  min-width: 0;
  border: 2px solid var(--slate-200);
  border-radius: var(--radius);
  padding: 8px;
  cursor: pointer;
  text-align: center;
  transition: border-color .15s, box-shadow .15s;
  background: var(--paper-hi);
  position: relative;
}

.tpl-card:hover { border-color: var(--accent); }

.tpl-card.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(201,160,90,.18);
}

/* Locked (Pro, not-yet-unlocked) card — dimmed AND grayscaled, so a locked
   card is unmistakable even at a glance (opacity alone at .65 wasn't enough
   contrast against the free cards — the bug Rune found 2026-09-10). Still
   fully clickable — cursor stays the inherited `pointer` from .tpl-card
   above (Rune's follow-up review: locked cards select + preview like free
   ones, they just can't be downloaded — see updateLockedState() in
   create.js). */
.tpl-card.premium {
  opacity: .55;
}

.tpl-card.premium .tpl-thumb-wrap {
  filter: grayscale(60%);
}

.tpl-card .pro-pill {
  position: absolute;
  top: 5px;
  right: 5px;
  font-size: .6rem;
  padding: 2px 6px;
  z-index: 1;
}

.tpl-thumb-wrap {
  width: 100%;
  /* Crop to the top ~40% of the A4 page instead of the whole sheet (audit:
     docs/audits/invoice-template-integrity-2026-08-11.md §5.4). Templates differ most in the
     masthead — the line-item table below it looks near-identical across all of them — so
     showing the full page at scale(0.177) in this ~141px cell buried the one region that
     actually distinguishes templates below legible resolution (11/12 thumbnails measured
     23-33 mean pixel-diff apart; only `bold`'s dark body stood out). The iframe keeps its
     existing width-fit scale (`scale(wrap.offsetWidth / 794)`, unchanged in builder.js/
     create.js/invoice-editor.js) — only this box's aspect-ratio changes, from the full A4
     ratio (0.707 = 210/297mm) to 1.77 (794px wide / (1123px tall * 0.40)), so `overflow:
     hidden` clips everything below the top 40% of the rendered page. Same cell width as
     before; only the (previously mostly-empty) card height shrinks to match the crop. */
  aspect-ratio: 1.77;
  background: var(--slate-100);
  border-radius: 3px;
  margin-bottom: 5px;
  overflow: hidden;
  position: relative;
}

.tpl-thumb-iframe {
  width: 794px;
  height: 1123px;
  border: none;
  transform-origin: top left;
  pointer-events: none;
  display: block;
}

.tpl-name {
  font-size: .7rem;
  font-weight: 600;
  color: var(--slate-600);
}

/* Still used by the AUTHED invoice editor's own picker (invoice-editor.js) —
   out of scope for this PR (public builder only). The public builder's
   picker (create.js) moved to the shared .pro-pill (above) 2026-09-10 for
   better contrast at 100px card width; .lock-badge stays for the editor. */
.lock-badge {
  position: absolute;
  top: 5px;
  right: 5px;
  background: var(--slate-700);
  color: #fff;
  font-size: .55rem;
  font-weight: 700;
  padding: 2px 5px;
  border-radius: 3px;
  letter-spacing: .3px;
}

/* One-line teaser notice shown above #btn-download when the selected
   template is Pro-locked (arrived via ?tpl=<pro>, item 3 of the free-builder
   Pro-teaser change). Hidden by default via the [hidden] attribute. */
.tpl-pro-notice {
  font-size: .8rem;
  line-height: 1.4;
  color: var(--slate-600);
  background: var(--slate-100);
  border: 1px solid var(--slate-200);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin-bottom: 8px;
}

.tpl-pro-notice[hidden] { display: none; }

/* ── "Set as default" star (authed invoice editor only) ──
   Top-LEFT so it never collides with the PRO lock-badge top-right. Rendered
   on every card including locked ones: choosing a default is a preference
   every tier may express (rendering a Pro template stays gated elsewhere). */
.tpl-default-btn {
  position: absolute;
  top: 3px;
  left: 3px;
  z-index: 2;
  background: var(--paper-hi);
  border: 1px solid var(--slate-200);
  border-radius: 50%;
  width: 20px;
  height: 20px;
  padding: 0;
  line-height: 1;
  font-size: .7rem;
  color: var(--slate-400);
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, color .15s, border-color .15s;
}

/* Revealed on hover/focus so 15 stars don't compete with the thumbnails —
   but always visible once one is the default, and always reachable by keyboard. */
.tpl-card:hover .tpl-default-btn,
.tpl-default-btn:focus-visible,
.tpl-default-btn.is-default { opacity: 1; }

.tpl-default-btn:hover { color: var(--accent); border-color: var(--accent); }

.tpl-default-btn.is-default {
  color: var(--accent);
  border-color: var(--accent);
}

.tpl-default-tag {
  margin-top: 2px;
  font-size: .55rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .3px;
  color: var(--accent);
}

.tpl-default-tag[hidden] { display: none; }

/* ── Colour presets ── */
.colour-preset {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  transition: transform .1s, border-color .1s;
}
.colour-preset:hover { transform: scale(1.2); }
.colour-preset.active { border-color: #fff; box-shadow: 0 0 0 2px var(--accent); }

/* ── Texture swatches (invoice editor) ── */
.texture-opt {
  width: 32px;
  height: 32px;
  border-radius: 6px;
  border: 2px solid var(--slate-200);
  cursor: pointer;
  padding: 0;
  transition: transform .1s, border-color .1s, box-shadow .1s;
  position: relative;
}
.texture-opt:hover { transform: scale(1.1); border-color: var(--slate-300); }
.texture-opt.active { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(201,160,90,.25); }

/* ── Line items ── */
/* Header row (labels) */
.line-items-header {
  display: grid;
  grid-template-columns: 1fr 64px 80px 80px 28px;
  gap: 4px;
  padding: 0 2px 4px;
}

.line-items-header span {
  font-size: .7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--slate-400);
}

/* Each line item: description full-width on top, then qty/rate/amount/remove below */
.line-item-row {
  background: var(--slate-50);
  border: 1px solid var(--slate-200);
  border-radius: var(--radius-sm);
  padding: 8px;
  margin-bottom: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.line-item-desc input {
  width: 100%;
  font-size: .85rem;
}

.line-item-nums {
  display: grid;
  grid-template-columns: 64px 1fr 80px 28px;
  gap: 4px;
  align-items: center;
}

.line-item-row input { font-size: .85rem; }

.line-item-amount {
  font-size: .85rem;
  font-weight: 600;
  color: var(--slate-700);
  text-align: right;
  padding: 8px;
  background: var(--slate-50);
  border: 1px solid var(--slate-200);
  border-radius: var(--radius-sm);
}

/* ── Totals summary ── */
.totals-summary {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-end;
  padding-top: 8px;
  border-top: 1px solid var(--slate-200);
}

.totals-row {
  display: flex;
  justify-content: space-between;
  width: 220px;
  font-size: .88rem;
  color: var(--slate-600);
}

.totals-row.grand {
  font-size: 1rem;
  font-weight: 700;
  color: var(--accent);
  border-top: 1.5px solid var(--accent);
  padding-top: 8px;
  margin-top: 4px;
}

/* ── Preview panel ── */
.preview-panel {
  overflow: auto;
  background: var(--slate-200);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px 16px;
  gap: 0;
  position: relative;
}

/* Zoom controls — top-right corner of the preview panel */
.preview-zoom {
  position: absolute;
  top: 12px;
  right: 16px;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #fff;
  border: 1px solid var(--slate-200);
  border-radius: 8px;
  padding: 2px 4px;
  box-shadow: 0 1px 3px rgba(0,0,0,.08);
  font-family: inherit;
}
.preview-zoom .zoom-btn {
  border: 0;
  background: transparent;
  color: var(--slate-700);
  font-size: 16px;
  line-height: 1;
  font-weight: 600;
  width: 28px;
  height: 28px;
  border-radius: 5px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.preview-zoom .zoom-btn:hover { background: var(--slate-100); color: var(--slate-900); }
.preview-zoom .zoom-btn:active { transform: scale(.96); }
.preview-zoom .zoom-pct {
  font-size: 12px;
  font-weight: 600;
  color: var(--slate-700);
  min-width: 42px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.preview-label {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--slate-400);
  margin-bottom: 12px;
  align-self: flex-start;
  padding-left: 4px;
}

.preview-page {
  width: 100%;
  max-width: 680px;
  aspect-ratio: 0.707;
  background: #fff;
  box-shadow: var(--shadow-lg);
  border-radius: 2px;
  overflow: hidden;
  position: relative;
}

.preview-page iframe {
  width: 794px;  /* A4 at 96dpi */
  height: 1123px;
  border: none;
  transform-origin: top left;
  display: block;
}

/* ── PRO banner over the live preview ──
   Rune's review of the Pro-teaser PR: locked cards are now clickable and
   select + preview like free ones (create.js updateLockedState()), so the
   preview itself needs an unmistakable "this is Pro, not yours yet" signal —
   a big gold ribbon over the rendering. Shown only while the selected
   template is Pro-locked; hidden for free templates and for Pro users
   (same predicate updateLockedState() uses for the button/notice, so all
   three always agree). `pointer-events: none` on the overlay itself so the
   preview underneath stays scrollable/inspectable — this is a teaser, not a
   blocker — with `pointer-events: auto` re-enabled on just the ribbon/sash
   link (below) so that one element stays clickable.

   Follow-up 2 (Rune + wife, 2026-09-10): the ribbon is now a real link to
   /pricing (create.js's pricingHref(), same target the "Unlock with Pro →"
   button already used), and a second visual — `sash` — was added for them
   to compare against the original `stamp`. Pick between them with the
   `data-variant` attribute on #preview-pro-banner in create.html /
   da/create.html (one-attribute flip per twin; "stamp" is the shipped
   default until Rune says otherwise).

   Follow-up 3 (Rune, 2026-09-10): "nothing like a stamp" — the `stamp`
   variant's gold rounded box + drop shadow is replaced with an actual
   postmark: a circular double ring (`.postmark-ring-outer/-inner`), the
   `builder.templates.postmarkRing` string running around the top of the
   ring on an SVG `<textPath>`, and "PRO" + a short rule in the centre —
   modelled on classic travel postmarks (city arced over the top, single
   ink colour, worn/broken edges). The whole thing is inline SVG
   (`.preview-pro-banner-postmark`) inside the same `<a>`, so the link,
   `data-cta` and aria-label wiring from Follow-up 2 are untouched — only
   the visual inside changed. The old `.preview-pro-banner-ribbon` box
   styling (background/box-shadow/border-radius/rotate) is switched off
   for `stamp` specifically, below; `sash` still uses it as-is.

   The page-wide diagonal tint that used to be `stamp`'s own background is
   now OFF for both variants — a postmark reads as the signal on its own,
   and Rune's actual complaint ("nothing like a stamp") was about the
   ribbon shape, not the tint, so there's no reason to keep double-signalling
   once the ribbon itself looks like a postmark. */
#preview-pro-banner {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

#preview-pro-banner[hidden] { display: none; }

/* Neither variant tints the page anymore (see Follow-up 3 note above) —
   the rule stays so a future variant can opt back in per-attribute. */
#preview-pro-banner[data-variant="stamp"],
#preview-pro-banner[data-variant="sash"] {
  background: none;
}

.preview-pro-banner-ribbon {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: var(--accent, #c9a05a);
  color: var(--on-gold, #1b1a17);
  padding: 14px 34px;
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0,0,0,.28);
  transform: rotate(-4deg);
  text-align: center;
  text-decoration: none;
  pointer-events: auto;
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease;
}

.preview-pro-banner-ribbon:hover,
.preview-pro-banner-ribbon:focus-visible {
  transform: rotate(-4deg) translateY(-2px) scale(1.03);
  box-shadow: 0 12px 30px rgba(0,0,0,.36);
}

.preview-pro-banner-ribbon:focus-visible {
  outline: 2px solid var(--on-gold, #1b1a17);
  outline-offset: 3px;
}

.preview-pro-banner-pro {
  font-size: clamp(1.1rem, 6vw, 1.7rem);
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  line-height: 1;
}

.preview-pro-banner-text {
  font-size: clamp(.62rem, 2.6vw, .78rem);
  font-weight: 600;
  letter-spacing: .01em;
}

/* ── Postmark variant (Follow-up 3, replaces the old stamp box) ──
   `.preview-pro-banner-ribbon`'s own box look (gold fill, shadow, radius,
   -4deg rotate, lift-on-hover) is reset to nothing for `stamp` — the
   postmark SVG below is the entire visual, so the `<a>` becomes a plain
   rotated frame around it. The `.preview-pro-banner-pro` / `-text` spans
   stay in the DOM (visually hidden) purely so the link's accessible name
   still has real text content backing the aria-label, and so the existing
   `common.pro` / `proPreviewBanner` i18n keys don't need a duplicate
   translation just for this variant. */
#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-ribbon {
  /* width/height: 100% gives the SVG's own `width: clamp(..., 34%, ...)` a
     definite percentage basis — #preview-pro-banner (this element's
     containing block) is absolutely positioned with `inset: 0` against
     .preview-page, so 100% here always equals .preview-page's own size on
     any viewport. Without this, the <a> shrink-wraps to its content (the
     SVG), the SVG's 34% has no definite ancestor to resolve against, and
     the browser falls back to resolving it against something else
     entirely — observed on the 390px mobile preview tab as an oversized,
     clipped postmark instead of a proportional one.

     Follow-up 5 (Rune, 2026-09-10): because this <a> is stretched to the
     whole .preview-page, the shared .preview-pro-banner-ribbon rule's
     `pointer-events: auto` made the ENTIRE preview clickable-to-/pricing —
     not just the postmark, which is the actual visible click affordance.
     Overridden to `none` here so the link's own box is inert; the postmark
     SVG below re-enables `auto` on itself so only that art is the click
     target. The <a> stays in the DOM/tab order either way — pointer-events
     only affects mouse/touch hit-testing, not keyboard focusability.

     Follow-up 6 (Rune, 2026-09-10): "move it to the bottom of the page
     instead of the middle". This box's own `flex-direction: column` comes
     from the shared `.preview-pro-banner-ribbon` rule above (untouched here)
     — so for THIS box, `justify-content` is the main (vertical) axis and
     `align-items` is the cross (horizontal) axis, the reverse of a row
     layout. Moving to the bottom is therefore a `justify-content` change.

     Follow-up 8 (final, Rune's pick — option 3, 2026-09-10): `align-items`
     moved from `center` to `flex-end` so the postmark seats bottom-RIGHT
     instead of bottom-centre — see `.preview-pro-banner-postmark`'s
     `margin-right: 8%` below for the matching horizontal offset. */
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  width: 100%;
  height: 100%;
  background: none;
  padding: 0;
  border-radius: 0;
  box-shadow: none;
  transform: none;
  pointer-events: none;
  cursor: default;
}

#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-ribbon:hover,
#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-ribbon:focus-visible {
  transform: none;
  box-shadow: none;
}

#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-pro,
#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  padding: 0;
  margin: -1px;
}

/* The postmark itself. `mix-blend-mode: multiply` + ~.78 opacity read as
   ink printed on the paper underneath rather than an opaque sticker;
   `postmark-ink` (feTurbulence + feDisplacementMap) breaks up the ring/
   text edges so they read as struck, not vector-perfect. Sized off
   `.preview-page`'s own width (clamp, not the ribbon's), and rotated
   -12deg as a whole so the rings/text/rule all tilt together.

   Follow-up 6 (Rune, 2026-09-10): "move it to the bottom of the page
   instead of the middle" — the old `translateY(-20%) rotate(-12deg)` struck
   the mark above centre; positioning now comes from the ribbon's own
   `justify-content: flex-end` (above) plus the `margin-bottom` below, so
   translateY is dropped entirely (rotate stays, applied last so the box is
   already in its resting position when it tilts).

   `margin-bottom` on this axis resolves as a PERCENTAGE OF THE CONTAINING
   BLOCK'S WIDTH, per the CSS box model spec — not its height, even though
   this is a bottom margin. `.preview-page` has a fixed `aspect-ratio: 0.707`
   (A4, width:height), so "N% of the page's height" converts to a
   width-relative percentage as N% / 0.707. `calc(5% / 0.707)` ≈ 7.07% of
   the container's width, which — given that fixed aspect ratio — equals
   exactly 5% of the page's height, landing the postmark's bottom edge 5%
   up from the page's bottom edge on any viewport (desktop and the 390px
   mobile preview tab alike), with its visual centre around 80-85% down the
   page. See tests/test_builder_premium_blocked_event.py for the numbers. */
.preview-pro-banner-postmark {
  display: none;
}

/* Follow-up 7 (Rune, 2026-09-10): "postmark is clipped at the page's bottom
   edge" — investigated with Playwright getBoundingClientRect()/getBBox()
   measurements across desktop (1280w), the true (un-squashed) page height,
   and the 390px mobile tab, on all of bold/geometric/creative (dark, same-
   hue-sidebar, and gold-sidebar cases): in every one, the ring's own visual
   bottom sits 3-7% of the page height above `.preview-page`'s bottom edge,
   never clipped — `height: auto` on this SVG already resolves to a true
   square against `width`'s clamp() in current evergreen Chromium/Firefox/
   Safari, because the `viewBox="0 0 200 200"` with no `width`/`height`
   attributes gives the element a 1:1 intrinsic aspect ratio per the CSS
   Sizing spec, and that's honoured even as a flex item in the ribbon's
   column flex container.

   What WAS producing an apparent clip — a hard black bar slicing through
   the ring's lower arc in `e2e_artifacts/picker7/stamp_geometric.png` and
   every tile of `sheet_all_pro.png` — turned out to be the throwaway
   capture script, not the page: `scripts/capture_pro_banner_variants.py`'s
   `DESKTOP_VIEWPORT` was only 900px tall, shorter than `.preview-page`'s
   true un-squashed height (~962px + the ~112px header above it) once the
   script's own panel-clipping neutralization (see that script's
   `select_template_and_reveal_preview` comment) is applied. Playwright's
   `locator(".preview-page").screenshot()` then has to capture beyond the
   viewport, and composited the site's real footer (further down the DOM,
   normally off-screen) into the crop in place of the empty space that's
   actually there — confirmed by re-measuring with a tall-enough viewport
   and by a plain full-page screenshot after a manual `scrollIntoView()`,
   both of which show the complete ring with room to spare. Fixed at the
   source: `DESKTOP_VIEWPORT`'s height in that script, not here.

   `aspect-ratio: 1 / 1` + `flex-shrink: 0` below are added anyway as a
   defensive hardening, not a behavior change (verified identical rendered
   geometry before/after) — belt-and-suspenders against any future browser
   that doesn't infer the intrinsic ratio from `viewBox`, or a future flex
   context on this box that isn't `align-items: center` (which is what
   currently keeps `flex-shrink`'s default from ever engaging here). */
#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-postmark {
  display: block;
  width: clamp(150px, 34%, 260px);
  height: auto;
  aspect-ratio: 1 / 1;
  flex-shrink: 0;
  margin-bottom: calc(5% / 0.707);
  margin-right: 8%;
  opacity: .78;
  /* Follow-up 8 (final, Rune's pick — option 3, 2026-09-10): seated
     bottom-right (via the ribbon's `align-items: flex-end` above + this
     `margin-right`) instead of bottom-centre, because bottom-right clears
     every Pro template's sidebar outright rather than needing a backing
     disc to stay legible on top of one — verified on all 12 Pro templates
     2026-09-10, including Creative's ~38% and Geometric's ~22% sidebars
     (the two widest). The `.postmark-backing` halo disc from Follow-up 6
     is removed; `mix-blend-mode` is back to `multiply` (ink on paper —
     the look Rune actually wants) now that clearing the sidebar, not a
     blend mode, is what keeps the mark readable. Note: `#preview-pro-
     banner` establishes its own stacking context, which can isolate
     `mix-blend-mode` from blending with anything outside that context —
     in practice this postmark still only ever composites against
     `.preview-page`'s own white ground and the iframe content within it,
     so it behaves close to plain alpha; that's fine, it's the look that
     was approved. */
  mix-blend-mode: multiply;
  transform: rotate(-12deg);
  transition: opacity .15s ease;
  /* Follow-up 5: the postmark itself is now the sole click target for the
     stamp variant (see the ribbon override above) — re-enable hit-testing
     on the SVG's own box. */
  pointer-events: auto;
  cursor: pointer;
}

#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-ribbon:hover .preview-pro-banner-postmark,
#preview-pro-banner[data-variant="stamp"] .preview-pro-banner-ribbon:focus-visible .preview-pro-banner-postmark {
  opacity: .95;
}

.postmark-ring-outer,
.postmark-ring-inner {
  fill: none;
  stroke: var(--accent, #c9a05a);
}

.postmark-ring-outer { stroke-width: 3; }
.postmark-ring-inner { stroke-width: 2; }

.postmark-ring-text {
  fill: var(--accent, #c9a05a);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: .02em;
}

.postmark-pro-word {
  fill: var(--accent, #c9a05a);
  font-size: 46px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  /* Follow-up 5 (Rune, 2026-09-10): "center the PRO in the center of the
     inner circle (currently it's topside)". The markup now sets y="100" —
     the inner ring's own centre (cx=100 cy=100 r=70) — but SVG <text>'s
     default alignment-baseline positions the glyphs' baseline AT y, not
     their visual centre, which still reads as sitting high. `dominant-
     baseline: central` re-anchors the glyph box's vertical centre to y
     instead, so y="100" now lands the word exactly midway between the
     ring's top and bottom edges (70px either way). */
  dominant-baseline: central;
}

.postmark-rule {
  stroke: var(--accent, #c9a05a);
  stroke-width: 1.5;
}

/* ── Sash variant — a lightly see-through band running corner to corner of
   the template page, instead of a centered postmark.

   Follow-up 4 (Rune's wife, 2026-09-10): two fixes to the band above.
   (a) It only ran a third of the way up the left edge to just past the
   top-right — well short of true corner-to-corner. The page's own diagonal
   angle is atan(1.4144) ≈ 54.7° (aspect-ratio: 0.707 ⇒ height ≈ width ×
   1.4144); rotation is now -50deg (close to that, pulled in slightly so the
   tips land a little short of the actual corners rather than bleeding past
   `.preview-page`'s rounded edge), and the band is sized off the page's own
   diagonal (diagonal ≈ width × 1.732 for this aspect ratio; band length ≈
   0.88 × diagonal ⇒ width: 154%, left: -27% to keep it centred) so it now
   reaches from just short of the bottom-left corner to just short of the
   top-right one at any page size. Thickness moved from a small fixed
   padding to `height: clamp(28px, 9%, 56px)` — ~9% of the page's own height
   (the `%` resolves against `#preview-pro-banner`'s box, which is
   `inset: 0` against `.preview-page`), clamped so it can't collapse below
   28px or balloon past 56px.
   (b) The flat `rgba(201, 160, 90, .6)` fill crossed the Geometric
   template's gold sidebar and read as a muddy smear — no way to tell
   "this is a Pro teaser" from "the sidebar is discoloured". Fixed with
   `mix-blend-mode: darken` on a `::before` that carries the band's own
   fill: over the white page the gold shows through at ~62% strength (same
   read as before), but over anything already darker than that gold
   (Geometric's sidebar, a dark table header) `darken` keeps the backdrop's
   own colour per channel, so the band visually disappears there instead of
   muddying it — "only the white part should have the sash on". The PRO/
   teaser text sits in its own stacking layer on top of the `::before`,
   fully opaque and unblended (`var(--on-gold)`) so it stays legible either
   way — blending the text too would let it fade toward the backdrop same
   as the band does, which is the opposite of what copy needs.
   `overflow: hidden` on the overlay itself, on top of `.preview-page`'s own
   clip, is belt-and-suspenders so the longer band can never spill onto the
   grey canvas around the page. Row layout (not the stamp's stacked column)
   puts "PRO" and the teaser copy on one line, with an added em dash between
   them via ::after so the two independently i18n'd spans read as a single
   sentence without duplicating the string in markup. Hover/focus raises
   opacity only (no lift, no rotation change) — the base rule's `#…`-scoped
   selector already outranks the shared `.preview-pro-banner-ribbon:hover`
   rule's lift/box-shadow on specificity, so nothing needs to be re-declared
   here to cancel it. */
#preview-pro-banner[data-variant="sash"] {
  overflow: hidden;
}

#preview-pro-banner[data-variant="sash"] .preview-pro-banner-ribbon {
  flex-direction: row;
  justify-content: center;
  align-items: center;
  gap: clamp(6px, 2vw, 12px);
  position: absolute;
  top: 50%;
  left: -27%;
  width: 154%;
  height: clamp(28px, 9%, 56px);
  padding: 0;
  border-radius: 0;
  background: none;
  color: var(--on-gold, #1b1a17);
  box-shadow: none;
  opacity: .92;
  transform: rotate(-50deg) translateY(-50%);
  transform-origin: center;
  transition: opacity .15s ease;
}

#preview-pro-banner[data-variant="sash"] .preview-pro-banner-ribbon::before {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--accent, #c9a05a) 62%, transparent);
  mix-blend-mode: darken;
  z-index: 0;
}

#preview-pro-banner[data-variant="sash"] .preview-pro-banner-ribbon:hover,
#preview-pro-banner[data-variant="sash"] .preview-pro-banner-ribbon:focus-visible {
  opacity: 1;
}

#preview-pro-banner[data-variant="sash"] .preview-pro-banner-pro,
#preview-pro-banner[data-variant="sash"] .preview-pro-banner-text {
  position: relative;
  z-index: 1;
  font-size: clamp(.62rem, 2.2vw, .85rem);
  letter-spacing: .08em;
  white-space: nowrap;
}

#preview-pro-banner[data-variant="sash"] .preview-pro-banner-pro::after {
  content: " —";
}

/* ── Mobile tab toggle (hidden on desktop) ── */
.mobile-tabs { display: none; }

/* ── Mobile layout ── */
@media (max-width: 860px) {
  .builder-wrap {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    height: auto;
    overflow: visible;
  }

  .builder-splitter { display: none; }

  .mobile-tabs {
    display: flex;
    background: var(--slate-900);
    border-bottom: 1px solid var(--slate-700);
  }

  .mobile-tab {
    flex: 1;
    padding: 10px;
    text-align: center;
    font-size: .85rem;
    font-weight: 600;
    color: var(--rule);
    cursor: pointer;
    border-bottom: 2px solid transparent;
  }

  .mobile-tab.active {
    color: var(--paper);
    border-bottom-color: var(--accent);
  }

  /* Auto-height on phones, so it needn't be a scroll container — and it must
     not be one: overflow-y:auto / contain:paint made the builder's sticky
     "Hent PDF" bar stick inside the panel instead of the viewport, so it sat
     at the very end of the form, never in the fold (plan P6). min-width:0
     keeps the 1fr grid track at the phone's width — overflow:auto used to
     supply that implicitly. */
  .form-panel  { height: auto; overflow: visible; contain: none; min-width: 0; }
  .preview-panel { height: 70vw; min-height: 320px; }

  /* .preview-panel is a fixed-height (70vw / min 320px) column flex
     container with overflow:auto. .preview-page's default flex-shrink:1
     let the flex algorithm compress it below the height its own
     aspect-ratio (0.707) demands whenever that height exceeds the panel's —
     the near-guaranteed case on a phone once .preview-page's width nears
     its 680px max-width. Because .preview-page also has overflow:hidden,
     the shrunk box clipped the rendered A4 page (JS's ResizeObserver in
     create.js keeps re-asserting the correct inline height from the
     current width, but flex-shrink kept squashing it straight back down —
     the O-4 "squash" bug). flex-shrink: 0 keeps .preview-page at its full
     aspect-ratio height; the now-taller box simply scrolls inside
     .preview-panel like any other overflowing content. */
  .preview-page { flex-shrink: 0; }

  .preview-panel.hidden,
  .form-panel.hidden { display: none; }
}
