/* Static poll page styles — shipped verbatim in every bundle. Mobile-first, light. */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap');

:root {
  /* Defaults aligned to the icomm "plataforma" design system. A poll's own design overrides
     --accent / --bg / font-family at runtime (see render.ts designStyle). */
  --bg: #f6f8fc;          /* icomm gray-50 */
  --card: #ffffff;
  --text: #1f2836;        /* icomm gray-800 */
  --muted: #6b7889;       /* icomm gray-500 */
  --accent: #45c9ec;      /* icomm cyan-400 */
  --accent-weak: #e8fafe; /* icomm cyan-50 — selected-option tint */
  --border: #e2e8f1;      /* icomm gray-200 */
  --ok: #1f9d57;
  --danger: #d24343;
  --warn: #b5871a;
  --warn-weak: #fbf1cc;   /* icomm warning-bg */
  --radius: 12px;
}
* { box-sizing: border-box; }
/* A button, an input or a select takes the browser's own UI font unless it is told otherwise — which left
   the author's typeface on the questions and off every control under them. The design is the page's font;
   this is what makes that true of all of it. */
button, input, select, textarea { font-family: inherit; }
/* The runtime hides things by setting `element.hidden`, and any class of ours that sets `display` beats
   the browser's own `[hidden]` rule — which is how the poll-wide terms ended up under every question
   instead of only the first. Settled once here, rather than remembering a `[hidden]` rule beside every
   new `display` one. */
[hidden] { display: none !important; }
html, body { margin: 0; }
body {
  font-family: 'Roboto', system-ui, -apple-system, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  padding: 1.25rem;
}
#poll-root { max-width: 640px; margin: 0 auto; }

/* One question at a time (body.poll-single, set in render.ts): the step is centered in the viewport
   instead of pinned to the top. `margin:auto` rather than `align-items:center` so a step taller than
   the screen still scrolls from its top edge instead of being clipped. */
body.poll-single { display: flex; min-height: 100vh; min-height: 100dvh; }
body.poll-single #poll-root { width: 100%; margin: auto; }
body.poll-single .poll-code-gate, body.poll-single .poll-loading { margin-top: 0; }

/* With a background photo (body.poll-bg-image) everything the respondent reads sits on one panel — a
   fully transparent title, progress bar and nav floating over a photo are unreadable. The panel is then
   the only surface, so the step card itself goes flush (no card-inside-a-card). Solid by default… */
body.poll-bg-image #poll-root {
  background: var(--card);
  border-radius: 16px;
  padding: 1.4rem 1.3rem;
  box-shadow: 0 14px 40px rgb(15 23 42 / 30%);
}
body.poll-bg-image .poll-block,
body.poll-bg-image .poll-closed,
body.poll-bg-image .poll-done {
  background: transparent; border: none; padding: 0; margin-bottom: 0;
}
body.poll-bg-image .poll-closed, body.poll-bg-image .poll-done { padding: 0.75rem 0; }
/* `margin: 0 auto`, not `0`: the loader is narrower (22rem) than the panel it now sits in, so
   dropping the horizontal `auto` would leave the spinner hugging the panel's left edge. */
body.poll-bg-image .poll-loading { margin: 0 auto; padding: 1rem 0; }
/* The card's own margin used to separate it from the title; flush inside the panel it needs this. */
body.poll-bg-image .poll-head { margin-bottom: 0.9rem; }
/* Sticking the banner to the viewport top would pull it out of the panel it's painted on. */
body.poll-bg-image .poll-preview-banner { position: static; }

/* …unless the author dialled that panel's opacity down (body.poll-bg-translucent + the --panel-* vars
   from render.ts): then it frosts, so the photo reads as part of the design instead of a strip of
   wallpaper around a solid box. The controls painted on the glass take their own, less transparent tint
   (--panel-inner-alpha) so a choice still looks tappable and the step stays ONE layer rather than opaque
   boxes stacked on a photo; `:not(.selected)` leaves the accent fill of a chosen answer alone.
   color-mix keeps --card in charge of the color; a browser without it just keeps the opaque panel
   declared above, which is exactly the pre-feature look. */
body.poll-bg-translucent {
  /* Secondary text (descriptions, hints, field labels) is tuned for a solid surface; against a photo
     showing through it needs more weight, so the token itself is darkened for the whole page. */
  --muted: #46536a;
}
body.poll-bg-translucent #poll-root {
  background: color-mix(in srgb, var(--card) var(--panel-alpha), transparent);
  border: 1px solid rgb(255 255 255 / 45%);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(115%);
  backdrop-filter: blur(var(--panel-blur)) saturate(115%);
}
body.poll-bg-translucent .poll-option:not(.selected),
body.poll-bg-translucent .poll-rank-item,
body.poll-bg-translucent .poll-rank-move,
body.poll-bg-translucent .poll-scale-btn:not(.selected),
body.poll-bg-translucent .poll-kbd,
body.poll-bg-translucent .poll-input {
  background: color-mix(in srgb, var(--card) var(--panel-inner-alpha), transparent);
}

.poll-head h1 { font-size: 1.6rem; margin: 0 0 0.25rem; }
.poll-desc { color: var(--muted); margin: 0 0 1rem; }

/* Preview mode: a non-saving review of the poll (opened with ?preview). Pinned so it stays visible. */
.poll-preview-banner {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  margin: 0 0 1rem; padding: 0.6rem 0.85rem;
  background: var(--warn-weak); border: 1px solid var(--warn); border-radius: 10px;
  font-size: 0.9rem;
}
.poll-preview-badge {
  flex: none; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.7rem;
  padding: 0.15rem 0.5rem; border-radius: 999px; background: var(--warn); color: #fff;
}
.poll-preview-note { color: var(--text); }

.poll-block {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
  margin-bottom: 1rem;
}
.poll-statement { background: transparent; border: none; padding: 0.5rem 0; }
.poll-q { font-size: 1.08rem; margin: 0 0 0.6rem; }
.poll-hint { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.75rem; }
.req { color: var(--danger); }
/* Formatting the author applied to a title or description (see sanitizeRichText for the whole list — bold,
   italic, underline, strikethrough and links, and nothing else). Only the link needs saying: left to the
   browser it would be the one blue thing on a page whose colour the author chose. */
.poll-q-text a, .poll-hint a { color: var(--accent); text-decoration: underline; }

/* Choices are full-width buttons (large tap targets), not native radios/checkboxes. The native
   control is kept visually hidden for keyboard + screen-reader support; the whole row is tappable. */
.poll-options { display: flex; flex-direction: column; gap: 0.6rem; }
.poll-option {
  display: flex; align-items: center; gap: 0.75rem;
  min-height: 3rem; padding: 0.85rem 1rem;
  border: 1.5px solid var(--border); border-radius: 12px;
  background: var(--card); color: var(--text); cursor: pointer;
  font-size: 1rem; line-height: 1.35;
  transition: border-color 0.12s ease, background-color 0.12s ease;
  -webkit-tap-highlight-color: transparent;
}
.poll-option:hover { border-color: var(--accent); }
.poll-option:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.poll-option.selected { border-color: var(--accent); background: var(--accent-weak); font-weight: 600; }
.poll-option-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.poll-option-label { flex: 1 1 auto; }
.poll-option-mark { flex: none; width: 1.5rem; text-align: center; color: var(--accent); font-weight: 700; opacity: 0; }
.poll-option-mark::before { content: '✓'; }
.poll-option.selected .poll-option-mark { opacity: 1; }

/* Image-choice grid: auto-enabled when any option in the question has an image. Options without one
   get a blank placeholder tile so the grid stays uniform; the label sits in a bottom caption overlay. */
.poll-options.poll-options--grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.75rem;
}
.poll-option--image {
  flex-direction: column; align-items: stretch; padding: 0; min-height: 0; overflow: hidden; position: relative;
}
.poll-option-image { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; background: var(--border); }
.poll-option-image--empty { background: var(--border); }
.poll-option-caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0));
  color: #fff;
}
.poll-option--image .poll-option-label { font-weight: 600; font-size: 0.92rem; }
.poll-option--image .poll-option-mark { color: #fff; }
.poll-option--image.selected {
  border-color: var(--accent); background: var(--card); box-shadow: 0 0 0 2px var(--accent) inset;
}

/* Ranking: sortable list (drag or ▲▼) */
.poll-ranking { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.poll-rank-item {
  position: relative; /* anchors the drop line drawn in the gap next to this item */
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 3rem; padding: 0.55rem 0.7rem; background: var(--card);
  border: 1.5px solid var(--border); border-radius: 12px;
  -webkit-tap-highlight-color: transparent;
}
.poll-rank-item.dragging { opacity: 0.45; }
/* Where the dragged option will land: a line centred in the 0.5rem gap above (or below the last item),
   rather than a highlight on the item it displaces — the gap is the position the drop actually produces. */
.poll-rank-item.drop-before::before,
.poll-rank-item.drop-after::after {
  content: ''; position: absolute; left: 0; right: 0; height: 3px;
  background: var(--accent); border-radius: 999px; pointer-events: none;
}
.poll-rank-item.drop-before::before { top: -0.25rem; margin-top: -1.5px; }
.poll-rank-item.drop-after::after { bottom: -0.25rem; margin-bottom: -1.5px; }
.poll-rank-handle { cursor: grab; color: var(--muted); font-size: 1.1rem; line-height: 1; touch-action: none; }
.poll-rank-pos {
  flex: none; width: 1.6rem; height: 1.6rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-weak); color: var(--accent); font-size: 0.85rem; font-weight: 600;
}
.poll-rank-image { flex: none; width: 2.5rem; height: 2.5rem; border-radius: 8px; object-fit: cover; }
.poll-rank-label { flex: 1; }
.poll-rank-moves { flex: none; display: flex; gap: 0.25rem; }
.poll-rank-move {
  width: 2.75rem; height: 2.75rem; border: 1px solid var(--border); background: var(--card);
  border-radius: 8px; cursor: pointer; color: var(--muted); font-size: 0.9rem; line-height: 1;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-rank-move:hover { border-color: var(--accent); color: var(--accent); }
.poll-rank-move:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.poll-input {
  width: 100%; min-height: 3rem; padding: 0.7rem 0.85rem;
  border: 1.5px solid var(--border); border-radius: 10px; font-size: 1rem; font-family: inherit;
}
/* 1rem (16px) keeps iOS Safari from zooming the page when the field is focused. */
.poll-input:focus { outline: 2px solid var(--accent); border-color: var(--accent); }
textarea.poll-input { min-height: 6rem; }

/* A date / datetime / time field. The text box is an ordinary .poll-input — its contents are written and
   read in the SURVEY's language by temporal.ts, not in the respondent's browser locale — and the button
   beside it opens the browser's own picker. */
.poll-temporal { position: relative; display: block; }
.poll-temporal .poll-input { width: 100%; padding-right: 2.6rem; }

.poll-temporal-btn {
  position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 2rem; height: 2rem; padding: 0;
  border: none; border-radius: 8px; background: none; cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-temporal-btn::before {
  content: ''; width: 1.15rem; height: 1.15rem;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M8 3v4M16 3v4M3 10h18'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M8 3v4M16 3v4M3 10h18'/%3E%3C/svg%3E") center / contain no-repeat;
}
.poll-temporal-time .poll-temporal-btn::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
}
.poll-temporal-btn:hover { background: var(--accent-weak); }
.poll-temporal-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* An invalid field turns red end to end — the glyph included, or it reads as the one part still fine. */
.poll-input-invalid ~ .poll-temporal-btn::before { background: var(--danger); }

/* A range is two of those boxes, still one field: one caption above them, one error line below. The two
   keep their own pickers because a native picker only ever chooses one moment. */
.poll-temporal-range { display: flex; align-items: center; gap: 0.5rem; }
.poll-temporal-range .poll-temporal { flex: 1 1 0; min-width: 0; }
.poll-temporal-sep { flex: none; color: var(--muted); }
/* Two date boxes side by side stop being readable well before the phone runs out of screen, so they
   stack — with the dash still between them, which is what says the two are one answer. */
@media (max-width: 26rem) {
  .poll-temporal-range { flex-direction: column; align-items: stretch; }
  .poll-temporal-sep { text-align: center; }
}

/* The input the picker actually belongs to. showPicker() refuses on an element that is not being rendered,
   so this is made invisible WITHOUT being removed: no ink, and out of the pointer's way. It is also
   aria-hidden and untabbable, so the text box over it stays the one field on the page.

   It covers the WHOLE field rather than hiding in a corner, because the browser anchors the popup to this
   element's box: parked at the right edge it opened out to the right of the field, away from what was
   clicked. Spanning the field, the calendar drops directly underneath it, aligned to its left edge. */
.poll-temporal-native {
  position: absolute; inset: 0;
  width: 100%; height: 100%; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}

.poll-scale { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.poll-scale-btn {
  min-width: 2.75rem; height: 2.75rem; padding: 0 0.5rem;
  border: 1.5px solid var(--border); background: var(--card); border-radius: 10px;
  font-size: 1.1rem; cursor: pointer; color: var(--muted);
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-scale-btn:hover { border-color: var(--accent); }
/* The rating ramp (`Field.colorByRating`, stars only) fills each star with the color of the value under the
   pointer — red at the low end, green at the high end. That color depends on the value, so renderScale sets
   it inline per button; all the stylesheet owes it is a smooth handover as the pointer crosses the scale. */
.poll-scale-ramped .poll-scale-btn { transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; }
.poll-scale-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.poll-scale-btn.selected { background: var(--accent); border-color: var(--accent); color: white; }
/* Emoji style: bigger glyph (a face at label size is unreadable), and an explicit emoji font stack so the
   poll's own font-family can't knock the faces back to a monochrome fallback. */
.poll-scale-emoji {
  font-size: 1.5rem; line-height: 1;
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
/* A face keeps its own colors, so the solid accent fill used for numbers/stars only muddies it — the
   selected face reads as the accent border plus a tint instead. */
.poll-scale-emoji.selected { background: var(--accent-weak); color: var(--text); }
/* "No aplica": a word, not a glyph, so it sizes to its text and carries a dashed edge and a gap before it
   — it must read as the way OUT of the scale, not as one more point on it. */
.poll-scale-na {
  width: auto; min-width: 0; padding: 0 0.9rem;
  margin-left: 0.5rem; border-style: dashed;
  font-size: 0.9rem; white-space: nowrap;
}

.poll-group { display: flex; flex-direction: column; gap: 0.75rem; }
.poll-field { display: flex; flex-direction: column; gap: 0.3rem; }
.poll-field-label { font-size: 0.88rem; color: var(--muted); }

/* A yes/no field (checkbox / toggle): one line, the control first and its wording beside it — where every
   other field keeps its caption above the input. The wording is the question itself here, so it is read at
   full size and in the body colour rather than as a small grey caption. */
.poll-field-bool { flex-direction: row; align-items: center; gap: 0.6rem; cursor: pointer; }
.poll-field-bool .poll-field-label { font-size: 1rem; color: var(--text); }
.poll-check {
  flex: none; width: 1.15rem; height: 1.15rem; margin: 0;
  accent-color: var(--accent); cursor: pointer;
}
.poll-check:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The same answer as .poll-check, drawn as a switch. `appearance: none` hands us the box and everything
   after it is paint on that ONE input — no wrapper, no second element — so the control the respondent
   flips is still the checkbox the keyboard reaches and the runtime reads.

   Proportions and colours are the platform's own switch (the one the builder marks a field obligatorio
   with): a 2:1 track, and OFF is a grey knob you can see against the track rather than a tint of the
   border — a pill with nothing in it does not read as something to flip. */
.poll-switch {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: none; width: 2.5rem; height: 1.25rem; margin: 0;
  border: 1px solid rgb(31 40 54 / 25%); border-radius: 999px; background: var(--card);
  cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease;
}
.poll-switch::after {
  content: ''; position: absolute; top: 50%; left: 0.125rem;
  width: 1rem; height: 1rem; transform: translateY(-50%);
  border-radius: 50%; background: rgb(31 40 54 / 25%);
  transition: left 0.15s ease, background 0.15s ease;
}
.poll-switch:checked { background: var(--accent); border-color: var(--accent); }
.poll-switch:checked::after { left: calc(100% - 1.125rem); background: #fff; }
.poll-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .poll-switch, .poll-switch::after { transition: none; } }

/* Form validation: the offending input is outlined and its reason sits directly under it. */
.poll-field-wrap { display: flex; flex-direction: column; gap: 0.25rem; }
.poll-field-error { color: var(--danger); font-size: 0.82rem; margin: 0; }
.poll-field-error[hidden] { display: none; }
.poll-input-invalid { border-color: var(--danger); }
.poll-input-invalid:focus { outline-color: var(--danger); border-color: var(--danger); }

/* Matrix / Likert grid — a real table of selectable cells at every width. On phones it stays a grid
   (never stacked): a wide scale scrolls horizontally with the sub-question column pinned, and each
   cell becomes a full-height tap target so the whole box is hittable, not just the small radio. */
.poll-matrix-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.poll-matrix { width: 100%; border-collapse: collapse; }
.poll-matrix-colhead {
  font-size: 0.8rem; font-weight: 600; color: var(--muted); text-align: center;
  padding: 0.35rem 0.3rem; vertical-align: bottom;
}
.poll-matrix-rowlabel { text-align: left; font-weight: 500; font-size: 0.95rem; padding: 0.5rem 0.7rem 0.5rem 0; }
.poll-matrix-cellwrap { text-align: center; padding: 0.3rem 0.2rem; }
.poll-matrix-row { border-top: 1px solid var(--border); }
.poll-matrix-cell {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.55rem;
  cursor: pointer; padding: 0.35rem; border-radius: 10px;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-matrix-mark {
  width: 22px; height: 22px; flex-shrink: 0; border: 2px solid var(--border); border-radius: 50%;
  background: var(--card); transition: border-color 0.1s, box-shadow 0.1s;
}
.poll-matrix-cell:hover .poll-matrix-mark { border-color: var(--accent); }
.poll-matrix-cell.selected .poll-matrix-mark { border-color: var(--accent); box-shadow: inset 0 0 0 5px var(--accent); }
.poll-option-input:focus-visible + .poll-matrix-mark { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The per-option column label is redundant on desktop (it's in the header) — shown only when stacked. */
.poll-matrix-collabel { display: none; }

@media (max-width: 560px) {
  /* Pin the sub-question column so it stays visible while a wide scale scrolls. */
  .poll-matrix-corner, .poll-matrix-rowlabel {
    position: sticky; left: 0; z-index: 1; background: var(--card); box-shadow: 1px 0 0 var(--border);
  }
  .poll-matrix-rowlabel { min-width: 6.5rem; max-width: 42vw; padding: 0.5rem 0.65rem 0.5rem 0; font-size: 0.9rem; }
  .poll-matrix-colhead { min-width: 2.9rem; padding: 0.3rem 0.2rem; }
  .poll-matrix-cellwrap { min-width: 2.9rem; padding: 0.12rem; }
  /* The whole cell is the tap target (not just the 22px dot), with a tint on the chosen column. */
  .poll-matrix-cell { display: flex; width: 100%; min-height: 2.9rem; padding: 0.4rem 0.2rem; border-radius: 8px; gap: 0; }
  .poll-matrix-cell.selected { background: var(--accent-weak); }
  .poll-matrix-mark { width: 24px; height: 24px; }
}

/* The wait before the first question exists: the page has painted, but the response session is still
   being opened (see runtime/index.ts). Baked into #poll-root by render.ts rather than drawn by the
   runtime, so it is already on screen while runtime.js itself is still being fetched; every render path
   clears #poll-root, so nothing has to take it back down. */
.poll-loading {
  display: flex; flex-direction: column; align-items: center; gap: 0.9rem;
  max-width: 22rem; margin: 2rem auto 0; padding: 2rem 1.5rem;
  text-align: center; color: var(--muted);
}
.poll-loading-text { margin: 0; font-size: 0.95rem; }

/* A ring with a gap in it, rotating. Same two colors as the progress bar — the poll's own accent
   travelling over the hairline grey — so the wait looks like part of the author's design rather than a
   browser default. Only `transform` animates, so it costs nothing to leave on screen. */
.poll-spinner {
  flex: none; width: 28px; height: 28px;
  border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%;
  animation: poll-spin 0.7s linear infinite;
}
@keyframes poll-spin { to { transform: rotate(360deg); } }
/* Asked for less motion: nothing travels, but the ring still has to say it is alive — a frozen ring
   says the opposite of what it is there for. */
@media (prefers-reduced-motion: reduce) {
  .poll-spinner { animation: poll-spin-fade 1.4s ease-in-out infinite; }
}
@keyframes poll-spin-fade { 50% { opacity: 0.3; } }

.poll-submit.poll-busy:disabled { display: inline-flex; align-items: center; justify-content: center; gap: 0.5em; opacity: 1; }
.poll-submit.poll-busy .poll-spinner {
  width: 1.05em; height: 1.05em; border-width: 2px;
  border-color: rgb(255 255 255 / 45%); border-top-color: #fff;
}

/* Access-code gate — shown before a code-gated poll starts */
.poll-code-gate { display: flex; flex-direction: column; gap: 0.85rem; max-width: 22rem; margin: 2rem auto 0; text-align: center; }
.poll-code-title { font-size: 1.4rem; margin: 0; }
.poll-code-prompt { color: var(--muted); margin: 0; }
.poll-code-input { text-align: center; letter-spacing: 0.05em; }
.poll-code-error { color: var(--danger); font-size: 0.9rem; margin: 0; }

/* Terms & conditions link + optional acceptance checkbox, shown just before submit */
.poll-terms { margin: 0 0 0.75rem; font-size: 0.9rem; color: var(--muted); }
.poll-terms-link { color: var(--accent); text-decoration: underline; }
.poll-consent {
  display: flex; align-items: flex-start; gap: 0.6rem; margin: 0 0 0.85rem;
  padding: 0.75rem 0.9rem; border: 1.5px solid var(--border); border-radius: 10px;
  cursor: pointer; font-size: 0.92rem; line-height: 1.45;
}
.poll-consent:hover { border-color: var(--accent); }
.poll-consent-input { flex: none; width: 1.15rem; height: 1.15rem; margin-top: 0.1rem; accent-color: var(--accent); cursor: pointer; }
.poll-consent-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.poll-consent.poll-consent-unmet { border-color: var(--danger); background: #fdecec; }

/* Terms & conditions QUESTION (block type `terms`) — distinct from the poll-wide link above: the document
   is part of the question, in a box of its own that scrolls. That box is the thing the "read it to the
   end" gate watches, for the pasted text and the linked page alike (see renderTerms). */
.poll-terms-doc {
  max-height: 22rem; overflow-y: auto; -webkit-overflow-scrolling: touch;
  margin: 0.6rem 0 0; border: 1.5px solid var(--border); border-radius: 10px; background: var(--card);
}
.poll-terms-doc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Author-pasted wording keeps its line breaks; the padding is on the text only — a frame is flush to the
   edges of its box, or it looks like a picture of a page rather than the page. */
.poll-terms-text { padding: 0.9rem 1rem; white-space: pre-line; font-size: 0.92rem; line-height: 1.55; }
/* The end of the document, as the last thing inside the scrolling box. Invisible, and 1px rather than 0 so
   it has a box for an IntersectionObserver to see arrive (see renderTerms). */
.poll-terms-end { height: 1px; }
/* The frame's HEIGHT is set inline, from the author's "alto de la página" (or measured, for a page served
   from this same origin): that height is what the box above scrolls through, so it cannot live here. */
.poll-terms-frame { display: block; width: 100%; border: 0; }
/* While there is still reading to do the box says so at its edge, and drops the mark on the way out. */
.poll-terms-doc.poll-terms-unread { border-color: var(--warn); }
.poll-terms-doc.poll-terms-read { border-color: var(--border); }
.poll-terms-source { margin: 0.4rem 0 0; font-size: 0.85rem; }
.poll-terms-tip { margin: 0.4rem 0 0; font-size: 0.85rem; color: var(--warn); }
.poll-terms-tip[hidden] { display: none; }
.poll-terms-accept { margin-top: 0.75rem; }

.poll-actions { margin-top: 0.5rem; }
.poll-submit {
  width: 100%; min-height: 3rem; padding: 0.85rem; border: none; border-radius: 10px;
  background: var(--accent); color: white; font-size: 1.02rem; cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-submit:hover { filter: brightness(1.05); }
.poll-submit:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.poll-submit:disabled { opacity: 0.5; cursor: default; }

.poll-status { text-align: center; color: var(--muted); font-size: 0.85rem; min-height: 1.2rem; margin-top: 0.6rem; }
.poll-status.error { color: var(--danger); }

.poll-closed, .poll-done {
  text-align: center; background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 2rem 1.5rem;
}
.poll-closing-title { margin: 0 0 0.6rem; }
.poll-closing-body { margin: 0.9rem 0 0; color: var(--muted); white-space: pre-line; }
/* Content-screen media (image or embedded video), centered under the title — welcome and closing alike */
.poll-screen-media { margin: 1rem auto 0; max-width: 480px; }
.poll-screen-image { display: block; width: 100%; height: auto; border-radius: var(--radius); }
.poll-screen-video { position: relative; width: 100%; padding-top: 56.25%; /* 16:9 */ }
.poll-screen-video-frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: 0; border-radius: var(--radius);
}
.poll-screen-video-link { color: var(--accent); text-decoration: underline; }
.poll-preview-end { margin: 1.1rem 0 0; }

/* ---- Moving between questions (one at a time) --------------------------------------------------
   The author picks three things in Diseño — which way the questions travel, the effect that travel
   plays, and how fast — and they arrive here as classes on the stage (`poll-axis-*`, `poll-anim-*`)
   plus one duration. Everything below is built from TWO keyframes that read their start/end state off
   custom properties, so each effect is just a set of values: that is what lets every one of them
   mirror itself on Back and follow either axis without a keyframe of its own. */
.poll-stage {
  position: relative;
  /* A question arriving from off its own edge must not paint over the heading and the nav on the way. */
  overflow: hidden;
  --poll-anim-dur: 380ms;
  /* 1 while advancing, -1 while going Back — every effect below is mirrored through this one number,
     so retreating looks like retreating instead of a second forward step. Set by the stepper. */
  --poll-anim-dir: 1;
  --poll-anim-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* Only ever one question is on screen here, so the card's own bottom margin — which separates stacked
   questions in the all-at-once mode — would just add a second gap above the nav. */
.poll-stage > .poll-block { margin-bottom: 0; }
/* The question being left is lifted out of the flow so the arriving one takes its place immediately:
   that overlap is the whole difference between two animations playing together and one queued behind
   the other. It is inert while it leaves — a click belongs to the question that is arriving. */
.poll-stage > .poll-step-leaving { position: absolute; inset: 0 0 auto 0; z-index: 1; pointer-events: none; }
.poll-stage > .poll-step-entering { position: relative; z-index: 2; }
/* Focus lands on the arriving question (see stepper.ts) so the keyboard and screen readers follow the
   eye. It is a programmatic landing point, never tabbed to, so it takes no ring of its own. */
.poll-stage > .poll-block:focus { outline: none; }
/* Old height → new height over the same beat as the effect, so a one-line question following a long
   one doesn't snap the page shut under the respondent. The stepper pins the two heights in px and only
   while a change is playing; at rest the stage is plain auto-height again. */
.poll-stage-resizing { transition: height var(--poll-anim-dur) var(--poll-anim-ease); }

.poll-step-entering { animation: poll-step-in var(--poll-anim-dur) var(--poll-anim-ease) both; }
.poll-step-leaving { animation: poll-step-out var(--poll-anim-dur) var(--poll-anim-ease) both; }

@keyframes poll-step-in {
  from {
    opacity: var(--pa-o, 0);
    transform: translate3d(var(--pa-x, 0), var(--pa-y, 0), 0) scale(var(--pa-s, 1)) rotateX(var(--pa-rx, 0deg)) rotateY(var(--pa-ry, 0deg));
  }
  to { opacity: 1; transform: none; }
}
@keyframes poll-step-out {
  from { opacity: 1; transform: none; }
  to {
    opacity: var(--pa-o, 0);
    transform: translate3d(var(--pa-x, 0), var(--pa-y, 0), 0) scale(var(--pa-s, 1)) rotateX(var(--pa-rx, 0deg)) rotateY(var(--pa-ry, 0deg));
  }
}

/* The axis turns each effect's signed travel and tilt into an actual direction: vertical questions
   move on Y and tip around X, horizontal ones move on X and tip around Y. Opacity and scale are the
   same either way. */
.poll-stage > .poll-step-entering { --pa-o: var(--pa-o-in, 0); --pa-s: var(--pa-s-in, 1); }
.poll-stage > .poll-step-leaving { --pa-o: var(--pa-o-out, 0); --pa-s: var(--pa-s-out, 1); }
.poll-axis-y > .poll-step-entering { --pa-y: var(--pa-travel-in, 0); --pa-rx: var(--pa-tilt-in, 0deg); }
.poll-axis-y > .poll-step-leaving { --pa-y: var(--pa-travel-out, 0); --pa-rx: var(--pa-tilt-out, 0deg); }
.poll-axis-x > .poll-step-entering { --pa-x: var(--pa-travel-in, 0); --pa-ry: var(--pa-tilt-in, 0deg); }
.poll-axis-x > .poll-step-leaving { --pa-x: var(--pa-travel-out, 0); --pa-ry: var(--pa-tilt-out, 0deg); }

/* Deslizar — a full hand-off, edge to edge, with nothing fading. Travel is a percentage of the step's
   own box, so a short question crosses a short distance and the pace reads the same either way. */
.poll-anim-slide {
  --pa-travel-in: calc(var(--poll-anim-dir) * 100%);
  --pa-travel-out: calc(var(--poll-anim-dir) * -100%);
  --pa-o-in: 1;
  --pa-o-out: 1;
}
/* Deslizar suave — the default. The same movement cut down to a nudge, with the fade doing the work:
   it says which way the survey went without making every question feel like a page turn. */
.poll-anim-slide-fade {
  --pa-travel-in: calc(var(--poll-anim-dir) * 34px);
  --pa-travel-out: calc(var(--poll-anim-dir) * -34px);
}
/* Fundido — no travel at all, which is exactly why the axis makes no difference to it. */
.poll-anim-fade { --pa-o-in: 0; --pa-o-out: 0; }
/* Zoom — the next question grows into place while the current one recedes; on Back the two swap
   roles, so stepping back feels like stepping back out. */
.poll-anim-zoom {
  --pa-s-in: calc(1 - var(--poll-anim-dir) * 0.08);
  --pa-s-out: calc(1 + var(--poll-anim-dir) * 0.06);
}
/* Voltear — the two questions tip past each other in perspective, like cards coming off a deck. */
.poll-anim-flip {
  perspective: 1400px;
  --pa-travel-in: calc(var(--poll-anim-dir) * 26px);
  --pa-travel-out: calc(var(--poll-anim-dir) * -26px);
  --pa-tilt-in: calc(var(--poll-anim-dir) * -22deg);
  --pa-tilt-out: calc(var(--poll-anim-dir) * 22deg);
}
/* Superponer — the next question slides in ON TOP of the current one, which stays where it is and
   falls back. Nothing sets its travel, so it simply doesn't move. */
.poll-anim-cover {
  --pa-travel-in: calc(var(--poll-anim-dir) * 100%);
  --pa-o-in: 1;
  --pa-o-out: 0.55;
  --pa-s-out: 0.97;
}
/* Descubrir — the mirror image: the current question slides away and uncovers the next one, which was
   underneath it the whole time. The one leaving has to be the one on top for that to read. */
.poll-anim-reveal {
  --pa-travel-out: calc(var(--poll-anim-dir) * -100%);
  --pa-o-in: 1;
  --pa-o-out: 1;
}
.poll-anim-reveal > .poll-step-leaving { z-index: 3; }

/* Asked for less motion: the questions simply replace one another. The stepper already takes that path
   on its own — this is what catches the preference being turned on mid-survey, with a change in flight. */
@media (prefers-reduced-motion: reduce) {
  .poll-step-entering, .poll-step-leaving { animation: none; }
  .poll-stage-resizing { transition: none; }
  .poll-scale-ramped .poll-scale-btn { transition: none; }
}

/* One-question-at-a-time navigation */
.poll-nav { margin-top: 1rem; }
.poll-progress {
  height: 6px; border-radius: 999px; background: var(--border);
  overflow: hidden; margin-bottom: 0.9rem;
}
.poll-progress-fill {
  height: 100%; width: 0; border-radius: inherit; background: var(--accent);
  transition: width 0.35s ease;
}
@media (prefers-reduced-motion: reduce) { .poll-progress-fill { transition: none; } }
.poll-required-msg { color: var(--danger); font-size: 0.9rem; margin-bottom: 0.6rem; }
.poll-nav-btns { display: flex; align-items: center; gap: 0.75rem; }
.poll-nav-back {
  background: transparent; border: 1.5px solid var(--border); color: var(--text);
  border-radius: 10px; min-height: 3rem; padding: 0.8rem 1.2rem; font-size: 1rem; cursor: pointer;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.poll-nav-back:hover { border-color: var(--accent); }
.poll-nav-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.poll-nav-back[hidden] { display: none; }
.poll-nav-forward { position: relative; display: flex; align-items: center; gap: 0.75rem; margin-left: auto; }
/* Why the continue button is held. A `disabled` button receives no pointer events in any browser, so its
   `title` tooltip never appears — this is a bubble of our own instead, shown for as long as the button is
   held rather than on hover, which also puts it in reach on a phone. Anchored above the button on a wide
   screen; below the row, full width, where there is no room beside it. */
.poll-nav-tip {
  position: absolute; bottom: calc(100% + 0.5rem); right: 0; z-index: 2;
  max-width: min(22rem, 80vw); padding: 0.5rem 0.7rem; border-radius: 8px;
  background: var(--text); color: #fff; font-size: 0.82rem; line-height: 1.4;
  box-shadow: 0 6px 18px rgb(31 40 54 / 18%);
}
/* The bubble's tail, pointing down at the button it belongs to. */
.poll-nav-tip::after {
  content: ''; position: absolute; top: 100%; right: 1.4rem;
  border: 6px solid transparent; border-top-color: var(--text);
}
.poll-nav-tip[hidden] { display: none; }
@media (max-width: 480px) {
  .poll-nav-tip { position: static; max-width: none; margin-top: 0.5rem; }
  .poll-nav-tip::after { display: none; }
  .poll-nav-forward { flex-wrap: wrap; justify-content: flex-end; }
}
.poll-nav-next { width: auto; padding: 0.85rem 1.8rem; }
/* Both buttons carry an arrow pointing the way the questions actually travel (↑↓ or ←→), so the axis
   the author chose is legible before the first change plays. The next button loses its arrow on the
   finishing step: there is no next question for it to point at. */
.poll-nav-back, .poll-nav-next { display: inline-flex; align-items: center; justify-content: center; }
.poll-nav-arrow { font-size: 1.1em; line-height: 1; }
.poll-nav-back .poll-nav-arrow { margin-right: 0.4em; }
.poll-nav-next .poll-nav-arrow { margin-left: 0.5em; }
.poll-nav-arrow[hidden] { display: none; }
/* Enter continues (see stepper.ts). Shown only where there is an Enter key to press — on a phone it
   would be noise, and the row then holds nothing but the button. Small and grey on purpose: it is an
   aside to the button beside it, not a second thing to read before answering. */
.poll-nav-hint { display: none; align-items: center; gap: 0.4em; color: var(--muted); font-size: 0.78rem; white-space: nowrap; }
@media (hover: hover) and (pointer: fine) { .poll-nav-hint { display: inline-flex; } }
/* The key drawn as the cap it is, so the eye takes "Enter" as something to press rather than a word in
   a sentence. The heavier bottom border is the whole trick — it reads as the depth of a physical key. */
.poll-kbd {
  font-family: inherit; font-size: 0.95em; line-height: 1; color: var(--text);
  padding: 0.28em 0.45em 0.2em; border-radius: 5px;
  border: 1px solid var(--border); border-bottom: 2px solid var(--border);
  background: var(--card);
}
