/* Editor + preview. The canvas is a column of block rows; each row is a
   borderless input that inherits the block type's typography, with a floating
   toolbar that appears on hover/focus. */

.editor-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: var(--size-4);
  /* same vertical rhythm as .site-header, so the brand lockup doesn't
     jump when moving between site pages and the app bar */
  padding: var(--size-5);
  background-color: var(--background);
  border-block-end: 1px solid var(--border);

  /* Appears instantly when a save starts, then lingers 600ms and fades
     200ms after it ends: a sub-100ms round-trip reads as one calm "Saving"
     beat instead of a flicker, and back-to-back debounced saves re-enter
     the visible state before the fade starts, merging into one. */
  .save-state {
    font-size: var(--scale-00);
    color: var(--muted-foreground);
    opacity: 0;
    transition: opacity 0.2s var(--ease-out-quad) 0.6s;

    &.saving {
      opacity: 1;
      transition: none;
    }
  }

  /* The multiplayer headcount; empty (and invisible) while editing alone,
     so it costs the header no space until a second editor connects. */
  .presence {
    display: flex;
    align-items: center;
    gap: var(--size-2);
    font-size: var(--scale-00);
    color: var(--muted-foreground);

    .presence-dot {
      inline-size: 0.5em;
      block-size: 0.5em;
      border-radius: 50%;
      background-color: var(--primary);
    }
  }

  nav {
    display: flex;
    align-items: center;
    gap: var(--size-1);
    margin-inline-start: auto;
  }

  /* Nav items are one family of quiet controls: links and buttons share a
     ghost treatment — plain at rest, the theme switcher's accent pill on
     hover — so the switcher reads as their icon-shaped sibling instead of
     the row's only real control. (.brand keeps its site-chrome styling.) */
  nav a,
  nav .nav-btn {
    display: inline-flex;
    align-items: center;
    padding: var(--size-1) var(--size-2);
    border: none;
    border-radius: var(--radius-xs);
    background: none;
    color: var(--muted-foreground);
    font-family: inherit;
    font-size: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.1s var(--ease-out-quad),
      color 0.1s var(--ease-out-quad);

    &:hover {
      background-color: var(--accent);
      color: var(--accent-foreground);
      text-decoration: none;
    }
  }
}

.editor-main {
  /* body is a flex column (sticky footer); auto inline margins on a flex
     item disable stretching and shrink-wrap it to its content, so fill
     explicitly and let max-width cap + center as in block layout. */
  inline-size: 100%;
  max-width: var(--width-md);
  margin-inline: auto;
  padding-inline: var(--size-5);
  padding-block: var(--size-10) var(--size-24);
}

/* Floating theme switcher for the chromeless pages (fill, done): those
   render the creator's document, so they get no Borderforms header. */
.page-theme {
  position: fixed;
  inset-block-start: var(--size-3);
  inset-inline-end: var(--size-5);
  z-index: var(--layer-1);
}

.canvas {
  --gutter: 6rem;
  display: flex;
  flex-direction: column;
  counter-reset: numbered page 1;
}

/* Numbered runs count themselves: any non-numbered block resets the counter
   for the siblings after it, so renumbering after reorder or insert is free. */
.block:not(.block-numbered) {
  counter-reset: numbered;
}

/* Each block owns the toolbar gutter: negative margin + matching padding
   extend its box left over the margin while the text stays in the column.
   Hovering anywhere in the gutter is hovering the block, and a drag straight
   down the gutter keeps the pointer over sibling blocks, so drop targeting
   works without drifting into the text column. */
.block {
  position: relative;
  margin-inline-start: calc(-1 * var(--gutter));
  padding-inline-start: var(--gutter);
}

.block-input {
  display: block;
  width: 100%;
  padding-block: var(--size-1);
  padding-inline: var(--size-1);
  border: none;
  border-radius: var(--radius-xs);
  background-color: transparent;
  color: var(--foreground);
  font-family: inherit;
  font-size: var(--scale-1);
  line-height: var(--line-md);
  /* contenteditable: wraps at the column edge and grows like a document
     line; 1lh keeps an empty block one line tall. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  min-block-size: 1lh;
  cursor: text;

  /* Active-block cue: a faint foreground tint rather than --surface, which
     is the answer inputs' fill and made the focused block read as one. */
  &:focus {
    outline: none;
    background-color: color-mix(in oklab, var(--foreground) 5%, transparent);
  }

  /* editor.js clears the stray <br> from emptied blocks so :empty matches. */
  &[contenteditable]:empty::before {
    content: attr(data-placeholder);
    color: var(--muted-foreground);
    opacity: 0.55;
    pointer-events: none;
  }

  a {
    color: var(--primary);
    text-decoration: underline;
  }
}

.block-h1 .block-input {
  padding-block: var(--size-2);
  font-size: var(--scale-fluid-5);
  font-weight: var(--weight-bold);
  line-height: var(--line-xs);
}

.block-h2 .block-input {
  padding-block: var(--size-2);
  font-size: var(--scale-fluid-4);
  font-weight: var(--weight-bold);
  line-height: var(--line-xs);
}

.block-h3 .block-input {
  padding-block: var(--size-1);
  font-size: var(--scale-fluid-3);
  font-weight: var(--weight-semibold);
  line-height: var(--line-sm);
}

/* Bold should move the same perceptual distance everywhere. Body text jumps
   400 -> 700; blocks with a heavier base weight need their bold pushed
   further, or it reads thinner than body bold (and inside headings it would
   not read at all). */
.block-field .block-input :is(b, strong) {
  font-weight: var(--weight-extrabold);
}

.block-h1 .block-input :is(b, strong),
.block-h2 .block-input :is(b, strong),
.block-h3 .block-input :is(b, strong) {
  font-weight: var(--weight-black);
}

/* First-line metrics for the toolbar-centering formula. */
.block-h1 {
  --first-line-pad: var(--size-2);
  --first-line: calc(var(--scale-fluid-5) * var(--line-xs));
}

.block-h2 {
  --first-line-pad: var(--size-2);
  --first-line: calc(var(--scale-fluid-4) * var(--line-xs));
}

.block-h3 {
  --first-line-pad: var(--size-1);
  --first-line: calc(var(--scale-fluid-3) * var(--line-sm));
}

.block-divider {
  --first-line-pad: 0px;
  --first-line: calc(var(--size-3) * 2 + 1px); /* the whole divider row */
}

.block-numbered {
  display: flex;
  align-items: flex-start;

  &::before {
    counter-increment: numbered;
    content: counter(numbered) ".";
    margin-inline-start: var(--size-4);
    margin-inline-end: var(--size-2);
    padding-block-start: var(--size-1);
    min-inline-size: var(--size-4);
    text-align: end;
    color: var(--muted-foreground);
    font-size: var(--scale-1);
    line-height: var(--line-md);
    font-variant-numeric: tabular-nums;
  }
}

.block-bullet {
  display: flex;
  /* Top-align the dot and mirror the textarea's first-line metrics (padding,
     font size, line height) so it sits on line one of a wrapped block rather
     than centring against the whole block. The list indent lives on the dot's
     margin, not the block's padding: block padding is the toolbar gutter. */
  align-items: flex-start;

  &::before {
    content: "\2022";
    margin-inline-start: var(--size-4);
    margin-inline-end: var(--size-2);
    padding-block-start: var(--size-1);
    font-size: var(--scale-1);
    line-height: var(--line-md);
    color: var(--muted-foreground);
  }
}

/* Toolbar overlays the block's own gutter padding, so it never covers the
   text; visible while the block (gutter included) is hovered or its input
   focused. Top-aligned and hugging the text column's edge. */
.block-controls {
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  inline-size: var(--gutter);
  padding-inline-end: var(--size-2);
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s var(--ease-out-quad);

  .block-toolbar {
    /* Center the toolbar on the block's first visual line: each type
       declares its top padding and first-line height (defaults match the
       plain text block); 1.5rem + 4px is the toolbar's own height. */
    margin-block-start: calc(
      var(--first-line-pad, var(--size-1)) +
      (var(--first-line, calc(var(--scale-1) * var(--line-md))) - 1.5rem - 4px) / 2
    );
    display: flex;
    gap: var(--size-px);
    padding: var(--size-px);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
  }

  button {
    inline-size: var(--size-6);
    block-size: var(--size-6);
    display: grid;
    place-items: center;
    border: none;
    /* Concentric with the toolbar shell: its radius-sm minus the 1px
       border + 1px padding the button sits inside. */
    border-radius: calc(var(--radius-sm) - 2px);
    background-color: transparent;
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      background-color: var(--accent);
      color: var(--accent-foreground);
    }

    &.danger:hover {
      background-color: var(--destructive);
      color: var(--destructive-foreground);
    }

    &.drag-handle {
      cursor: grab;

      &:active {
        cursor: grabbing;
      }
    }
  }
}

/* The row being dragged, as SortableJS renders its placeholder. */
.block.sortable-ghost {
  opacity: 0.4;
}

/* Kill the inline-baseline gap so grid centering is exact. */
.icon-glyph {
  display: block;
}

/* Inline marks: the fixed palette the sanitizer allows; class-based so both
   themes restyle them. Values swap under html.dark and the system-dark
   fallback, mirroring global.css's theme mechanism. */
:root {
  --mark-c-zinc: var(--color-zinc-600);
  --mark-c-red: var(--color-red-600);
  --mark-c-amber: var(--color-amber-600);
  --mark-c-emerald: var(--color-emerald-600);
  --mark-c-sky: var(--color-sky-600);
  --mark-c-violet: var(--color-violet-600);
  --mark-hl-zinc: var(--color-zinc-200);
  --mark-hl-red: var(--color-red-200);
  --mark-hl-amber: var(--color-amber-200);
  --mark-hl-emerald: var(--color-emerald-200);
  --mark-hl-sky: var(--color-sky-200);
  --mark-hl-violet: var(--color-violet-200);
}

html.dark {
  --mark-c-zinc: var(--color-zinc-400);
  --mark-c-red: var(--color-red-400);
  --mark-c-amber: var(--color-amber-400);
  --mark-c-emerald: var(--color-emerald-400);
  --mark-c-sky: var(--color-sky-400);
  --mark-c-violet: var(--color-violet-400);
  --mark-hl-zinc: var(--color-zinc-700);
  --mark-hl-red: var(--color-red-900);
  --mark-hl-amber: var(--color-amber-900);
  --mark-hl-emerald: var(--color-emerald-900);
  --mark-hl-sky: var(--color-sky-900);
  --mark-hl-violet: var(--color-violet-900);
}

@media (prefers-color-scheme: dark) {
  html:not([class]),
  html[class=""] {
    --mark-c-zinc: var(--color-zinc-400);
    --mark-c-red: var(--color-red-400);
    --mark-c-amber: var(--color-amber-400);
    --mark-c-emerald: var(--color-emerald-400);
    --mark-c-sky: var(--color-sky-400);
    --mark-c-violet: var(--color-violet-400);
    --mark-hl-zinc: var(--color-zinc-700);
    --mark-hl-red: var(--color-red-900);
    --mark-hl-amber: var(--color-amber-900);
    --mark-hl-emerald: var(--color-emerald-900);
    --mark-hl-sky: var(--color-sky-900);
    --mark-hl-violet: var(--color-violet-900);
  }
}

.c-zinc { color: var(--mark-c-zinc); }
.c-red { color: var(--mark-c-red); }
.c-amber { color: var(--mark-c-amber); }
.c-emerald { color: var(--mark-c-emerald); }
.c-sky { color: var(--mark-c-sky); }
.c-violet { color: var(--mark-c-violet); }
.hl-zinc { background-color: var(--mark-hl-zinc); }
.hl-red { background-color: var(--mark-hl-red); }
.hl-amber { background-color: var(--mark-hl-amber); }
.hl-emerald { background-color: var(--mark-hl-emerald); }
.hl-sky { background-color: var(--mark-hl-sky); }
.hl-violet { background-color: var(--mark-hl-violet); }

/* Formatting bubble: positioned by editor.js over the current selection.
   Inverted against the page theme (dark bubble on the light canvas and the
   reverse) so it reads as a selection tooltip, not another surface-toned
   toolbar like the block controls. The --bubble-* locals swap under
   html.dark and the system-dark fallback, mirroring the mark palette. */
.fmt-menu {
  --bubble-bg: var(--color-zinc-900);
  --bubble-fg: var(--color-zinc-100);
  --bubble-muted: var(--color-zinc-400);
  --bubble-hover: var(--color-zinc-700);
  --bubble-border: var(--color-zinc-700);

  position: fixed;
  z-index: 30;
  display: none;
  /* A grid, not wrapped flex: a shrink-to-fit fixed element sizes wrapped
     flex as if nothing wraps, summing both rows into one wide bubble. The
     button columns alone set the width; the palette spans under them. */
  grid-template-columns: repeat(6, var(--size-6));
  gap: var(--size-px);
  padding: var(--size-px);
  background-color: var(--bubble-bg);
  border: 1px solid var(--bubble-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);

  &.open {
    display: grid;
  }

  /* Swatches are sized so the centred palette row fits inside the button
     row's width, so opening a palette never widens the bubble. */
  .fmt-palette {
    grid-column: 1 / -1;
    display: none;
    align-items: center;
    justify-content: center;
    gap: var(--size-1);
    padding: var(--size-1);
    border-block-start: 1px solid var(--bubble-border);
  }

  &.show-c .fmt-palette[data-kind="c"],
  &.show-hl .fmt-palette[data-kind="hl"] {
    display: flex;
  }

  /* Each swatch previews its mark on a canvas-coloured disc, so the preview
     stays true to the page even though the bubble behind it is inverted. */
  .fmt-swatch {
    inline-size: var(--size-4);
    block-size: var(--size-4);
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background-color: var(--background);
    color: var(--foreground);
    font-size: var(--scale-000);
    font-weight: var(--weight-semibold);
    /* The inherited 1.5 line box is taller than the disc and drags the A
       off centre; a bare line box centres it. */
    line-height: var(--line-none);
    cursor: pointer;

    &:hover {
      border-color: var(--ring);
    }
  }

  /* "No colour": a drawn diagonal, centred by construction (a text glyph
     never optically centres in a round button). */
  .fmt-swatch-clear {
    background-image: linear-gradient(
      135deg,
      transparent 46%,
      var(--destructive) 46%,
      var(--destructive) 54%,
      transparent 54%
    );
  }

  /* Re-state the palette values at swatch specificity: the scoped button
     styling above would otherwise out-rank the bare .c-*/.hl-* classes. */
  .fmt-swatch.c-zinc { color: var(--mark-c-zinc); }
  .fmt-swatch.c-red { color: var(--mark-c-red); }
  .fmt-swatch.c-amber { color: var(--mark-c-amber); }
  .fmt-swatch.c-emerald { color: var(--mark-c-emerald); }
  .fmt-swatch.c-sky { color: var(--mark-c-sky); }
  .fmt-swatch.c-violet { color: var(--mark-c-violet); }
  .fmt-swatch.hl-zinc { background-color: var(--mark-hl-zinc); }
  .fmt-swatch.hl-red { background-color: var(--mark-hl-red); }
  .fmt-swatch.hl-amber { background-color: var(--mark-hl-amber); }
  .fmt-swatch.hl-emerald { background-color: var(--mark-hl-emerald); }
  .fmt-swatch.hl-sky { background-color: var(--mark-hl-sky); }
  .fmt-swatch.hl-violet { background-color: var(--mark-hl-violet); }

  > button {
    inline-size: var(--size-6);
    block-size: var(--size-6);
    display: grid;
    place-items: center;
    padding: 0;
    border: none;
    /* Concentric with the bubble shell: its radius-sm minus the 1px
       border + 1px padding the button sits inside. */
    border-radius: calc(var(--radius-sm) - 2px);
    background-color: transparent;
    color: var(--bubble-muted);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      background-color: var(--bubble-hover);
      color: var(--bubble-fg);
    }
  }
}

html.dark .fmt-menu {
  --bubble-bg: var(--color-zinc-200);
  --bubble-fg: var(--color-zinc-900);
  --bubble-muted: var(--color-zinc-600);
  --bubble-hover: var(--color-zinc-300);
  --bubble-border: var(--color-zinc-300);
}

@media (prefers-color-scheme: dark) {
  html:not([class]) .fmt-menu,
  html[class=""] .fmt-menu {
    --bubble-bg: var(--color-zinc-200);
    --bubble-fg: var(--color-zinc-900);
    --bubble-muted: var(--color-zinc-600);
    --bubble-hover: var(--color-zinc-300);
    --bubble-border: var(--color-zinc-300);
  }
}

/* Textless blocks (divider, page break): the invisible readonly input on top
   keeps them clickable and in the keyboard flow; focus highlights the visual. */
.divider-input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  opacity: 0;
  cursor: default;
}

.divider-row {
  position: relative;
  padding-block: var(--size-3);

  hr {
    margin: 0;
    border: none;
    border-block-start: 1px solid var(--border);
  }
}

.block-divider:focus-within .divider-row hr {
  border-color: var(--ring);
}

/* Page break: dashed line with a numbered chip; the counter renumbers on
   reorder for free, like the numbered list. */
.block-page {
  counter-increment: page;
  --first-line-pad: 0px;
  --first-line: calc(var(--size-3) * 2 + var(--size-6));
}

.page-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--size-3);
  padding-block: var(--size-3);

  &::before,
  &::after {
    content: "";
    flex: 1;
    border-block-start: 1px dashed var(--border);
  }

  .page-chip {
    display: grid;
    place-items: center;
    block-size: var(--size-6);
    padding-inline: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background-color: var(--surface);
    color: var(--muted-foreground);
    font-size: var(--scale-00);

    &::after {
      content: "Page " counter(page);
    }
  }
}

.block-page:focus-within .page-row {
  &::before,
  &::after {
    border-color: var(--ring);
  }

  .page-chip {
    border-color: var(--ring);
  }
}

.block:hover .block-controls,
.block:focus-within .block-controls {
  opacity: 1;
  pointer-events: auto;
}

/* No room for a gutter on narrow screens: blocks span the column and the
   toolbar floats just above the row's left corner instead. */
@media (max-width: 1120px) {
  .block {
    margin-inline-start: 0;
    padding-inline-start: 0;
  }

  .block-controls {
    inset-inline-start: var(--size-1);
    inset-block-start: auto;
    inset-block-end: 100%;
    inline-size: auto;
    padding-inline-end: 0;
    padding-block-end: var(--size-1);
    justify-content: flex-start;

    .block-toolbar {
      margin-block-start: 0;
    }
  }

  .slash-menu {
    inset-inline-start: var(--size-1);
  }
}

/* Field blocks (question, choices, pickers, rating): the block text is the
   question label; the boxed .answer-input is shared with the preview so the
   editor is literally WYSIWYG. For typed answers its VALUE edits the
   placeholder the filler sees. */
.block-field .block-input {
  font-weight: var(--weight-medium);
}

.block-field {
  margin-block-end: var(--size-3);
}

.answer-input {
  display: block;
  inline-size: 100%;
  margin-block-start: var(--size-1);
  padding-block: var(--size-2);
  padding-inline: var(--size-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background-color: var(--surface);
  color: var(--foreground);
  font-family: inherit;
  font-size: var(--scale-0);

  &::placeholder {
    color: var(--muted-foreground);
    opacity: 0.7;
  }

  &:focus {
    outline: none;
    border-color: var(--ring);
  }
}

/* Indent the editor copy so its box lines up with where the block text
   starts (the textarea's inline padding). */
.block-field .answer-input {
  margin-inline-start: var(--size-1);
  inline-size: calc(100% - var(--size-1));
}

/* Long-answer textareas: fillers may only resize vertically so the form
   column holds its width. */
.answer-long {
  resize: vertical;
  line-height: var(--line-md);
}

/* Non-editable editor previews for native pickers. */
.answer-static {
  opacity: 0.7;
}

/* Field validation: an asterisk button at the label row's right edge opens
   a popover with the rules. The button follows the block toolbar's reveal
   (hover/focus) but pins itself, accented, once any rule is set. */

/* The label shares its right edge with the asterisk button: reserve the
   button's footprint (size-6 plus its size-1 inset and a matching size-1
   gap) so long labels wrap instead of running under it. */
.block-field .block-input {
  padding-inline-end: var(--size-8);
}

.field-rules {
  position: absolute;
  /* Mirror the text's inline padding so the button keeps the same breathing
     room from the highlight's right edge as the label does on the left. */
  inset-inline-end: var(--size-1);
  /* Center on the block's first visual line, like the block toolbar, but
     for a bare size-6 button (no border/padding to add). */
  inset-block-start: calc(
    var(--first-line-pad, var(--size-1)) +
    (var(--first-line, calc(var(--scale-1) * var(--line-md))) - var(--size-6)) / 2
  );

  summary {
    list-style: none;
    display: grid;
    place-items: center;
    inline-size: var(--size-6);
    block-size: var(--size-6);
    border-radius: var(--radius-xs);
    color: var(--muted-foreground);
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.1s var(--ease-out-quad);

    &::-webkit-details-marker {
      display: none;
    }

    &:hover {
      background-color: var(--accent);
      color: var(--accent-foreground);
    }
  }

  &.has-rules summary,
  &[open] summary {
    opacity: 1;
    color: var(--primary);
  }
}

.block:hover .field-rules summary,
.block:focus-within .field-rules summary {
  opacity: 1;
}

.rules-pop {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(100% + var(--size-1));
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: var(--size-2);
  inline-size: var(--size-44);
  padding: var(--size-3);
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}

.field-rule {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-2);
  color: var(--muted-foreground);
  font-size: var(--scale-000);
  cursor: pointer;

  /* The checkbox row reads label-last; keep the pair left-aligned. */
  &:has(input[type="checkbox"]) {
    justify-content: flex-start;
  }

  input[type="number"] {
    inline-size: var(--size-14);
    padding-block: 0;
    padding-inline: var(--size-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-xs);
    background-color: var(--background);
    color: var(--foreground);
    font-size: var(--scale-000);

    &:focus {
      outline: none;
      border-color: var(--ring);
    }
  }
}

/* The filler-facing "required" cue next to a field label. */
.req-mark {
  color: var(--destructive);
  margin-inline-start: var(--size-1);
}

/* Option lists in the editor: each row previews the control glyph next to a
   borderless input, mirroring block editing one level down. */
.options-editor {
  margin-block-start: var(--size-1);
  margin-inline-start: var(--size-1);
  counter-reset: opt;
}

.option-row {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  counter-increment: opt;

  .option-marker {
    position: relative;
    min-inline-size: var(--size-5);
    display: grid;
    place-items: center;
  }

  .option-glyph {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  /* Handle swaps in over the glyph on hover or when itself focused. */
  .option-drag {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-xs);
    background-color: var(--accent);
    color: var(--accent-foreground);
    cursor: grab;
    opacity: 0;

    &:active {
      cursor: grabbing;
    }
  }

  &:hover .option-drag,
  .option-marker:focus-within .option-drag {
    opacity: 1;
  }

  &:hover .option-glyph,
  .option-marker:focus-within .option-glyph {
    opacity: 0;
  }

  .option-input {
    flex: 1;
    padding-block: var(--size-1);
    padding-inline: var(--size-1);
    border: none;
    border-radius: var(--radius-xs);
    background-color: transparent;
    color: var(--foreground);
    font-family: inherit;
    font-size: var(--scale-0);

    &:focus {
      outline: none;
      background-color: var(--surface);
    }

    &::placeholder {
      color: var(--muted-foreground);
      opacity: 0.55;
    }
  }

  .option-delete {
    inline-size: var(--size-6);
    block-size: var(--size-6);
    display: grid;
    place-items: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-xs);
    background-color: transparent;
    color: var(--muted-foreground);
    cursor: pointer;
    opacity: 0;

    &:hover {
      background-color: var(--destructive);
      color: var(--destructive-foreground);
    }
  }

  &:hover .option-delete,
  &:focus-within .option-delete {
    opacity: 1;
  }
}

/* Glyphs live in CSS so the optimistic reorder needs no morph: dropdown
   numbers are a counter, like the numbered list. */
.block-choice .option-glyph::before {
  content: "\25CB";
}

.block-checkboxes .option-glyph::before,
.block-multiselect .option-glyph::before {
  content: "\25A1";
}

.block-dropdown .option-glyph::before {
  content: counter(opt) ".";
  font-variant-numeric: tabular-nums;
}

.add-option {
  margin-inline-start: var(--size-7);
  padding-block: var(--size-1);
  border: none;
  background: none;
  color: var(--muted-foreground);
  font-size: var(--scale-00);
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

/* Rating: radios in reverse DOM order; the checked input's DOM successors are
   the stars to its visual left, so :checked ~ label fills them without JS. */
.rating {
  display: inline-flex;
  flex-direction: row-reverse;
  justify-content: flex-end;

  input {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    opacity: 0;
  }

  label {
    color: var(--muted);
    font-size: var(--scale-4);
    line-height: var(--line-none);
    cursor: pointer;
  }

  input:checked ~ label,
  label:hover,
  label:hover ~ label {
    color: var(--warning);
  }

  input:focus-visible + label {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
  }
}

/* Editor-side rating preview: decorative only. */
.rating-static {
  margin-inline-start: var(--size-1);
  color: var(--muted);
  font-size: var(--scale-4);
  line-height: var(--line-none);
}

/* Slash popup: server-rendered inside each block, opened by editor.js when
   the block's text starts with "/". Anchored under the block so it needs no
   positioning logic. */
.slash-menu {
  position: absolute;
  inset-block-start: 100%;
  /* The block box starts at the gutter; anchor the menu to the text column. */
  inset-inline-start: calc(var(--gutter) + var(--size-1));
  z-index: 20;
  display: none;
  flex-direction: column;
  min-inline-size: 14rem;
  max-block-size: 16rem;
  overflow-y: auto;
  padding: var(--size-1);
  /* scrollIntoView aligns to the scrollport edge, ignoring padding; declare
     the padding as scroll-padding so the first/last options land fully in
     view with their breathing room. */
  scroll-padding-block: var(--size-1);
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);

  &.open {
    display: flex;
  }

  .slash-option {
    padding-block: var(--size-1);
    padding-inline: var(--size-2);
    border: none;
    border-radius: var(--radius-xs);
    background-color: transparent;
    color: var(--foreground);
    font-size: var(--scale-0);
    text-align: start;
    cursor: pointer;

    &.hidden {
      display: none;
    }

    &:hover {
      background-color: var(--accent);
      color: var(--accent-foreground);
    }

    &.selected {
      background-color: var(--primary-muted);
      color: var(--primary-muted-foreground);
    }
  }

  .slash-empty {
    display: none;
    padding-block: var(--size-1);
    padding-inline: var(--size-2);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  &.none .slash-empty {
    display: block;
  }

  /* editor.js sets .flip when the menu would run past the viewport bottom. */
  &.flip {
    inset-block-start: auto;
    inset-block-end: 100%;
  }
}

.add-block {
  align-self: flex-start;
  margin-block-start: var(--size-2);
  padding-block: var(--size-1);
  padding-inline: var(--size-1);
  border: none;
  background: none;
  color: var(--muted-foreground);
  font-size: var(--scale-0);
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

.preview {
  /* Blocks can hold Shift+Enter newlines; render them. */
  h1,
  h2,
  h3,
  p,
  li,
  label {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }

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

  /* Same bold-contrast normalization as the editor. */
  :is(h1, h2, h3) :is(b, strong) {
    font-weight: var(--weight-black);
  }

  .q-field :is(> label, legend, .q-label) :is(b, strong) {
    font-weight: var(--weight-extrabold);
  }

  .q-field {
    margin-block: var(--size-4);

    > label,
    .q-label,
    legend {
      display: block;
      margin-block-end: var(--size-1);
      padding: 0;
      font-weight: var(--weight-medium);
    }
  }

  .q-options {
    border: none;
    margin-inline: 0;
    padding: 0;

    .choice {
      display: flex;
      align-items: center;
      gap: var(--size-2);
      margin-block: var(--size-1);
      font-weight: var(--weight-regular);
      white-space: pre-wrap;
    }
  }

  select.answer-input[multiple] {
    block-size: auto;
  }

  hr {
    margin-block: var(--size-6);
    border: none;
    border-block-start: 1px solid var(--border);
  }

  .page-nav {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    margin-block-start: var(--size-8);
    padding-block-start: var(--size-4);
    border-block-start: 1px solid var(--border);

    .page-count {
      grid-column: 2;
      font-size: var(--scale-00);
      color: var(--muted-foreground);
    }

    button {
      padding-block: var(--size-1);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--surface);
      color: var(--foreground);
      font-size: var(--scale-0);
      cursor: pointer;

      &:hover {
        background-color: var(--accent);
      }

      &:first-child {
        grid-column: 1;
        justify-self: start;
      }

      &:last-child {
        grid-column: 3;
        justify-self: end;
      }
    }
  }

  h1 {
    margin-block: var(--size-6) var(--size-3);
    font-size: var(--scale-fluid-5);
    font-weight: var(--weight-bold);
    line-height: var(--line-xs);
  }

  h2 {
    margin-block: var(--size-5) var(--size-2);
    font-size: var(--scale-fluid-4);
    font-weight: var(--weight-bold);
    line-height: var(--line-xs);
  }

  h3 {
    margin-block: var(--size-4) var(--size-2);
    font-size: var(--scale-fluid-3);
    font-weight: var(--weight-semibold);
    line-height: var(--line-sm);
  }

  p {
    margin-block: var(--size-2);
    line-height: var(--line-md);
  }

  ul,
  ol {
    margin-block: var(--size-2);
    padding-inline-start: var(--size-5);
  }

  li {
    margin-block: var(--size-1);
    line-height: var(--line-md);
  }
}

/* Fill (public form) --------------------------------------------------- */

.preview {
  /* Explicit nav placement: the preview's first/last-child grid rules stop
     resolving once the fill nav holds a fourth (submit) button. */
  .page-nav {
    .nav-back {
      grid-column: 1;
      justify-self: start;
    }

    .nav-next {
      grid-column: 3;
      justify-self: end;
    }
  }

  /* Submit footer on a single-page form; paged forms submit from .page-nav. */
  .fill-submit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    row-gap: var(--size-3);
    justify-content: flex-end;
    margin-block-start: var(--size-8);
    padding-block-start: var(--size-4);
    border-block-start: 1px solid var(--border);

    button {
      padding-block: var(--size-1);
      padding-inline: var(--size-4);
      border-radius: var(--radius-sm);
      font-size: var(--scale-0);
      cursor: pointer;
    }
  }

  button[type="submit"],
  .page-nav button[type="submit"]:hover {
    border: 1px solid var(--primary);
    background-color: var(--primary);
    color: var(--primary-foreground);
  }

  button[type="submit"]:hover {
    opacity: 0.9;
  }
}

/* Results --------------------------------------------------------------- */

.results {
  /* The form's name, matching the settings page's h1 treatment. The response
     count below reads as its subtitle, so it loses its own top margin. */
  > h1 {
    margin-block-start: var(--size-6);
    font-size: var(--scale-3);
    font-weight: var(--weight-bold);
    line-height: var(--line-sm, 1.25);
  }

  .results-count {
    margin-block: var(--size-1) var(--size-2);
    font-size: var(--scale-00);
    color: var(--muted-foreground);
  }

  .results-empty {
    color: var(--muted-foreground);
    line-height: var(--line-md);
  }

  .results-tabs {
    display: flex;
    gap: var(--size-4);
    margin-block-end: var(--size-4);
    border-block-end: 1px solid var(--border);
  }

  .results-tab {
    padding-block: var(--size-2);
    color: var(--muted-foreground);
    text-decoration: none;
    font-size: var(--scale-00);
    font-weight: var(--weight-medium);
    /* Transparent placeholder keeps inactive tabs the same height. */
    border-block-end: 2px solid transparent;
    margin-block-end: -1px;

    &:hover {
      color: var(--foreground);
    }

    &.active {
      color: var(--foreground);
      border-block-end-color: var(--primary);
    }
  }

  /* Summary tab: one aggregate card per question. */
  .summary {
    display: grid;
    gap: var(--size-4);
    margin-block-end: var(--size-8);
  }

  .summary-card {
    padding: var(--size-4);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }

  /* Analytics funnel: a KPI row of stat tiles, which is the right form for a
     handful of headline numbers (a three-bar chart of unrelated totals would
     invite comparing bar heights that share no scale). The per-page and
     per-question breakdowns below reuse .dist, so the whole page speaks one
     visual language and this adds no new colour. */
  .metric-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: var(--size-3);
    margin-block-start: var(--size-3);
  }

  .metric-tile {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  /* Values wear a text token, not the meter's violet: a coloured number
     reads as a category. Deliberately no tabular-nums and well under hero
     size -- these are three peers, and proportional figures are what a
     standalone value wants. */
  .metric-value {
    font-size: var(--scale-4);
    font-weight: var(--weight-semibold);
    line-height: var(--line-xs, 1.1);
    color: var(--foreground);
  }

  .metric-label {
    font-size: var(--scale-00);
    color: var(--foreground);
  }

  .metric-note {
    font-size: var(--scale-000);
    color: var(--muted-foreground);
  }

  /* Deliberately not a flex row: this note wraps, and flex centring would
     align the icon to the middle of the whole wrapped block instead of to the
     first line. Inline flow is what .icon's vertical-align is built for, and
     the text wraps under it normally. */
  .analytics-caveat {
    margin-block-start: var(--size-3);

    .icon {
      --icon-size: 1em;
      /* The gap has to be a margin, not flex or markup whitespace: templ
         strips the whitespace between a component call and the text node
         next to it, so the icon and the sentence render touching. */
      margin-inline-end: var(--size-1);
    }
  }

  .summary-q {
    font-size: var(--scale-1);
    font-weight: var(--weight-semibold);
    line-height: var(--line-sm);
  }

  .summary-meta {
    margin-block: var(--size-1) var(--size-3);
    font-size: var(--scale-000);
    color: var(--muted-foreground);

    .summary-avg {
      margin-inline-start: var(--size-3);
      color: var(--warning);
      font-weight: var(--weight-semibold);
    }
  }

  .summary-none {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  .dist {
    display: grid;
    gap: var(--size-2);
  }

  .dist-row {
    display: grid;
    grid-template-columns: minmax(var(--size-16), max-content) 1fr minmax(var(--size-16), max-content);
    align-items: center;
    gap: var(--size-3);
    font-size: var(--scale-00);
  }

  .dist-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-inline-size: var(--size-64);
  }

  .dist-track {
    block-size: var(--size-2);
    border-radius: var(--radius-full);
    background-color: var(--muted);
    overflow: hidden;
  }

  .dist-bar {
    block-size: 100%;
    border-radius: var(--radius-full);
    background-color: var(--primary);
  }

  .dist-count {
    color: var(--muted-foreground);
    font-size: var(--scale-000);
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  .summary-answers {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: var(--size-1);
    font-size: var(--scale-00);
    line-height: var(--line-md);

    li {
      padding-block: var(--size-1);
      border-block-end: 1px solid var(--border);
      overflow-wrap: anywhere;

      &:last-child {
        border-block-end: none;
      }
    }
  }

  .summary-more {
    margin-block-start: var(--size-1);

    summary {
      cursor: pointer;
      color: var(--primary);
      font-size: var(--scale-000);
    }
  }

  /* Tombstoned questions: history, visually one register quieter. */
  .archived-head {
    margin-block-start: var(--size-8);
    font-size: var(--scale-1);
    font-weight: var(--weight-semibold);
  }

  .archived-note {
    margin-block: var(--size-1) var(--size-3);
    font-size: var(--scale-000);
    color: var(--muted-foreground);
  }

  .summary-card-archived {
    background-color: transparent;

    .summary-q {
      color: var(--muted-foreground);
    }

    .dist-bar {
      background-color: var(--muted-foreground);
    }
  }

  /* Responses tab: expandable rows, several can be open at once. */
  .resp-list {
    display: grid;
  }

  .resp-row {
    border-block-end: 1px solid var(--border);

    summary {
      display: grid;
      grid-template-columns: max-content 1fr;
      align-items: baseline;
      gap: var(--size-4);
      padding-block: var(--size-2);
      cursor: pointer;
      list-style: none;

      &::-webkit-details-marker {
        display: none;
      }

      &:hover .resp-preview {
        color: var(--foreground);
      }
    }
  }

  .resp-time {
    color: var(--muted-foreground);
    font-size: var(--scale-000);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }

  .resp-preview {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .resp-row[open] .resp-preview {
    display: none;
  }

  .resp-answers {
    margin: 0;
    padding: var(--size-2) 0 var(--size-4);
    display: grid;
    grid-template-columns: minmax(var(--size-32), max-content) 1fr;
    gap: var(--size-2) var(--size-6);
    font-size: var(--scale-00);

    dt {
      color: var(--muted-foreground);
    }

    dd {
      margin: 0;
      overflow-wrap: anywhere;
    }
  }

  /* Erasing a response sits under its answers, so the button is only
     reachable once the row has been read. Destructive on hover only, the
     restraint the invite Revoke button already sets: a red button per row
     would shout over the responses themselves. */
  .resp-actions {
    padding-block-end: var(--size-4);
  }

  .resp-delete {
    padding-block: var(--size-1);
    padding-inline: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--background);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      border-color: var(--destructive);
      color: var(--destructive);
    }
  }

  .resp-pages {
    display: flex;
    align-items: center;
    gap: var(--size-4);
    margin-block: var(--size-4) var(--size-8);
    font-size: var(--scale-00);
    color: var(--muted-foreground);

    a {
      color: var(--primary);
      text-decoration: none;

      &:hover {
        text-decoration: underline;
      }
    }
  }
}

/* Landing --------------------------------------------------------------- */

/* The three capability hues, and the only thing that sets --accent-hue. Applied
   to a row in the three-link list and again to the Free and Premium entries
   where that same capability comes back, so one concept is one colour every
   place it appears: edit on the builder, results on the numbers, share on
   everything that puts a form in front of someone. Entries belonging to no
   single capability set nothing and fall back to --primary.

   --accent-hue, NOT --accent. That name is already taken, by the quiet surface
   tint the whole app hovers with (zinc-200 in light), so setting it here both
   shadowed a system token inside every accented row and — because a defined
   token never triggers the fallback in var(--accent, var(--primary)) — painted
   every unaccented icon on the page zinc-200 instead of violet. */
.accent-edit {
  --accent-hue: var(--accent-edit);
}

.accent-results {
  --accent-hue: var(--accent-results);
}

.accent-share {
  --accent-hue: var(--accent-share);
}

/* Entry motion. Ten elements reveal: the three link rows, the six free cards,
   and the plan card as one unit. Nothing else — a reveal on the FAQ rows or the
   section headings would be movement that reveals no hierarchy and directs no
   attention, which is the test for whether it earns its place.

   Gated twice. The media query is this codebase's existing no-preference
   pattern, so reduced-motion gets the finished state immediately rather than a
   quicker movement. The js-reveal class is what keeps the page working with
   JavaScript off, which every other control here already does: the hidden state
   exists only once the script that will un-hide it has run, so a visitor with
   no JS gets the content instead of ten invisible boxes.

   On body, NOT on html, and that is not a style preference. The system theme is
   selected with html:not(.light):not(.dark), and in its first form was written
   html:not([class]), html[class=""] — so putting this class on the root element
   silently turned dark mode off for anyone on the system setting. The theme
   switcher also assigns document.documentElement.className outright, which
   would have wiped the class on the next toggle. The root element belongs to
   the theme; nothing else should write to it.

   transform and opacity only. 420ms sits inside the 300-450ms entry band, and
   --ease-out-quint is the closest of the easing tokens already defined to the
   decelerating curve this wants, so no new curve gets invented for it.

   No will-change. It is only supposed to be set while something is animating,
   and with the animation driven by a class that is added and never removed
   there is no clean moment to drop it again; ten elements moving on transform
   and opacity are already on the compositor without it. */
@media (prefers-reduced-motion: no-preference) {
  body.js-reveal .reveal-group > *,
  body.js-reveal .reveal {
    opacity: 0;
    /* A fixed 10px, not a percentage: these are cards of quite different
       heights, and a relative offset would give each a different distance to
       travel and so a different apparent speed. */
    transform: translateY(10px);
    transition:
      opacity 0.42s var(--ease-out-quint) var(--reveal-delay, 0ms),
      transform 0.42s var(--ease-out-quint) var(--reveal-delay, 0ms);
  }

  body.js-reveal .reveal-group.is-in > *,
  body.js-reveal .reveal.is-in {
    opacity: 1;
    transform: none;
  }

  /* Staggered as a group, not one card at a time. The observer watches the list
     rather than each entry, because on a phone the grid is a single column and
     per-entry observation would mean the sixth card sat at its 300ms delay
     after scrolling into view — a lag, where the delay is meant to be a stagger
     against siblings arriving at the same moment.

     By DOM order, which is visual order at every column count these grids take
     (three, two, one). 60ms a step; the longest sequence is the six free cards,
     so the last starts at 300ms, inside the 400ms a sequence is allowed. */
  body.js-reveal .reveal-group > *:nth-child(2) {
    --reveal-delay: 60ms;
  }

  body.js-reveal .reveal-group > *:nth-child(3) {
    --reveal-delay: 120ms;
  }

  body.js-reveal .reveal-group > *:nth-child(4) {
    --reveal-delay: 180ms;
  }

  body.js-reveal .reveal-group > *:nth-child(5) {
    --reveal-delay: 240ms;
  }

  body.js-reveal .reveal-group > *:nth-child(6) {
    --reveal-delay: 300ms;
  }
}

/* The landing page alone runs wider than the editor's measure, because the
   feature grids want three columns and 768px only fits two. Everything that
   should stay a reading column opts into --landing-measure, so the extra width
   reaches only the things asking for it: the two feature lists. */
.landing-main {
  --landing-measure: 34rem;

  /* The gap between top-level sections, and the only spacing on this page not
     taken from the shared scale. The scale tops out at values tuned for gaps
     inside a component, and every section here used to take --size-10 (40px)
     — the same gap the hero puts between its own paragraphs. Five sections at
     one interval is a list, not a page: nothing said where one idea stopped
     and the next began.

     Fluid, because a fixed 96px is right on a laptop and a sixth of a phone
     screen spent on nothing. Floors at 56px, which is still clearly more than
     any gap inside a section, and reaches its ceiling at roughly the page's
     own max width, so it stops growing exactly where the page does. */
  --landing-gap: clamp(3.5rem, 2rem + 6vw, 6rem);

  max-width: var(--width-lg);
}

.landing {
  max-inline-size: var(--landing-measure);
  margin-inline: auto;
  margin-block-start: var(--size-10);

  /* landing CTAs style their :focus away (inputs swap border colour), so
     keyboard focus gets an explicit ring rather than the UA default */
  :focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  /* Receipt for a deleted form, above the headline because it is the answer
     to the click that landed here. Reads as a completed action, not a
     warning: the destructive moment was the confirm, and this is what came
     of it. Same shield-on-the-first-line shape as .landing-hint. */
  .landing-deleted {
    display: flex;
    align-items: flex-start;
    gap: var(--size-2);
    margin-block-end: var(--size-6);
    padding: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--surface);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);

    /* 16px, matching the plan card's footing rather than the 14px that came
       from 1em of this notice's own type. The two are the same thing in two
       places — a shield on the first line of a small-print sentence — and an
       icon wants to sit a little above cap height to read at all. */
    .icon {
      --icon-size: var(--size-4);
      margin-block-start: 2px;
      color: var(--success);
      flex: none;
    }
  }

  h1 {
    font-family: var(--font-display);
    font-size: var(--scale-fluid-6);
    font-weight: var(--weight-bold);
    letter-spacing: var(--letter-sm);
    line-height: var(--line-xs);
    text-wrap: balance;
  }

  .landing-sub {
    margin-block: var(--size-4) var(--size-8);
    color: var(--muted-foreground);
    line-height: var(--line-md);
  }

  /* The boxed form: a label, a field, an error, a hint and a submit. The
     recover page is what this is for and the only page still using it — the
     landing hero borrowed it for a form with no fields at all, where the
     border was a box drawn around a single button and the hint inside it had
     to carry a badge to stop the box looking empty. That hero is .landing-cta
     below now, and this stayed behind with the form that has something in it. */
  .landing-create {
    display: flex;
    flex-direction: column;
    gap: var(--size-2);
    padding: var(--size-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);

    label {
      font-size: var(--scale-00);
      font-weight: var(--weight-medium);
    }

    .landing-optional {
      color: var(--muted-foreground);
      font-weight: var(--weight-regular);
    }

    input {
      padding-block: var(--size-2);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);
      font-family: inherit;
      font-size: var(--scale-0);

      &:focus {
        outline: none;
        border-color: var(--ring);
      }
    }

    .landing-error {
      color: var(--danger, #b91c1c);
      font-size: var(--scale-00);
    }

    /* The caveat under the field. The icon rule is kept for a caller that
       passes one; the hero, which is where the icon used to come from, no
       longer does. */
    .landing-hint {
      display: flex;
      align-items: flex-start;
      gap: var(--size-2);
      color: var(--muted-foreground);
      font-size: var(--scale-00);
      line-height: var(--line-md);

      .icon {
        margin-block-start: 2px;
        color: var(--success);
      }
    }

    /* Last in a stack of fields, so it sits on the left edge with a gap above
       it. Layout only: what it looks like is the shared rule below. */
    button {
      align-self: start;
      margin-block-start: var(--size-2);
    }
  }

  /* The hero's two actions on one line. Two forms rather than one, because
     they post to two different endpoints, so the row is a wrapper around both
     rather than anything either form owns. Wraps on a narrow screen, where the
     two stack and each takes the width it needs. */
  .landing-cta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--size-3);
  }

  /* One recipe for the page's primary action, on the hero's create button and
     on the recover form's submit. The two are the same button in two places,
     and drawing them from one rule is what keeps them that way. */
  .landing-create button,
  .landing-cta-go {
    padding-block: var(--size-3);
    padding-inline: var(--size-6);
    /* Transparent, not none. Neither button sets a height, so each one is as
       tall as its content plus its padding plus its border, and border-box
       does not enter into it: that only redistributes space when a height is
       given. A bordered secondary beside a border-none primary would therefore
       stand 2px taller. A border nobody can see costs nothing and makes the
       two boxes the same box. */
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background-color: var(--primary);
    color: var(--primary-foreground);
    font-size: var(--scale-1);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    transition:
      background-color 0.15s var(--ease-out-quad),
      transform 0.12s var(--ease-in-out-quad);

    &:hover {
      background-color: var(--primary-hover);
    }

    /* Nothing on this page acknowledged a press. 1px is the whole movement, on
       transform only, so the button beside it never shifts. */
    &:active {
      transform: translateY(1px);
    }
  }

  /* The demo, as the quieter half of the pair: same size and shape as the
     primary so the two sit level, but bordered rather than filled. It used to
     be a bare text link, which read as a footnote to the button above it
     instead of the alternative to it that it is.

     Same padding, radius and type size as the primary, so the pair sit level.
     Only weight and fill differ, which is the whole of the hierarchy between
     them. */
  .landing-cta-alt {
    padding-block: var(--size-3);
    padding-inline: var(--size-6);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);
    color: var(--surface-foreground);
    font-size: var(--scale-1);
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition:
      background-color 0.15s var(--ease-out-quad),
      color 0.15s var(--ease-out-quad),
      transform 0.12s var(--ease-in-out-quad);

    &:hover {
      background-color: var(--accent);
      color: var(--accent-foreground);
    }

    &:active {
      transform: translateY(1px);
    }
  }

  /* The caption under the pair: what the buttons cost you and what they hand
     back. Plain text, no icon and no border — it belongs to both buttons, so
     anything that framed it would attach it to one of them. */
  .landing-cta-note {
    margin-block-start: var(--size-4);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);
  }
}

/* Links panel ------------------------------------------------------------ */

/* Results and settings read as documents, not toolbars: their headers drop
   the divider AND the stickiness — a borderless sticky header has nothing
   to separate it from content scrolling beneath it, and a document's
   header belongs to the document, scrolling away with it. */
.editor-header-plain {
  position: static;
  border-block-end: none;
}

.links-panel {
  margin-block: var(--size-6) var(--size-2);
  padding: var(--size-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background-color: var(--surface);

  .links-head {
    display: flex;
    align-items: center;
    justify-content: space-between;

    h2 {
      font-size: var(--scale-1);
      font-weight: var(--weight-semibold);
    }
  }

  .links-dismiss {
    padding-block: var(--size-1);
    padding-inline: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--background);
    color: var(--foreground);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      background-color: var(--accent);
    }
  }

  .links-warning {
    margin-block: var(--size-2) var(--size-4);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);
  }

  .link-row {
    display: grid;
    grid-template-columns: minmax(10rem, 1fr) 2fr;
    gap: var(--size-3);
    align-items: center;
    padding-block: var(--size-3);
    border-block-start: 1px solid var(--border);

    .link-info {
      display: flex;
      flex-direction: column;
      gap: var(--size-1);

      strong {
        font-size: var(--scale-0);
      }

      span {
        color: var(--muted-foreground);
        font-size: var(--scale-00);
        line-height: var(--line-sm);
      }
    }
  }

  /* One level up from .link-row on purpose: the same copy control serves the
     capability links and the one-time-link rows further down the settings
     page, which are not .link-rows. */
  .link-copy {
    display: flex;
    gap: var(--size-2);

    input {
      flex: 1;
      min-inline-size: 0;
      padding-block: var(--size-1);
      padding-inline: var(--size-2);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--muted-foreground);
      font-family: var(--font-mono, monospace);
      font-size: var(--scale-00);
    }

    button {
      display: inline-flex;
      align-items: center;
      gap: var(--size-1);
      padding-block: var(--size-1);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);
      font-size: var(--scale-00);
      cursor: pointer;
      white-space: nowrap;

      .icon {
        --icon-size: 1em;
      }

      &:hover {
        background-color: var(--accent);
      }
    }
  }

  .links-email {
    margin-block-start: var(--size-3);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  @media (max-width: 640px) {
    .link-row {
      grid-template-columns: 1fr;
    }
  }
}

/* Plan section (inside the links panel): the editor's upgrade affordance
   and detach control. Patched over SSE by the attach/detach handlers. */
/* Settings panel rows: label + hint on the left, the control on the right,
   mirroring .link-row's grid so the two panels read as one family. */
.settings-panel {
  .settings-row {
    display: grid;
    grid-template-columns: minmax(10rem, 2fr) 1fr;
    gap: var(--size-3);
    align-items: center;
    padding-block: var(--size-3);
    border-block-start: 1px solid var(--border);

    &:first-of-type {
      border-block-start: none;
    }

    .settings-info {
      display: flex;
      flex-direction: column;
      gap: var(--size-1);

      /* .settings-title is the heading for a row whose control is a button.
         It cannot be a label: a button is a labelable element, so label[for]
         pointing at one forwards clicks to it, and a delete row's heading must
         not double as its trigger. Colour is restated because the span rule
         below matches it too and would otherwise mute it into a description. */
      label,
      .settings-title {
        font-weight: var(--weight-semibold);
        font-size: var(--scale-0);
        color: var(--foreground);
      }

      span {
        color: var(--muted-foreground);
        font-size: var(--scale-00);
      }
    }

    input,
    textarea {
      justify-self: end;
      max-inline-size: 16rem;
      padding-block: var(--size-1);
      padding-inline: var(--size-2);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);
      font-size: var(--scale-00);

      &:focus {
        outline: none;
        border-color: var(--ring);
      }
    }

    textarea {
      inline-size: 16rem;
      resize: vertical;
      font-family: inherit;
    }

    input[type='number'] {
      inline-size: 8rem;
    }

    /* Inline flow, not flex: these messages wrap (the invite-only warning
       runs to two lines), and flex centring would leave the icon beside the
       middle of the sentence instead of on its first line. Same reasoning as
       .analytics-caveat, including the margin — templ strips the whitespace
       between a component call and the text next to it, so the gap cannot
       come from the markup. */
    .settings-error {
      color: var(--destructive, #b91c1c);
      font-size: var(--scale-00);

      .icon {
        --icon-size: 1em;
        margin-inline-end: var(--size-1);
      }
    }

    /* The delete card's receipt ("Deleted 12 responses."). Muted, not red:
       it reports something that already went well, unlike .settings-error. */
    .settings-note {
      color: var(--muted-foreground);
      font-size: var(--scale-00);
    }

    .password-set {
      display: flex;
      gap: var(--size-2);
      justify-self: end;
    }

    .password-actions {
      justify-self: end;
    }

    .settings-btn {
      padding-block: var(--size-1);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);
      font-size: var(--scale-00);
      cursor: pointer;
      white-space: nowrap;

      &:hover {
        background-color: var(--accent);
      }
    }

    /* Label + count + button, sharing the control column of the mint row. */
    .invite-mint {
      display: flex;
      align-items: center;
      gap: var(--size-2);
      justify-self: end;

      input[type='text'] {
        flex: 1 1 9rem;
        min-inline-size: 5rem;
      }

      /* Narrower than the settings-wide number input: this one holds 1-2
         digits, not a response limit. */
      input[type='number'] {
        inline-size: 4.5rem;
      }
    }
  }
}

/* The invite section is an SSE patch target, so its rows sit one level
   deeper than the rest of the card's — which makes the first of them a
   :first-of-type and costs it the divider the settings rows share. Put it
   back, or the browser toggle above runs into the invite-only switch. */
#invite-section > .settings-row:first-of-type {
  border-block-start: 1px solid var(--border);
}

/* One-time fill links. The list sits inside a settings card, so it inherits
   the panel box and the .link-copy control; only the rows themselves and the
   footer tally need styling. */
.invite-list {
  list-style: none;
  /* The reset zeroes margins, not list padding. */
  padding-inline-start: 0;
  margin-block-start: var(--size-2);

  .invite-row {
    display: grid;
    grid-template-columns: minmax(8rem, 1fr) 2fr;
    gap: var(--size-3);
    align-items: center;
    padding-block: var(--size-2);
    border-block-start: 1px solid var(--border);
  }

  .invite-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-inline-size: 0;

    strong {
      font-size: var(--scale-00);
      /* Labels are free text: an email address must wrap rather than push
         the copy control off the row. */
      overflow-wrap: anywhere;
    }

    .invite-unlabelled {
      color: var(--muted-foreground);
      font-weight: var(--weight-normal);
    }

    .invite-status {
      color: var(--muted-foreground);
      font-size: var(--scale-00);
    }
  }

  /* Destructive, so it reads that way on hover only — a red row of Revoke
     buttons would shout over the links themselves. */
  .invite-revoke:hover {
    border-color: var(--destructive, #b91c1c);
    color: var(--destructive, #b91c1c);
  }

  @media (max-width: 640px) {
    .invite-row {
      grid-template-columns: 1fr;
    }
  }
}

.invite-empty,
.invite-foot {
  margin-block-start: var(--size-3);
  color: var(--muted-foreground);
  font-size: var(--scale-00);
}

.invite-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-3);
  flex-wrap: wrap;
}

/* Hidden field editor row: chip + param-name input; nothing renders on the
   public form, so the row must say so where the builder can see it. */
.block-hidden {
  .hidden-row {
    display: flex;
    align-items: center;
    gap: var(--size-2);
    flex-wrap: wrap;
  }

  .hidden-chip {
    padding-block: 2px;
    padding-inline: var(--size-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-round, 999px);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    white-space: nowrap;
  }

  .hidden-key {
    flex: 0 1 14rem;
    min-inline-size: 8rem;
    padding-block: var(--size-1);
    padding-inline: var(--size-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--background);
    color: var(--foreground);
    font-family: var(--font-mono, monospace);
    font-size: var(--scale-00);

    &:focus {
      outline: none;
      border-color: var(--ring);
    }
  }

  .hidden-hint {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }
}

/* The preview's stand-in for a hidden field (the fill page renders nothing). */
.hidden-note {
  padding-block: var(--size-1);
  color: var(--muted-foreground);
  font-size: var(--scale-00);

  code {
    font-family: var(--font-mono, monospace);
  }
}

/* Respondent country chip on results rows (flag emoji + ISO code). */
.resp-country {
  color: var(--muted-foreground);
  font-size: var(--scale-00);
  white-space: nowrap;
}

/* Inline server-side validation message under a field (submit backstop). */
.q-error {
  margin-block-start: var(--size-1);
  color: var(--destructive, #b91c1c);
  font-size: var(--scale-00);
}

/* Submit-side error banner (captcha failure re-render) and the paged
   form's captcha slot. */
.fill-error {
  padding-block: var(--size-2);
  padding-inline: var(--size-3);
  border: 1px solid var(--destructive, #b91c1c);
  border-radius: var(--radius-sm);
  color: var(--destructive, #b91c1c);
  font-size: var(--scale-00);
}

.fill-captcha {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  margin-block-start: var(--size-4);
}

.fill-submit .cf-turnstile {
  margin-inline-end: var(--size-3);
}

/* The widget renders in late (explicitly, on first visibility), so its box
   is reserved up front at Turnstile's fixed managed size — nothing below
   moves when the iframe arrives. The blocked-note renders inside the same
   reserved space. Flex, not block: Turnstile injects its iframe into a
   shadow root, which page selectors cannot reach, so the inline iframe's
   phantom baseline pixels can't be fixed on the iframe itself — but the
   host's layout mode governs shadow children, and flex items produce no
   line-box baseline space at all. */
.cf-turnstile {
  display: flex;
  align-items: center;
  min-inline-size: 300px;
  /* Measured, not documented: Cloudflare's docs say 65px, the widget
     actually renders 71px (Turnstile adds its own padding). Reserving the
     real height is what makes the late render shift nothing. */
  min-block-size: 71px;
}

/* The fill page's password gate and closed states. */
.fill-locked {
  .unlock-row {
    display: flex;
    gap: var(--size-2);
    margin-block-start: var(--size-4);

    input {
      flex: 1;
      min-inline-size: 0;
      padding-block: var(--size-2);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);

      &:focus {
        outline: none;
        border-color: var(--ring);
      }
    }

    button {
      padding-block: var(--size-2);
      padding-inline: var(--size-4);
      border: 1px solid var(--primary);
      border-radius: var(--radius-sm);
      background-color: var(--primary);
      color: var(--primary-foreground);
      cursor: pointer;
    }
  }

  .unlock-error {
    margin-block-start: var(--size-2);
    color: var(--destructive, #b91c1c);
    font-size: var(--scale-00);
  }
}

/* Form settings page: grouped cards. Each card carries the
   .links-panel/.settings-panel classes so the panel box and row styling
   apply wholesale — the page and the editor's links panel read as one
   family. */
.settings-page {
  > h1 {
    font-size: var(--scale-3);
    font-weight: var(--weight-bold);
  }

  .settings-card {
    margin-block: var(--size-5) 0;
    /* An anchor jump into a card (the Premium badges point at #plan-card)
       must clear the sticky editor header, or the heading lands under it. */
    scroll-margin-block-start: 5rem;

    h2 {
      display: flex;
      align-items: center;
      gap: var(--size-2);
      font-size: var(--scale-1);
      font-weight: var(--weight-semibold);
      margin-block-end: var(--size-1);

      .icon {
        --icon-size: 1em;
        color: var(--muted-foreground);
      }

      /* Plan-feature marker (the .hidden-chip pill shape in the brand
         violet), pushed to the card's far edge so premium cards scan at a
         glance. The primary-muted pair is theme-aware, so the tint holds in
         dark mode too. */
      .premium-badge {
        margin-inline-start: auto;
        display: inline-flex;
        align-items: center;
        gap: var(--size-1);
        padding-block: 2px;
        padding-inline: var(--size-2);
        border-radius: var(--radius-round, 999px);
        background-color: var(--primary-muted);
        color: var(--primary-muted-foreground);
        font-size: var(--scale-00);
        font-weight: var(--weight-normal);
        white-space: nowrap;
        /* It links to the plan card; no underline, the tint carries it. */
        text-decoration: none;

        &:hover {
          background-color: var(--primary);
          color: var(--primary-foreground);
        }

        /* The h2 mutes its icons; the badge's rocket rides the pill's own
           foreground instead. */
        .icon {
          color: inherit;
        }
      }
    }

    /* Inside its own card the plan section needs no divider chrome. */
    .plan-section {
      margin-block-start: 0;
      padding-block-start: 0;
      border-block-start: none;
    }
  }

  /* The delete card. Only its heading icon is tinted; the buttons go
     destructive on hover, matching the invite Revoke button. A last card
     rendered in solid red would read as a warning about the whole settings
     page, and the weight belongs in the copy, which says plainly that
     nothing here comes back. */
  .settings-card-danger {
    h2 .icon {
      color: var(--destructive);
    }

    .settings-btn-danger:hover:not(:disabled) {
      border-color: var(--destructive);
      color: var(--destructive);
    }

    /* Nothing to delete: the row still explains itself, the button just
       stops offering. */
    .settings-btn:disabled {
      color: var(--muted-foreground);
      cursor: default;

      &:hover {
        background-color: var(--background);
      }
    }
  }

  .settings-hint {
    margin-block-end: var(--size-2);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);

    code {
      font-family: var(--font-mono, monospace);
    }
  }
}

/* The links panel's pointer to everything that moved to the settings page. */
.links-more {
  margin-block-start: var(--size-3);
  color: var(--muted-foreground);
  font-size: var(--scale-00);

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

.plan-section {
  margin-block-start: var(--size-4);
  padding-block-start: var(--size-4);
  border-block-start: 1px solid var(--border);

  strong {
    font-size: var(--scale-0);
  }

  p {
    margin-block: var(--size-1) var(--size-3);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);
  }

  .plan-attach {
    display: flex;
    gap: var(--size-2);

    input {
      flex: 1;
      min-inline-size: 0;
      padding-block: var(--size-1);
      padding-inline: var(--size-2);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--foreground);
      font-family: var(--font-mono, monospace);
      font-size: var(--scale-00);

      &:focus {
        outline: none;
        border-color: var(--ring);
      }
    }
  }

  .plan-attach button,
  .plan-detach {
    padding-block: var(--size-1);
    padding-inline: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--background);
    color: var(--foreground);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      background-color: var(--accent);
    }
  }

  .plan-error {
    display: flex;
    align-items: center;
    gap: var(--size-1);
    color: var(--danger, #b91c1c);

    .icon {
      --icon-size: 1em;
    }
  }

  .plan-hint a {
    color: var(--primary);
  }
}

/* License (plan) page ---------------------------------------------------- */

.license {
  .license-head {
    display: flex;
    align-items: baseline;
    gap: var(--size-4);
    flex-wrap: wrap;

    h1 {
      font-size: var(--scale-3);
      font-weight: var(--weight-bold);
    }
  }

  .license-status {
    display: flex;
    align-items: center;
    gap: var(--size-3);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  .license-chip {
    padding-block: 2px;
    padding-inline: var(--size-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-round, 999px);
    font-size: var(--scale-00);
  }

  .license-chip-active {
    color: var(--primary);
    border-color: var(--primary);
  }

  .license-chip-lapsed,
  .license-chip-pending {
    color: var(--danger, #b91c1c);
    border-color: currentColor;
  }

  .license-warning {
    margin-block-start: var(--size-4);
    padding: var(--size-3) var(--size-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-md);
  }

  .license-actions {
    display: flex;
    gap: var(--size-2);
    margin-block: var(--size-4) var(--size-8);
  }

  h2 {
    font-size: var(--scale-1);
    font-weight: var(--weight-semibold);
    margin-block-end: var(--size-3);
  }

  .license-add {
    display: flex;
    gap: var(--size-2);
    flex-wrap: wrap;
    margin-block-end: var(--size-4);

    .license-attach {
      display: flex;
      flex: 1;
      gap: var(--size-2);
      min-inline-size: 16rem;

      input {
        flex: 1;
        min-inline-size: 0;
        padding-block: var(--size-1);
        padding-inline: var(--size-2);
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        background-color: var(--background);
        color: var(--foreground);
        font-family: var(--font-mono, monospace);
        font-size: var(--scale-00);

        &:focus {
          outline: none;
          border-color: var(--ring);
        }
      }
    }
  }

  .license-actions button,
  .license-add button {
    padding-block: var(--size-1);
    padding-inline: var(--size-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--background);
    color: var(--foreground);
    font-size: var(--scale-00);
    cursor: pointer;

    &:hover {
      background-color: var(--accent);
    }
  }

  .license-empty {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  .landing-error {
    margin-block-end: var(--size-3);
    color: var(--danger, #b91c1c);
    font-size: var(--scale-00);
  }

  .license-form {
    margin-block-end: var(--size-4);
    padding: var(--size-4) var(--size-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);

    .license-form-head {
      display: flex;
      /* start, not center: the title now stacks a meta line beneath it */
      align-items: flex-start;
      justify-content: space-between;
      margin-block-end: var(--size-2);

      .license-form-meta {
        color: var(--muted-foreground);
        font-size: var(--scale-00);
      }

      h3 {
        font-size: var(--scale-1);
        font-weight: var(--weight-semibold);
      }

      a {
        color: var(--foreground);
        text-decoration: none;

        &:hover {
          color: var(--primary);
        }
      }
    }

    .license-detach {
      padding-block: var(--size-1);
      padding-inline: var(--size-3);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      background-color: var(--background);
      color: var(--muted-foreground);
      font-size: var(--scale-00);
      cursor: pointer;

      &:hover {
        color: var(--danger, #b91c1c);
        background-color: var(--accent);
      }
    }

    /* Reuse the links panel's row styling wholesale. */
    .link-row {
      display: grid;
      grid-template-columns: minmax(8rem, 1fr) 2fr;
      gap: var(--size-3);
      align-items: center;
      padding-block: var(--size-2);
      border-block-start: 1px solid var(--border);

      .link-info {
        display: flex;
        flex-direction: column;

        strong {
          font-size: var(--scale-00);
        }

        span {
          color: var(--muted-foreground);
          font-size: var(--scale-00);
          line-height: var(--line-sm);
        }
      }

      .link-copy {
        display: flex;
        gap: var(--size-2);

        input {
          flex: 1;
          min-inline-size: 0;
          padding-block: var(--size-1);
          padding-inline: var(--size-2);
          border: 1px solid var(--border);
          border-radius: var(--radius-sm);
          background-color: var(--background);
          color: var(--muted-foreground);
          font-family: var(--font-mono, monospace);
          font-size: var(--scale-00);
        }

        button {
          display: inline-flex;
          align-items: center;
          gap: var(--size-1);
          padding-block: var(--size-1);
          padding-inline: var(--size-3);
          border: 1px solid var(--border);
          border-radius: var(--radius-sm);
          background-color: var(--background);
          color: var(--foreground);
          font-size: var(--scale-00);
          cursor: pointer;

          .icon {
            --icon-size: 1em;
          }

          &:hover {
            background-color: var(--accent);
          }
        }
      }
    }

    @media (max-width: 640px) {
      .link-row {
        grid-template-columns: 1fr;
      }
    }
  }
}

/* Landing pricing + recovery --------------------------------------------- */

/* The capability model spelled out link by link; also the page's crawlable
   substance, so it stays prose, not icons. */
.landing-links {
  margin-block-start: var(--size-10);

  /* No negative tracking. --letter-sm is a display treatment and this heading
     is 20px: at that size it takes about half a pixel out of every gap, which
     buys nothing and costs the display face a little of its fit. The hero h1,
     at 48px, keeps it. */
  h2 {
    font-family: var(--font-display);
    font-size: var(--scale-2);
    font-weight: var(--weight-semibold);
  }

  ul {
    margin-block-start: var(--size-4);
    color: var(--muted-foreground);
    line-height: var(--line-md);
    list-style: none;
    padding: 0;
  }

  /* rows mirror the editor's links panel — hairline rules, same row
     rhythm — so the trio is shown in the product's own vocabulary */
  li {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--size-2) var(--size-3);
    padding-block: var(--size-3);
    border-block-start: 1px solid var(--border);
  }

  /* The accent's own container, and the only place on this page that gets one.
     These rows carry no card, so the tint is what makes the icon read as a
     deliberate mark rather than a bullet — and this list is the page's one
     ownable idea, so it is where the treatment is worth spending. The feature
     grids below take their accent as icon colour alone: a tinted box inside an
     already-bordered card is the boxes-inside-a-box arrangement the free list
     and the plan card were each redrawn to get rid of.

     Fill and hairline are both mixed from the row's own --accent, so one rule
     serves all three hues and a fourth would need no new CSS. Ratios for every
     hue against its own tint are recorded with the tokens. */
  .badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    inline-size: var(--size-9);
    block-size: var(--size-9);
    border: 1px solid color-mix(in oklab, var(--accent-hue) var(--accent-line), transparent);
    border-radius: var(--radius-sm);
    background-color: color-mix(in oklab, var(--accent-hue) var(--accent-tint), transparent);
    color: var(--accent-hue);

    /* Matched to the feature grids rather than left at the 25px that fell out
       of 1.25em of --scale-2. Every icon that carries meaning on this page is
       now 20px; only the two footnote shields are smaller. */
    .icon {
      --icon-size: var(--size-5);
    }
  }

  .landing-link-chip {
    justify-self: end;
    padding-block: var(--size-1);
    padding-inline: var(--size-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background-color: var(--surface);
    color: var(--muted-foreground);
    font-family: var(--font-mono);
    font-size: var(--scale-00);
    white-space: nowrap;
  }

  strong {
    color: var(--foreground);
    font-weight: var(--weight-semibold);
  }

  @media (max-width: 640px) {
    li {
      grid-template-columns: auto 1fr;
    }

    .landing-link-chip {
      grid-column: 2;
      justify-self: start;
    }
  }
}

/* Free, Premium and the questions are three sections of one page, so they
   share a spine: a heading and a blurb on the reading column, with whatever
   sits underneath free to take the extra width. Stated once, because three
   copies of it is three chances for the three to drift out of line. */
.landing-free,
.landing-pricing,
.landing-faq {
  /* Three peers, so one interval between all of them. The three-link list
     above keeps the smaller --size-10 on purpose: it lives inside the hero
     column rather than beside these, and the difference between the two gaps
     is what says so. */
  margin-block-start: var(--landing-gap);

  /* These sit outside .landing so they can use the full page width, which also
     puts them outside that section's focus-ring rule. The buy button styles
     its own :focus away like the landing CTAs do, so the ring is restated here
     rather than inherited. */
  :focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  /* The heading and its blurb stay on the page's reading column while what
     follows takes the extra width. Run full-bleed they wrapped at roughly
     twice a comfortable measure and sat on a left edge the hero above never
     uses; held to --landing-measure they line up with everything else on the
     page, and the wider block beneath them reads as the one deliberate
     exception rather than as two competing layouts. */
  > h2,
  > .landing-sub {
    max-inline-size: var(--landing-measure);
    margin-inline: auto;
  }

  /* Tracking left alone at this size; see .landing-links h2 for why. */
  > h2 {
    font-family: var(--font-display);
    font-size: var(--scale-2);
    font-weight: var(--weight-semibold);
  }

  /* The blurb and what it introduces are two different things, not a caption
     and its subject, so the gap under the blurb is wider than the one above. */
  > .landing-sub {
    margin-block: var(--size-2) var(--size-8);
  }
}

/* The entry itself: icon, label, a line of explanation, laid out on an
   auto-fit grid rather than a fixed two or three, since custom domains is
   conditional and the plan's list is five entries or six depending on the
   deployment.

   Shared by the free list and the plan's, so an entry is drawn the same way
   in both and the two read as one inventory split across a line. What is not
   shared is how each list is contained: the plan's rows sit inside the plan's
   card, and the free list has to bring its own. Both below. */
.landing-features {
  display: grid;
  /* min(18rem, 100%), not a bare 18rem: a track floor wider than the container
     cannot be satisfied, so on a phone the grid would push past its parent and
     take the page's horizontal scroll with it. */
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: var(--size-4);
  margin: 0;
  padding: 0;
  list-style: none;

  li {
    display: flex;
    align-items: flex-start;
    gap: var(--size-3);
  }

  .icon {
    --icon-size: var(--size-5);

    /* onto the cap height of the label beside it, which sits a shade lower
       than the icon box's own top edge */
    margin-block-start: 2px;
    /* The entry's capability hue where it has one, and grey where it has none.
       No container: the card is already the box (see .landing-links .badge).

       Grey, not --primary, and this is the whole of what makes the accents
       legible. All twelve icons were --primary before, and colouring three of
       six in a grid while leaving the rest at full-strength violet put the
       loudest colour on the entries that mean the least: the muted accents read
       as faded versions of their neighbours rather than as a different kind of
       thing. Greyscale surround, few hues, is the arrangement that lets a
       colour carry information.

       It also finishes the rule the accent tokens are written around:
       --primary means "action", so it belongs on the two hero buttons and the
       buy button, and an icon is never an action. 10.02:1 on --surface in
       light, 5.66:1 in dark. */
    color: var(--accent-hue, var(--muted-foreground));
  }

  strong {
    display: block;
    font-weight: var(--weight-semibold);
  }

  p {
    margin-block-start: 2px;
    color: var(--muted-foreground);
    font-size: var(--scale-00);
    line-height: var(--line-lg);
  }
}

/* Out on the bare page the entries have nothing holding them, so what works
   as a row inside the plan's card reads as text floating in a grid: no edge
   says where one entry stops and the next starts, and the gaps between them
   look like accidents of how the sentences happened to wrap.

   A card each, not one card round the six. One card would put a bordered box
   under the plan's bordered box and turn the section into the two-plans
   comparison the stacked layout cannot support; six make the free tier an
   inventory of things you get, which is what it is.

   Cards stretch to the tallest in their row, unlike the rows they replace.
   That is the trade: the type list is the longest entry by some way and the
   two beside it grow to match. Even card heights are worth more here than the
   space they waste, and the alternative — align-items: start — buys the space
   back by leaving a row of cards with ragged bottoms, which looks like a bug
   where a tall card looks like a card. */
.landing-free .landing-features {
  /* Wider than the plan's, because a bordered card needs more air around it
     than a bare row does: at the row gap the borders of neighbouring cards
     read as one grid rule rather than as two separate edges. */
  gap: var(--size-6);

  li {
    padding: var(--size-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);
    color: var(--surface-foreground);
    /* One light source for the page's cards, stated per theme rather than
       derived from one: a 1px shadow in light, an inner top highlight in dark,
       where a shadow under a surface darker than its own border reads as
       nothing at all. See --card-lift. */
    box-shadow: var(--card-lift);
  }

  /* The 2px the plan's rows use is right for a row, where the label and its
     line of explanation are one compact unit and the gap to the next row is
     what does the separating. In a card with 20px of padding round it that
     same 2px reads as the two lines touching. */
  p {
    margin-block-start: var(--size-1);
  }
}

/* The upgrade pitch: same simple bones as the sections above (heading,
   features, button) but tinted with primary's quiet sibling, so it pops
   as "the paid tier" without becoming another bordered surface card. The
   muted pair guarantees contrast in both themes; the primary button pops
   against the tint. */
.landing-pricing {

  /* The whole plan is one surface now. There used to be a tinted band here
     with cards floating on it, which read as boxes inside a box: two nested
     surfaces where the content only needed one. With the tint gone the brand
     colour is free to mean "action" on the button and the feature icons
     rather than "region", and the card's three parts are separated by rules
     instead of by gaps between smaller cards. */
  .landing-plan {
    /* anchors the gradient ring below */
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--surface);
    color: var(--surface-foreground);
    box-shadow: var(--card-lift);
  }

  /* The one card on this page with a price on it, and until now the same weight
     as the six free cards above it — same hairline, same fill, same flatness.
     A 1px ring in the brand colour, strongest along the top edge and gone by
     just past halfway down, is the whole of the emphasis it gets.

     On a pseudo-element so the card keeps the plain hairline every other
     bordered thing here uses; the ring sits over that border rather than inside
     it, which is what inset -1px and the +1px radius are for. Masked to the
     ring itself: two identical mask layers, one clipped to the content box, the
     other to the border box, composited to exclude the first from the second,
     which leaves exactly the 1px frame.

     Static. This is depth, not a state, so nothing here transitions — and a
     box-shadow is not what is being changed on hover anywhere on this page. */
  .landing-plan::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: calc(var(--radius-lg) + 1px);
    padding: 1px;
    /* 32% and gone by 45%, not 55% and 60%. At the stronger setting the ring
       read as a solid violet border down two thirds of the card — a "this is
       the plan we want you to pick" highlight, which is both louder than this
       page argues and a claim the card is not making. What is wanted is that
       the top edge catches a little light. */
    background: linear-gradient(
      to bottom,
      color-mix(in oklab, var(--primary) 32%, transparent),
      transparent 45%
    );
    mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
    mask-clip: content-box, border-box;
    mask-composite: exclude;
    -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
    -webkit-mask-clip: content-box, border-box;
    -webkit-mask-composite: xor;
    pointer-events: none;
  }

  /* Pick, price, buy: one line, reading left to right in the order the
     decision is actually made. The toggle and the price it determines sit
     together at the left, since spread evenly the price floated in the middle
     of the card attached to neither the control that sets it nor the button
     that charges it. Only the button is pushed to the far edge. */
  .landing-plan-buy {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--size-4);
    padding: var(--size-5);

    button {
      margin-inline-start: auto;
    }

    /* Below roughly this width the three stop fitting on a line and wrap into
       a ragged stack: a wide toggle, a short price, a mid-width button, each
       ending somewhere different. Stacking them deliberately instead, with the
       toggle and button taking the full width, gives the block two clean edges.
       800 rather than the usual 640 because that is where the content actually
       runs out of room, and stacking a little early looks fine where stacking
       late looks broken. */
    @media (max-width: 800px) {
      .landing-plan-toggle,
      button {
        inline-size: 100%;
      }

      /* The track is stretched to the card now, so the two options divide it
         between them. Without this they keep hugging their own text and sit
         packed at the left with dead grey track to their right. */
      .landing-plan-toggle label {
        flex: 1;
      }

      button {
        margin-inline-start: 0;
      }
    }
  }

  /* Segmented control: one track, the checked option filled. The radios
     themselves are taken out of the paint but NOT out of the accessibility
     tree or the tab order. Clip, not display:none, because a hidden input
     cannot be focused and the toggle would stop working from the keyboard. */
  .landing-plan-toggle {
    /* anchors the clipped radios below. Without it they resolve against the
       initial containing block, i.e. the top of the document, and tabbing to
       one scrolls the page away from the control being operated. */
    position: relative;
    display: flex;
    gap: var(--size-1);
    /* the control is a choice between two things, not a banner: sized to fit
       its options rather than stretched across the card. No align-self, so it
       centres against the price and button on the same line. */
    max-inline-size: 100%;
    padding: var(--size-1);
    border: none;
    border-radius: var(--radius-full);
    background-color: var(--accent);

    input {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    /* Unselected options are muted; the selected one comes up to full
       strength. Text carries the state rather than fill alone, because the
       fill flips relative lightness between themes. --surface is lighter
       than the --accent track in light mode and darker in dark, so a reader
       cannot learn "the lighter one is chosen" and have it hold. Contrast
       means the same thing in both.

       Weight deliberately does NOT change with state: 500 and 600 set at
       different widths, so the segments would resize on every click and the
       control would jitter under the cursor. */
    label {
      display: inline-flex;
      /* No flex-grow here on purpose. The track is sized to its content at
         this width, so equal halves would squeeze the yearly option and drop
         its saving pill onto a second line; the stacked layout below, where
         the track is stretched to the card, turns growing back on. */
      /* A flex item will not shrink below its own min-content width unless
         told to, and this one's min-content includes a nowrap saving pill.
         Without these two the toggle pushes wider than the card on a phone
         and the whole thing overflows. Shrinking is allowed, and the pill
         drops under the word rather than off the edge. */
      flex-wrap: wrap;
      min-inline-size: 0;
      justify-content: center;
      align-items: center;
      gap: var(--size-2);
      padding-block: var(--size-2);
      padding-inline: var(--size-4);
      border-radius: var(--radius-full);
      color: var(--muted-foreground);
      font-size: var(--scale-00);
      font-weight: var(--weight-semibold);
      cursor: pointer;
      transition:
        background-color 0.15s var(--ease-out-quad),
        color 0.15s var(--ease-out-quad);
    }

    /* Hover walks an unselected option up to the contrast the selected one
       has, which is the thing this control uses to mean "chosen": the cursor
       previews the click rather than introducing a third colour that means
       nothing on its own.

       --accent-foreground rather than --foreground, because the label sits on
       the --accent track and that is the token paired with it. It resolves a
       step past --muted-foreground in both themes, darker on light and lighter
       on dark, so hover reads as "more" either way round.

       Only the unselected label. The checked one is already at full strength,
       and giving it a hover would promise a change that clicking it cannot
       deliver. Colour alone, no fill: a background here would compete with the
       fill that marks the actual selection. The transition on the base rule
       already covers colour, so this eases in with everything else. */
    input:not(:checked) + label:hover {
      color: var(--accent-foreground);
    }

    /* The label is the only visible part, so it has to wear the focus ring
       the clipped radio would otherwise show on its own box. */
    input:focus-visible + label {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
    }

    input:checked + label {
      background-color: var(--surface);
      color: var(--surface-foreground);
      /* lifts the selected option off the track in dark mode, where its fill
         alone sits below the track rather than above it */
      box-shadow: var(--shadow-xs);
    }
  }

  /* Both price lines are in the DOM; the checked radio picks which shows.
     Written as "hide the one not chosen" so that a browser without :has()
     falls back to showing both. Each is labelled with its own period, so that
     degrades to cluttered rather than to wrong. */
  .landing-plan:has(#interval-monthly:checked) [data-interval="yearly"],
  .landing-plan:has(#interval-yearly:checked) [data-interval="monthly"] {
    display: none;
  }

  .landing-plan-price {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--size-1);
  }

  .landing-plan-amount {
    font-family: var(--font-display);
    font-size: var(--scale-3);
    font-weight: var(--weight-bold);
    letter-spacing: var(--letter-sm);
  }

  .landing-plan-per {
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  /* A pill, following the license page's chip: same shape means the same
     thing across the app, a short piece of status attached to what it
     describes. Its own colour pair rather than the chip's --primary-muted,
     which goes darker than the card in dark mode (see --saving). */
  .landing-plan-save {
    padding-block: 2px;
    padding-inline: var(--size-2);
    border-radius: var(--radius-full);
    background-color: var(--saving);
    color: var(--saving-foreground);
    font-size: var(--scale-000);
    font-weight: var(--weight-semibold);
    white-space: nowrap;
  }

  /* Sized to its label, not to the card: a full-width button here would be a
     banner. The min-width keeps it weighty enough to balance the price. */
  .landing-plan button {
    min-inline-size: 12rem;
  }

  /* Inside the card the entries stay bare rows and take the card's padding and
     the hairline that separates them from the buy block above: they are one of
     its three parts, and the card is the thing already holding them. Giving
     them the free list's borders would be boxes inside a box, the arrangement
     this card was redrawn to get rid of. */
  .landing-features {
    padding: var(--size-5);
    border-block-start: 1px solid var(--border);
  }

  /* The card's footing: who charges, in a footnote's voice, ruled off from
     the features above so it reads as a note on the whole card rather than a
     seventh feature. The icon is pinned and the sentence wraps beside it. */
  .payment-trust {
    display: flex;
    align-items: flex-start;
    gap: var(--size-2);
    padding: var(--size-4) var(--size-5);
    border-block-start: 1px solid var(--border);
    color: var(--muted-foreground);
    font-size: var(--scale-00);

    /* 16px rather than the 17.5px that fell out of 1.25em of --scale-00, so
       this and the deleted notice above are the same size as each other and
       clearly a step under the 20px icons that carry a feature. */
    .icon {
      --icon-size: var(--size-4);
      color: var(--success);
      flex-shrink: 0;
      /* nudged onto the first line's centre; align-items handles the block,
         this handles the icon sitting slightly high against cap height */
      margin-block-start: 2px;
    }
  }

  button {
    padding-block: var(--size-3);
    padding-inline: var(--size-6);
    border: none;
    border-radius: var(--radius-md);
    background-color: var(--primary);
    color: var(--primary-foreground);
    font-size: var(--scale-1);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    transition:
      background-color 0.15s var(--ease-out-quad),
      transform 0.12s var(--ease-in-out-quad);

    &:hover {
      background-color: var(--primary-hover);
    }

    &:active {
      transform: translateY(1px);
    }
  }
}

/* The questions are prose, so unlike the two lists above them they keep the
   reading column all the way down. Spread to the page's full width a one-line
   question becomes a target you have to aim at, and the answer under it runs
   at twice a comfortable measure. */
.landing-faq {
  details {
    max-inline-size: var(--landing-measure);
    margin-inline: auto;
    border-block-end: 1px solid var(--border);
  }

  /* The first row needs the rule the others get from the question above them,
     so the stack is closed at both ends rather than only at the bottom. It
     also carries the gap under the heading: the two sections above get theirs
     from the bottom margin of a blurb, and this one has no blurb to ask a
     question under. Matched to that margin so all three headings sit the same
     distance above what they introduce. */
  details:first-of-type {
    margin-block-start: var(--size-8);
    border-block-start: 1px solid var(--border);
  }

  /* The whole row is the hit area: a question is a sentence, and asking
     someone to hit the four characters of a marker instead is a worse target
     on every device. */
  summary {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--size-4);
    padding-block: var(--size-4);
    font-weight: var(--weight-medium);
    line-height: var(--line-md);
    cursor: pointer;
    transition: color 0.15s var(--ease-in-out-quad);

    /* Seven full-width rows carried cursor: pointer and answered the cursor
       with nothing at all. Colour, not a fill: the row runs the full width of
       the reading column with no inset of its own, so a background would have
       to reach past the hairlines above and below it to look deliberate, and
       padding it inwards would indent the question away from the answer that
       is supposed to continue it. --primary is already what means "you can act
       on this" everywhere else here. The marker sets its own colour, so it has
       to be told to come along. */
    &:hover {
      color: var(--primary);

      &::after {
        color: inherit;
      }
    }

    /* list-style covers Firefox and Chrome; the -webkit- pseudo-element is
       what Safari still draws its own triangle through. */
    list-style: none;

    &::-webkit-details-marker {
      display: none;
    }

    /* A typed +/− rather than a rotating chevron, because the icon set has no
       chevron in it and a glyph needs no new asset to ship. The two are the
       same width in the mono face, so the question beside them does not shift
       by a hair when one is opened. */
    &::after {
      content: "+";
      flex: none;
      color: var(--muted-foreground);
      font-family: var(--font-mono);
      font-size: var(--scale-2);
      line-height: 1;
      transition: color 0.15s var(--ease-in-out-quad);
    }
  }

  details[open] summary::after {
    /* U+2212 minus, not a hyphen: at this size a hyphen reads as a speck. */
    content: "−";
  }

  /* Indented by nothing. The answer is the question's continuation, so it
     lines up with it; stepping it in would make the stack look like an
     outline of something rather than a list of pairs. */
  details p {
    margin-block-end: var(--size-4);
    color: var(--muted-foreground);
    line-height: var(--line-lg);
  }

  /* Underlined at rest, not on hover only. This is the page's one link sitting
     inside a paragraph of body text, and colour alone was carrying it: --primary
     against the answer's --muted-foreground measures 1.43:1 in light and 1.08:1
     in dark, where 3:1 is the floor for distinguishing a link from the text
     around it by colour. In dark mode it was effectively not a distinction.

     Thickness and offset stated rather than left to the UA, which at this size
     draws a heavier rule and sits it close enough to the baseline to cut the
     descenders of "privacy policy". Hover thickens rather than recolouring, so
     the change is on the thing that was already carrying the meaning. */
  details a {
    color: var(--primary);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
    transition: text-decoration-thickness 0.15s var(--ease-in-out-quad);

    &:hover {
      text-decoration-thickness: 2px;
    }
  }
}

/* Legal pages: quiet document typography, one prose column. */
.legal {
  max-inline-size: var(--prose-md);
  margin-inline: auto;

  h1 {
    font-family: var(--font-display);
    font-size: var(--scale-fluid-4);
    font-weight: var(--weight-bold);
    letter-spacing: var(--letter-sm);
  }

  .legal-updated {
    margin-block-start: var(--size-2);
    color: var(--muted-foreground);
    font-size: var(--scale-00);
  }

  h2 {
    margin-block-start: var(--size-8);
    font-family: var(--font-display);
    font-size: var(--scale-2);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--letter-sm);
  }

  p,
  ul {
    margin-block-start: var(--size-3);
    line-height: var(--line-lg);
  }

  ul {
    display: flex;
    flex-direction: column;
    gap: var(--size-2);
    padding-inline-start: var(--size-5);
    list-style: disc;
  }

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

/* Free-tier branding on public forms; the premium gate removes it. */
.fill-branding {
  margin-block-start: var(--size-8);
  text-align: center;
  font-size: var(--scale-00);

  a {
    color: var(--muted-foreground);
    text-decoration: none;

    &:hover {
      color: var(--primary);
    }
  }
}

/* Brand mark ------------------------------------------------------------- */
/* .logo-mark base sizing/colour lives in global.css with the site chrome. */

.fill-branding a {
  display: inline-flex;
  align-items: center;
  gap: 0.5ch;
}
