/* CueFlip — styles for /survey/ only.
   Kept out of the shared stylesheet so the legal and support pages carry none
   of this weight. Same variables, same restraint: monochrome, one accent for
   the selected state, no shadows beyond the one that lifts the language thumb,
   and no animation that a reader has to wait for. */

/* ---------- bilingual copy ----------

   Both languages ship in the markup and CSS hides one of them, so the page is
   correct before any script runs: <html lang="en"> is the served state, and the
   language switch (or the reader's own browser language) flips the root
   attribute. Using display:none means the hidden language is also hidden from
   screen readers, which is the point — a reader should hear one language, not
   both interleaved. Every copy node carries its own lang attribute so
   VoiceOver picks the right voice for it. */

html[lang="en"] .zh,
html[lang="zh"] .en { display: none; }

/* ---------- page head ----------

   The switch used to sit directly under the lead paragraph with nothing
   between them — measured at a 0px gap, which read as a stray pill dropped on
   the end of a sentence. It belongs on the eyebrow line instead: a control
   with a row of its own, above the title, where a locale switch is looked for. */

.head-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 0 0 18px;
}

.head-row .eyebrow { margin: 0; }

/* ---------- language switch ---------- */

.lang-switch {
  /* inline-flex, not flex: as a block the pill stretches to the full measure
     and reads as a banner rather than a control. */
  position: relative;
  display: inline-flex;
  flex: none;
  padding: 3px;
  background: var(--card);
  border: 1px solid var(--rule-faint);
  border-radius: 980px;
}

/* The moving part. A separate element rather than a background on the pressed
   button, because a background cannot travel between two elements — swapping
   it is the hard cut this replaces. */
.lang-thumb {
  position: absolute;
  z-index: 0;
  top: 3px;
  left: 3px;
  /* The track's inner width is 100% - 6px (3px of padding each side), so half
     of it is 50% - 3px. translateX(100%) then moves the thumb by exactly its
     own width and lands it on the second half. */
  width: calc(50% - 3px);
  height: calc(100% - 6px);
  background: var(--bg);
  border: 1px solid var(--rule-faint);
  border-radius: 980px;
  transition: transform 280ms cubic-bezier(.32, .72, 0, 1);
}

.lang-switch:has(button[data-lang="zh"][aria-pressed="true"]) .lang-thumb {
  transform: translateX(100%);
}

.lang-switch button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--fg-secondary);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  /* flex-basis 0 with a shared minimum makes both halves exactly equal, which
     is what lets the thumb be a flat 50% instead of a measured width. */
  flex: 1 1 0;
  min-width: 82px;
  padding: 9px 4px;
  border-radius: 980px;
  cursor: pointer;
  /* Above the thumb, or the thumb paints over the label. */
  position: relative;
  z-index: 1;
  transition: color 200ms ease;
}

.lang-switch button[aria-pressed="true"] {
  color: var(--fg);
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .lang-thumb,
  .lang-switch button { transition: none; }
}

/* Hidden until scripting proves it can do anything. Without JS the switch
   cannot work, and a dead control is worse than no control. */
.lang-switch[hidden] { display: none; }

/* ---------- form skeleton ---------- */

form { margin: 0; }

fieldset {
  /* fieldset defaults to min-width:min-content, which stops it shrinking
     inside a grid or flex parent and forces a horizontal scrollbar on a
     phone. This single line is what keeps the rating rows narrow. */
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

/* A free-text question is a single control, so its prompt is a <label> rather
   than a <legend> in a one-input fieldset — same weight, same rhythm, correct
   semantics either way. */
legend,
.legend {
  display: block;
  padding: 0;
  margin: 0 0 16px;
  font-size: 19px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -.012em;
}

/* Spacing between questions is a MARGIN, never the fieldset's padding.
   A fieldset with a rendered <legend> puts the legend in its border box and
   starts the content box below it, so `padding-top` lands under the title
   instead of above it. That is not a theory: with `padding: 32px 0` the
   measured gap above the legend was 0.0px and below it 40px, which jammed
   every question's title against the previous question's rule and floated its
   own options away. Margins are not affected by the rendered legend, so the
   rhythm below is the rhythm on screen:

     72px  a new section
     36px  a new question (above its title)
     16px  title to its options — deliberately the smallest gap, so the title
           groups downward with what it labels
     32px  last option to the rule that closes the question */
.q {
  margin: 0;
  padding: 0 0 32px;
  border-bottom: 1px solid var(--rule-faint);
}

.q + .q { margin-top: 36px; }

form h2 { margin: 72px 0 0; }
form h2 + .q { margin-top: 30px; }
/* The first heading follows the callout, which already carries 44px below it. */
form > h2:first-of-type { margin-top: 8px; }

.q .hint {
  /* Sits between the title and the options, so it takes the title's gap and
     leaves the 16px for itself. */
  margin: -10px 0 16px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--fg-secondary);
}

.optional {
  font-weight: 400;
  font-size: 15px;
  color: var(--fg-tertiary);
}

/* ---------- choices (radio / checkbox) ---------- */

.choices {
  display: grid;
  gap: 8px;
}

/* Two columns once there is room for them. Ten single-file options read as a
   settings list; two columns read as a set to choose from. */
@media (min-width: 640px) {
  .choices.two-up { grid-template-columns: 1fr 1fr; column-gap: 10px; }

  /* An odd number of options leaves a hole in the second column, which reads
     as a missing option rather than as the end of the list. Every group that
     has one ends in a catch-all ("Something else", "Not sure", "I haven't
     really used it yet"), so letting that last one span both columns closes
     the hole AND tells the truth about what it is. */
  .choices.two-up > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}

.choice {
  display: flex;
  /* Centred, not top-aligned: the grid stretches both cells of a row to the
     taller one, so a top-aligned control on a one-line option sits high
     against a two-line neighbour. */
  align-items: center;
  gap: 12px;
  /* 50px tall at one line: past the 44pt minimum target, and the whole row is
     the target, not just the dot. */
  padding: 14px 16px;
  min-height: 50px;
  border: 1px solid var(--rule);
  border-radius: 12px;
  cursor: pointer;
  transition: border-color 140ms ease, background-color 140ms ease;
}

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

.choice:hover { background: var(--card); }

/* The native control stays in the accessibility tree and keeps keyboard
   behaviour (arrow keys within a radio group); only its painting is replaced. */
.choice input {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 21px;
  height: 21px;
  margin: 0;
  border: 1.5px solid var(--rule);
  background: var(--bg);
  cursor: pointer;
  transition: border-color 140ms ease, background-color 140ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .choice input { transition: none; }
}

.choice input[type="radio"] { border-radius: 50%; }
.choice input[type="checkbox"] { border-radius: 6px; }

.choice input:checked {
  border-color: var(--fg);
  background: var(--fg);
}

/* The mark is drawn with a background image rather than a pseudo-element so it
   sits on the input itself and cannot be knocked out of place by the label's
   line height. */
.choice input[type="radio"]:checked {
  background-image: radial-gradient(circle, var(--bg) 0 4px, transparent 4.5px);
}

/* The tick is drawn against the filled box, so it has to be the background
   colour, not white: in dark mode the fill is near-white and a white tick is
   invisible. A data URI cannot read a custom property, so the two schemes get
   one image each rather than one image and a bug. */
.choice input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-6.5' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-size: 15px 15px;
  background-position: center;
  background-repeat: no-repeat;
}

@media (prefers-color-scheme: dark) {
  .choice input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-6.5' fill='none' stroke='%23000000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

/* Selection is carried by the filled control AND by the row's border weight,
   so it survives Differentiate Without Colour and greyscale printing. */
.choice:has(input:checked) {
  border-color: var(--fg);
  background: var(--card);
}

.choice-text { font-size: 16px; line-height: 1.4; }

/* ---------- scales ----------

   One control for every numeric answer on the page: the eight feature ratings,
   the satisfaction question and the 0-10 recommendation. It is a segmented
   strip that always fills its container, and that is the whole fix for two
   separate bugs the old flex row had:

     * The boxes were laid out left to right at their natural width while
       `.scale-ends` was justified across the full measure, so "Very satisfied"
       sat 84px to the right of the last box, pointing at nothing.
     * Eleven fixed-width boxes could not fit a phone, so the 0-10 scale wrapped
       into a ragged 6 + 5 with a hole on the right.

   Filling the container makes the end labels line up with the ends by
   construction, and makes eleven cells a matter of arithmetic instead of luck.
   grid-auto-flow with auto-columns means no question has to declare how many
   options it has. */

.scale {
  display: grid;
  grid-auto-flow: column;
  /* minmax(0, 1fr), not 1fr: the default minimum is min-content, which would
     let a wide cell push the strip past its container again. */
  grid-auto-columns: minmax(0, 1fr);
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--bg);
}

.scale-opt {
  position: relative;
  min-width: 0;
}

.scale-opt input {
  /* Visually hidden, still focusable, still announced. Not display:none — that
     would take it out of the tab order and out of the accessibility tree. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.scale-box {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50px;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  color: var(--fg-secondary);
  transition: background-color 140ms ease, color 140ms ease;
}

/* Hairlines between the cells rather than a gap, so the strip is one object
   with no dead space in it — the reason eleven cells still land on a phone. */
.scale-opt + .scale-opt .scale-box { box-shadow: inset 1px 0 0 var(--rule); }

/* Rounded on the outside only. Done per-cell instead of with overflow:hidden
   on the strip, which would clip the focus ring. */
.scale-opt:first-child .scale-box { border-radius: 11px 0 0 11px; }
.scale-opt:last-child .scale-box { border-radius: 0 11px 11px 0; }

/* "Haven't used it" is an answer, not a sixth point on the scale, so it is
   toned back and fenced off from the run of numbers. */
.scale-opt.scale-na .scale-box {
  color: var(--fg-tertiary);
  box-shadow: inset 2px 0 0 var(--rule);
}

@media (prefers-reduced-motion: reduce) {
  .scale-box { transition: none; }
}

.scale-opt:hover .scale-box { background: var(--card); }

.scale-opt input:checked + .scale-box {
  background: var(--fg);
  color: var(--bg);
  font-weight: 600;
}

/* Eleven cells on a narrow phone are about 31px wide. The number still reads
   at 15px; 16px is what starts to touch the hairlines. */
.scale--dense .scale-box { font-size: 15px; }

/* Under the strip and exactly as wide as it, which is the only reason the
   right-hand label lands under the last cell. */
.scale-ends {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--fg-tertiary);
}

.scale-ends span:last-child { text-align: right; }

/* ---------- rating rows ---------- */

.ratings { display: grid; }

/* Each row is a div[role="radiogroup"] labelled by its own span, NOT a
   fieldset with a legend. A rendered <legend> is lifted out of the fieldset's
   anonymous content box and is not a grid item, so `display: grid` here put
   the feature name on one line and pushed the scale onto the next, hard right.
   The ARIA pair carries the same grouping and name to a screen reader and does
   take part in the grid. */
.rating {
  display: grid;
  gap: 12px;
  align-items: center;
  padding: 16px 0;
  border-bottom: 1px solid var(--rule-faint);
}

.rating:first-child { padding-top: 0; }
.rating:last-child { padding-bottom: 0; border-bottom: 0; }

.rating-label {
  font-size: 16px;
  line-height: 1.4;
}

/* From 640px up the label and the strip sit on one line. The strip takes a
   fixed width rather than a share of the row so that every strip on the page
   starts and ends at the same x — the block reads as a table without being
   one, and a long feature name cannot shove its own scale out of line. */
@media (min-width: 640px) {
  .rating {
    grid-template-columns: minmax(0, 1fr) 318px;
    column-gap: 24px;
    padding: 12px 0;
  }
}

/* ---------- text fields ---------- */

.field {
  display: block;
  width: 100%;
  padding: 13px 15px;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  /* 16px minimum, or iOS Safari zooms the page on focus. */
  font-size: 16px;
  line-height: 1.5;
}

textarea.field {
  min-height: 132px;
  resize: vertical;
}

.field::placeholder { color: var(--fg-tertiary); }

.counter {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--fg-tertiary);
  font-variant-numeric: tabular-nums;
}

/* ---------- submit ---------- */

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  padding-top: 36px;
}

.btn {
  appearance: none;
  border: 1px solid var(--fg);
  border-radius: 980px;
  background: var(--fg);
  color: var(--bg);
  font: inherit;
  font-size: 17px;
  font-weight: 500;
  padding: 14px 30px;
  min-height: 50px;
  cursor: pointer;
  transition: opacity 140ms ease;
}

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

.btn:hover { opacity: .85; }

.btn[disabled] {
  opacity: .4;
  cursor: default;
}

.actions .note {
  margin: 0;
  font-size: 14px;
  color: var(--fg-tertiary);
}

/* ---------- status ---------- */

.status {
  margin: 20px 0 0;
  padding: 16px 18px;
  border-radius: 12px;
  background: var(--card);
  font-size: 16px;
  line-height: 1.5;
}

.status:empty { display: none; }

.status.is-error {
  /* Not colour alone: the rule down the leading edge carries the same meaning
     for anyone who cannot separate the two states by hue. */
  border-left: 3px solid var(--fg);
  background: var(--card);
  font-weight: 500;
}

/* The whole form is replaced by this on success. */
.done {
  padding: 44px 0 8px;
}

.done h2 { margin-top: 0; }

/* ---------- focus ----------

   One ring, drawn the same way on every control, and never removed. outline
   (not box-shadow) so it survives forced-colors / Windows High Contrast. */

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
  border-radius: 6px;
}

.choice:has(input:focus-visible) {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
}

/* Drawn inside the cell: the cells of a strip touch, so an offset ring would
   be painted over by the neighbour on both sides. */
.scale-opt input:focus-visible + .scale-box {
  outline: 2px solid var(--fg);
  outline-offset: -3px;
  border-radius: 11px;
}

/* The ring is already drawn on the label box; drawing it on the invisible
   input as well would paint a second, misaligned rectangle. */
.scale-opt input:focus-visible,
.choice input:focus-visible { outline: none; }

/* ---------- screen-reader-only ---------- */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Visible once focused: the skip link, and nothing else so far. */
.skip-link:focus {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 20;
  width: auto;
  height: auto;
  margin: 0;
  padding: 12px 18px;
  clip: auto;
  clip-path: none;
  background: var(--bg);
  border: 1px solid var(--fg);
  border-radius: 10px;
  font-size: 15px;
}

/* ---------- honeypots ----------

   Hidden from sight and from assistive technology, left in the tab order's
   shadow with tabindex="-1". A human never sees them; a naive bot fills them
   and the submission is dropped server-side. Two of them, with different
   names, because a scraper tuned to skip one field called "website" is a
   scraper that has already been written. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------- narrow screens ---------- */

@media (max-width: 620px) {
  legend, .legend { font-size: 18px; }
  .q { padding-bottom: 28px; }
  .q + .q { margin-top: 30px; }
  form h2 { margin-top: 56px; }
  form h2 + .q { margin-top: 26px; }
  .choice { padding: 13px 14px; }
  .scale-box { height: 48px; }
  .scale--dense .scale-box { font-size: 13px; }
  .actions { padding-top: 30px; }
  .btn { width: 100%; justify-content: center; }
  /* Two long labels either side of a 346px line collide; stacking them keeps
     each one under the end it describes. */
  .scale-ends { gap: 12px; font-size: 12px; }
}

/* ---------- forced colours ----------

   In Windows High Contrast the custom paint is thrown away, so the selected
   state has to be re-stated in system colours or every option looks identical. */
@media (forced-colors: active) {
  .choice input:checked,
  .scale-opt input:checked + .scale-box {
    background: Highlight;
    border-color: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }
  .choice input { forced-color-adjust: none; border-color: CanvasText; }
  .scale { border-color: CanvasText; }
  .lang-thumb { border-color: CanvasText; }
}
