/* Shapefinder — one stylesheet, organised by the thing on screen.
 *
 * Sections, in order: tokens and base, the scene of floating shells, the questionnaire sheet, the
 * answer tiles, the Continue row, the budget rail, the tooltip, the results button, and the results
 * sheet (its list view is layouts.css). Each section carries its own phone rules at the end of it rather than in one block far
 * away, and every selector is defined exactly once — the file this replaced had `#reset` in eleven
 * places and two dozen `!important`s whose only job was to beat an earlier copy of the same rule.
 *
 * The five that remain each beat something this file does not control: an inline style written by
 * JS, a component's own display rule, the user-agent rule for a closed <dialog>, and the blanket
 * reduced-motion kill (plus the one exception to it).
 */

/* ==== Tokens and base ======================================================================== */
:root {
  --bg: #f5f5f4;
  --surface: #fff;
  --ink: #292524;          /* body text, and the dark buttons */
  --ink-deep: #1c1917;     /* dark button, hover */
  --ink-hover: #44403c;    /* icon button, hover */
  --muted: #6b645f;        /* secondary text: #78716c was 4.3:1 on --bg, under AA's 4.5 */
  --muted-strong: #57534e; /* tile hints, which sit on a tinted tile */
  --dim: #a8a29e;          /* unavailable text */
  --dim-soft: #c4c1be;     /* unavailable hint */
  --line: #e7e5e4;
  --tile: #f5f5f4;
  --tile-hover: #e7e5e4;
  --tile-active: #d6d3d1;
  --raised: #fafaf9;       /* text on dark, and the unavailable tile */
  --badge: #ea580c;        /* "Recommended" */
  --badge-secondary: #9ca3af;
  --warn: #b45309;
  --warn-hover: #92400e;
  --range: #3a83f2;        /* the filled part of the budget rail */
  --fit-ok: oklch(.56 .13 150);    /* a Fit row that is met */
  --fit-near: oklch(.64 .13 70);   /* nearly met */
  --fit-miss: oklch(.56 .17 28);   /* not met */
  --ease-out: cubic-bezier(.23, 1, .32, 1);
}

/* Smooth Shadow Plugin utilities. The site is plain CSS rather than Tailwind, so the package's
   source utilities are expressed here using the same stacked shadow and ring values. */
.smooth-shadow-ring {
  --smooth-shadow-color: var(--tw-shadow-color, black);
  --smooth-ring-color: rgba(0, 0, 0, .05);
  --smooth-ring-width: 1px;
  box-shadow: 0 17.54px 23.39px 0 color-mix(in srgb, var(--smooth-shadow-color) 4%, transparent),
    0 9.4px 12.5px 0 color-mix(in srgb, var(--smooth-shadow-color) 3%, transparent),
    0 5.25px 7px 0 color-mix(in srgb, var(--smooth-shadow-color) 2%, transparent),
    0 2.79px 3.72px -2px color-mix(in srgb, var(--smooth-shadow-color) 1%, transparent),
    0 1.16px 1.5px 0 color-mix(in srgb, var(--smooth-shadow-color) 1%, transparent),
    0 0 0 var(--smooth-ring-width) var(--smooth-ring-color);
}
.shadow-black { --tw-shadow-color: #000; }
.smooth-ring-neutral-300\/30 { --smooth-ring-color: rgb(212 212 216 / 30%); }

/* Inter cut down to Latin, Latin Extended-A (the ł in "zł"), punctuation, currency signs and arrows,
   with both axes and kerning kept: 96 KB where the full font (data/fonts/InterVariable.woff2) was
   352 KB, 60% of the mouse page's first load (2026-10-16 audit). Every character the pages and the
   catalogs print was checked against it. Rebuilt with fontTools:
   python -m fontTools.subset data/fonts/InterVariable.woff2 --unicodes="U+0000-017F,U+02C6,U+02DA,
   U+02DC,U+2000-206F,U+20A0-20CF,U+2100-218F,U+2190-21FF,U+2212,U+2215,U+2248,U+2260,U+2264,U+2265,
   U+FEFF,U+FFFD" --layout-features="kern,liga,calt,ccmp,locl,mark,mkmk,tnum,case" --flavor=woff2
   --output-file=latin.woff2
   then the weight axis cut to the 400-700 the pages use (nothing asks for lighter or heavier):
   python -m fontTools.varLib.instancer latin.woff2 wght=400:700 -o public/assets/Inter-latin-400-700.woff2
   96 KB to 66 KB; text drawn at 400-700 differs from the full axis by under 0.02% of a line's width
   (measured, 2026-10-16). A new name, because /assets/ is cached as immutable (netlify.toml). */
@font-face {
  font-family: Inter;
  src: url('assets/Inter-latin-400-700.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 14px Inter, Arial, sans-serif;
  font-optical-sizing: auto;
  overflow: hidden;
}

button, a {
  font-family: inherit;
  -webkit-tap-highlight-color: transparent;
}
button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  border: 0;
  background: none;
}
button:focus-visible, a:focus-visible {
  outline: 2px solid var(--muted);
  outline-offset: 5px;
}
[hidden] { display: none !important }   /* beats the display any component rule sets */

/* The live region that announces how many shells match. Visually hidden, never laid out.
   .visually-hidden is the page heading and intro: the page is built by JS, so without them the
   served HTML says nothing about what the site is (Amazon read it as "under construction"). */
.status, .visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.noscript { max-width: 480px; margin: 128px auto; padding: 0 24px; text-align: center; color: var(--muted); }

/* ==== The scene of floating shells =========================================================== */
/* Positions come from scene.js as inline transforms; everything here is presentation only. */
#scene {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.mouse {
  position: absolute;
  top: 0;
  left: 0;
  width: 190px;
  height: 222px;
  padding: 0;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: center;
  /* The renders are photos on white, so multiply drops their background into the page. */
  mix-blend-mode: multiply;
  will-change: transform;
  transition: transform 620ms var(--ease-out), opacity 195ms var(--ease-out);
}
/* Until fonts and the panel have settled the shells are placed, not animated (see app.js). */
body:not(.ready) .mouse { transition: none }
.mouse.featured { z-index: 2 }
/* The dimmed shells are clickable, so they brighten on hover. Their opacity is an inline style
   written per answer, which only !important can beat. */
.mouse:hover { opacity: 1 !important }

.mouse img {
  display: block;
  width: 228px;
  height: 228px;
  margin: -19px -19px 0;
  object-fit: contain;
  pointer-events: none;
  transition: transform .25s;
}
@media (hover: hover) and (pointer: fine) {
  .mouse:hover img { transform: translateY(-7px) }
}

.mouse .caption {
  display: block;
  width: 190px;
  min-height: 32px;
  text-align: center;
  white-space: normal;
  font-size: 15px;
  line-height: 1.25;
  opacity: 0;
  transition: opacity 230ms var(--ease-out);
}
.mouse .caption small {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  color: var(--muted);
}
.mouse .caption small:empty { display: none }
/* Only the three in the middle are named: an ambient shell is scenery, and thirteen names at once
   read as a list rather than a backdrop. */
.mouse.featured .caption {
  font-size: 12px;
  line-height: 1.2;
  opacity: 1;
}
.mouse.featured .caption small { font-size: 10px }
.mouse:not(.featured) .caption { visibility: hidden }

@media (max-width: 700px) {
  /* The shells are scaled to about .6 on a phone, which shrank the name to roughly 8px on screen.
     Dividing the node's own scale back out keeps the caption at its intended size whatever the
     shell does; scene.js publishes that scale as --s. */
  .mouse.featured .caption {
    font-size: calc(13px / var(--s, 1));
    font-weight: 600;
  }
  .mouse.featured .caption small {
    font-size: calc(11px / var(--s, 1));
    margin-top: calc(2px / var(--s, 1));
    color: var(--muted-strong);
  }
  /* Two lines at most, which is what scene.js leaves room for under each featured shell: a long pad
     name ran to three and its last line sat on the pad below. */
  .mouse.featured .caption { width: var(--capw, 190px) }
  .mouse .caption-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
  }
}

/* ==== The questionnaire sheet ================================================================ */
/* A card floating over the scene on the desktop; a bottom sheet on a phone. */
.questionnaire {
  position: absolute;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  width: 760px;
  padding: 28px 26px;
  background: var(--surface);
  border: 0;
  border-radius: 15px;
  z-index: 5;
}
.questionnaire h2 {
  margin: 0 28px 24px;
  text-align: center;
  font-size: 25px;
  font-weight: 600;
  letter-spacing: -.6px;
  line-height: 1.3;
}
#question:focus, .l-head h2:focus { outline: none }

/* Back, reset and the info button are one 28px icon box each, sitting on the heading's line and on
   the content edges. The footer is positioned over the whole sheet so its two buttons can be placed
   against those edges without taking part in the flow. */
.question-head {
  position: absolute;
  top: 30px;
  right: 26px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.question-footer {
  position: absolute;
  inset: 0;
  padding: 0;
  margin: 0;
  min-height: 0;
  border-top: 0;
  pointer-events: none;
}
.help,
.question-footer button {
  width: 28px;
  height: 28px;
  min-width: 28px;
  min-height: 28px;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  pointer-events: auto;
}
/* The info button is placed by .question-head; the other two place themselves over the sheet. */
.help {
  font-size: 13px;
  border-radius: 50%;
}
.question-footer button {
  position: absolute;
  top: 30px;
  font-size: 0;
  border-radius: 8px;
  transition: background .18s;
}
.question-footer button:disabled { opacity: .25; cursor: default }
.help svg,
.question-footer button svg {
  display: block;
  width: 21px;
  height: 21px;
  margin: 0;
}
.help:hover,
.question-footer button:hover { color: var(--ink-hover) }
.question-footer #back { left: 26px }
.question-footer #reset { left: 66px }
.question-footer #back:disabled { display: none }

@media (max-width: 700px) {
  /* Edge to edge and flush with the bottom of the viewport, rounded on the top corners only: that
     saves the side gutters and the bottom gap, which is a lot of room on a phone. The card shadow
     only falls downwards, which is the one edge a bottom sheet has off screen, so a light upward
     pair lifts the top edge off the scene instead. */
  .questionnaire {
    left: 0;
    right: 0;
    width: 100%;
    bottom: 0;
    transform: none;
    /* Kept lean (Patryk, 2026-10-16: a smaller sheet, more of the scene above it): 20px over the
       title and 14px under the tiles, where it was 28 and 20. */
    padding: 20px 17px calc(14px + env(safe-area-inset-bottom, 0px));
    border-radius: 16px 16px 0 0;
  }
  /* The icons stay on the title's first line, as on the desktop, and the title is padded the same on
     both sides to clear them, so it is centred on the sheet whichever icons a page shows: Start over
     ends 78px from the sheet's edge, 61px inside its padding, and the title keeps 13px from it. At a
     6px gap a one-line "How should the keys feel?" ran right up to the icons, and next to two icons
     on the left and none on the right it read as off-centre (Patryk, 2026-10-16). 16px read as a
     caption next to the tiles. A longer title wraps to two even lines rather than leaving one word
     behind. */
  .questionnaire h2 {
    margin: 3px 0 14px;
    padding: 0 74px;
    font-size: clamp(17px, 4.7vw, 19px);
    letter-spacing: -.45px;
    line-height: 1.25;
    text-wrap: balance;
  }
  .question-head { top: 24px; right: 16px }
  .question-footer button { top: 24px }
  /* 28px icons are small for a thumb, and Start over wipes every answer: the two sit 8px apart (they
     were 2px) and each takes taps a few px beyond its edge, the two hit areas still not touching. */
  .question-footer button::before { content: ""; position: absolute; inset: -4px -3px }
  .question-footer #back { left: 14px }
  .question-footer #reset { left: 50px }
}

/* A phone on its side (390-430px high) gets the desktop sheet, and the grip screen's photo tiles made
   it taller than the screen, its title and top row above the top edge with no way to scroll there
   (2026-10-16 audit). There the sheet scrolls inside itself. */
@media (max-height: 500px) and (min-width: 701px) {
  /* 100px: the sheet stands 32px off the bottom, and its top keeps clear of the 62px nav. */
  .questionnaire { max-height: calc(100vh - 100px); overflow-y: auto }
  /* Continue stays in view at the foot of the scrolling sheet, not below its fold. */
  #multi-actions { position: sticky; bottom: 0; z-index: 1; background: var(--surface) }
}

/* ==== Answer tiles =========================================================================== */
.options {
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 8px;
  overflow: visible;
}
.option {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  min-height: 96px;
  padding: 15px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border: 0;
  border-radius: 8px;
  background: var(--tile);
  text-align: center;
  /* A quick double tap on a tile advanced the step and selected the next screen's tile text. */
  user-select: none;
  -webkit-user-select: none;
  transition: background .18s, border-color .18s;
}
.option:hover { background: var(--tile-hover) }
/* A picked tile is outlined and wears a round tick badge on its top-right corner, half outside it
   (questionnaire.js, mousepads.js). A checkbox inside the corner ran into long labels on a 128px
   phone tile ("Extra buttons"); the badge takes no room in the tile at all. The outline already marks
   the pick, so the fill steps down from --tile-active to the hover grey. */
.option.selected {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
  background: var(--tile-hover);
}
.option-check {
  position: absolute;
  top: -7px;
  right: -7px;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--ink);
  color: #fff;
  box-shadow: 0 0 0 2px var(--surface);
  pointer-events: none;
}
.option-check svg { width: 12px; height: 12px }
.option strong {
  display: block;
  width: 100%;
  font-size: 13px;
  font-weight: 700;
}
.option small {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  margin: 0 auto;
  max-width: 150px;
  min-height: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted-strong);
}
/* Two bare tiles, so the word itself has to carry the tile. */
.option[data-field="side"] strong { font-size: 20px }
/* Mouse size benefits from more breathing room than the compact filters, and its badge sits out of
   the flow, so a tile with one keeps the same padding as a tile without. */
.option[data-field="msize"],
.option[data-field="msize"].has-badge {
  min-height: 104px;
  padding-top: 15px;
  padding-bottom: 15px;
}
.option[data-field="weight"] { min-height: 96px }
/* The connection tiles carry the longest descriptions ("Pairs with a second device" wraps to two
   lines), and the selected-state checkbox sits absolute in the top-right corner. At the shared 96px
   the centred two-line content rides up under that checkbox; the extra height pushes it clear. */
.option[data-field="connection"] { min-height: 116px }

/* Shape answers carry a real catalog render as a worked example, on the tile rather than in a
   dialog. The renders sit in a lot of whitespace, so the box is scaled into rather than enlarged. */
.options.with-shots .option {
  min-height: 180px;
  justify-content: center;
  padding-top: 12px;
  padding-bottom: 12px;
}
.option-shot {
  display: block;
  width: 100%;
  max-width: 146px;
  height: 114px;
  margin: 0 auto 2px;
  object-fit: contain;
  mix-blend-mode: multiply;
  pointer-events: none;
}
/* The trackball render reads small next to the mice, so nudge just this shot up 10%. */
.option[data-value="trackball"] .option-shot { transform: scale(1.1) }
/* The trackball-type tiles are only three wide, so the wide tiles left tall empty margins around the
   two shots. Trim their height and shot back so the screen reads like the Shape grid. */
.options.with-shots:has(.option[data-field="ballType"]) .option { min-height: 168px }
.option[data-field="ballType"] .option-shot { height: 112px }

/* Grip is a qualitative choice no label carries — "claw" means nothing to someone who has not met
   the word — so each tile carries a render of the hand on the shell. The source files are square
   with wide empty margins above and below the subject, so `cover` crops those away; `contain` would
   fit the whole square and shrink the hand to nothing. */
.option.has-icon {
  min-height: 198px;
  justify-content: center;
  padding-top: 14px;
  padding-bottom: 14px;
  gap: 2px;
}
.grip-icon {
  display: block;
  width: 100%;
  height: 108px;
  margin: 0 auto 5px;
  object-fit: cover;
  object-position: center;
  pointer-events: none;
}
/* Grip has six choices. The panel widens so all six fit on one line, which also lets the heading
   stay on one line and gives the hand renders more height; narrower screens keep three across. */
.options:has(> .option[data-field="grip"]) {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  overflow: visible;
}
.options:has(> .option[data-field="grip"]) > .option {
  flex: none;
  min-width: 0;
}
.questionnaire:has(.option[data-field="grip"]) .grip-icon { height: 118px }
/* The heading stays on one line on every width, so the tiles keep the full height of the row. */
.questionnaire:has(.option[data-field="grip"]) h2 { white-space: nowrap }
@media (min-width: 900px) {
  .questionnaire:has(.option[data-field="grip"]) { width: min(1100px, calc(100vw - 64px)) }
  .options:has(> .option[data-field="grip"]) { grid-template-columns: repeat(6, minmax(0, 1fr)) }
}

/* The badge is advice, not a filter, so it sits on the tile rather than changing how it behaves:
   every size stays clickable and the recommendation only says which one we would have picked. */
.rec-badge {
  position: absolute;
  top: 0;
  left: 0;
  padding: 3px 8px;
  white-space: nowrap;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: #fff;
  background: var(--badge);
  border-radius: 8px 0 8px 0;
  pointer-events: none;
}
.rec-badge.secondary {
  background: var(--badge-secondary);
  font-weight: 600;
}

/* An answer that would leave nothing to show. Not `disabled`: a disabled button swallows pointer
   events in Chrome, and the tile still has to explain itself on hover and on focus. Its reason is
   rendered in a viewport layer by questionnaire.js, not as a pseudo-element, because the tiles clip
   their renders and would clip the tooltip with them. */
.option.unavailable {
  overflow: visible;
  cursor: not-allowed;
  color: var(--dim);
  background: var(--raised);
}
.option.unavailable:hover { background: var(--raised) }
.option.unavailable strong { font-weight: 500 }
.option.unavailable small { color: var(--dim-soft) }
.option.unavailable .option-shot {
  transform: none;
  filter: grayscale(1);
  opacity: .35;
}
.option.unavailable .grip-icon {
  filter: grayscale(1);
  opacity: .38;
}

/* Registered so the width of the scroller's edge fade can animate (the phone rule below). */
@property --fade {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
@keyframes options-fade {
  0%, 92% { --fade: 56px }
  100% { --fade: 0px }
}

@media (max-width: 700px) {
  /* A horizontal scroller sized for four tiles. Two or three grow to fill the sheet; from four up
     the row overflows, and the tile cut by the screen edge is what signals that it scrolls. A row of
     tiles runs out to both edges of the phone (the sheet's 17px padding is taken back and returned
     inside the scroller), so that tile is cut by the screen rather than by an invisible line 17px
     short of it, and it fades out towards the edge instead of ending in a hard cut. */
  .options {
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 2px;
    scrollbar-width: none;
  }
  .options::-webkit-scrollbar { display: none }
  /* The 8px on top gives the tick badge, which stands 7px above a picked tile, room inside the
     scroller: a row that scrolls sideways clips vertically too. The negative margin keeps the tiles
     where they were. */
  .options:has(> .option) {
    margin: -8px -17px 0;
    padding: 8px 17px 2px;
    scroll-padding: 0 17px;
  }
  /* The fade follows the scroll: it is there while more tiles wait to the right and lifts over the
     last few pixels, so the final tile is never left half faded. Where scroll-driven animations are
     missing (Firefox) the row keeps the plain cut at the screen edge. */
  @supports (animation-timeline: scroll()) {
    .options:has(> .option) {
      -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
      mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
      animation: options-fade linear both;
      animation-timeline: scroll(self inline);
    }
  }
  .options > .option {
    flex: 1 1 140px;
    min-width: 128px;
    min-height: 68px;
    padding: 12px 7px;
  }
  /* Three tiles at 128px need 400px, so on a 390px phone the third ("No preference") was cut and
     the row scrolled for one tile (2026-10-16 audit). Three always share the width. */
  .options > .option:first-child:nth-last-child(3),
  .options > .option:first-child:nth-last-child(3) ~ .option { min-width: 0 }
  .option strong { font-size: 13px }
  .option small { font-size: 11px }
  .option[data-field="msize"],
  .option[data-field="msize"].has-badge {
    min-height: 98px;
    padding-top: 12px;
    padding-bottom: 12px;
  }
  .option[data-field="weight"] { min-height: 82px }
  .option[data-field="connection"] { min-height: 98px }
  .options.with-shots .option {
    min-height: 142px;
    padding-top: 8px;
    padding-bottom: 8px;
  }
  .option-shot { max-width: 100px; height: 76px }
  .option.has-icon {
    min-height: 156px;
    padding-top: 9px;
    padding-bottom: 9px;
  }
  .grip-icon { height: 76px; margin-bottom: 4px }
  /* On the phone grip is one scrolling row like the shape tiles, fade and all, instead of the
     three-across grid above, which stacked six tall tiles into two rows and pushed Continue off
     the screen. */
  .options:has(> .option[data-field="grip"]) {
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .options:has(> .option[data-field="grip"]) > .option {
    flex: 1 1 140px;
    min-width: 128px;
  }
  .questionnaire:has(.option[data-field="grip"]) .option.has-icon { min-height: 166px }
  .questionnaire:has(.option[data-field="grip"]) .grip-icon { height: 90px }
  .rec-badge { font-size: 8px; padding: 2px 6px }
}

/* ==== The Continue row ======================================================================= */
/* Continue confirms a multi-answer screen and the budget rail; every other tile advances by itself.
   It sits in its own row under the tiles because the footer is icon-only in this theme (its buttons
   run at font-size:0 and carry SVGs), so a worded button placed there would be invisible.
   The row collapses to nothing until the first pick. Height is the one property here with no
   transform equivalent, so it animates to a measured pixel value (--row-h, written by app.js)
   rather than to auto. The sheet is anchored to the bottom of the screen, so the growing row lifts
   it and the button rides up from under the tiles as one motion. */
#multi-actions {
  display: block;
  height: 0;
  overflow: hidden;
  transition: height 220ms var(--ease-out);
}
#multi-actions.open { height: var(--row-h, 60px) }
.multi-row {
  display: flex;
  padding-top: 10px;
}
#next {
  flex: 1;
  width: 100%;
  padding: 17px 20px;
  border-radius: 8px;
  background: var(--ink);
  color: #fff;
  font-size: 16px;
  font-weight: 500;
  line-height: 1;
  text-align: center;
  /* Folded away with the row: it starts below its place and rises into it, on the same duration and
     curve as the row's height, so the two read as one movement rather than two. `visibility` is
     delayed on the way out so the fade is visible before the button stops being focusable. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(100%);
  transition: background .18s, opacity 220ms var(--ease-out), transform 220ms var(--ease-out),
              visibility 0s 220ms;
}
#multi-actions.open #next {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: background .18s, opacity 220ms var(--ease-out), transform 220ms var(--ease-out),
              visibility 0s;
}
#next:disabled { cursor: default }
/* Blocked by the budget guard, it has to look it. Only in the open row: a folded row keeps its own
   opacity 0. */
#multi-actions.open #next:disabled { opacity: .4 }
@media (max-width: 700px) {
  #next { padding: 14px 20px }
}
#next:focus-visible { outline-offset: -4px; outline-color: #fff }
@media (hover: hover) and (pointer: fine) {
  #next:hover { background: var(--ink-hover) }
}

/* ==== The budget step ======================================================================== */
/* One rail from $0 to $150 with a draggable price tile, and No limit beside it. */
.budget-control { width: 100%; min-width: 0 }
.budget-layout {
  --rail-h: 48px;
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.budget-range {
  position: relative;
  flex: 1;
  min-width: 0;
  height: var(--rail-h);
  margin: 8px 0 34px;
  background: var(--tile);
  border-radius: 12px;
  touch-action: pan-y;
  user-select: none;
}
.budget-interval {
  position: absolute;
  left: var(--range-left);
  width: var(--range-width);
  top: 0;
  bottom: 0;
  background: var(--range);
  border-radius: 12px;
  pointer-events: none;
}
.budget-tick {
  position: absolute;
  top: calc(100% + 18px);
  transform: translateX(-50%);
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
}
.budget-handle {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 56px;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--ink);
  cursor: grab;
  z-index: 1;
}
.budget-handle strong {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.budget-handle:hover {
  border-color: var(--tile-active);
}
.budget-range.dragging,
.budget-range.dragging .budget-handle { cursor: grabbing }
.budget-range.unlimited .budget-interval { background: transparent; box-shadow: none }
.budget-range.unlimited .budget-handle {
  color: var(--dim);
  background: var(--raised);
}
.budget-unlimited {
  align-self: flex-start;
  height: var(--rail-h);
  margin-top: 8px;
  padding: 0 19px;
  border: 0;
  border-radius: 12px;
  background: var(--tile);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
}
.budget-unlimited:hover { background: var(--tile-hover) }
.budget-unlimited[aria-pressed="true"] { background: var(--tile-active) }
.budget-handle:focus-visible,
.budget-unlimited:focus-visible {
  outline: 2px solid var(--muted);
  outline-offset: 2px;
}

/* How many shells the current ceiling leaves, or — when it leaves none — the cheapest amount that
   would work, with a one-click set. It shares the heading's line, so an equal-width pad on the left
   mirrors it and the heading stays centred; baseline alignment sits it on the title's baseline. */
/* It is a paragraph inside the sheet, and it kept the sheet's paragraph type and colour long after
   a later rule tried to make it smaller and darker — these are the values actually on screen. */
.budget-count {
  margin: 0 0 18px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
}
.budget-count:empty { display: none }
.budget-count strong { color: var(--ink); font-weight: 600 }
.budget-count.warn { color: var(--warn) }
.qtitle:has(.budget-count:not(:empty)) { display: flex; align-items: baseline }
.qtitle:has(.budget-count:not(:empty)) .qtitle-pad { flex: 1 1 0 }
.qtitle:has(.budget-count:not(:empty)) .budget-count { flex: 1 1 0; text-align: right }
.budget-fix {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  color: var(--warn);
  text-decoration: underline;
}
.budget-fix:hover { color: var(--warn-hover) }
.budget-fix:focus-visible {
  outline: 2px solid var(--muted);
  outline-offset: 2px;
  border-radius: 3px;
}

@media (max-width: 700px) {
  /* A phone has no room beside the title for the count: shared with it, the title sat off-centre and
     the count wrapped down the sheet a word per line. It goes on its own line under the title. */
  .qtitle:has(.budget-count:not(:empty)) { display: block }
  .qtitle:has(.budget-count:not(:empty)) .budget-count { text-align: center }
  .qtitle:has(.budget-count:not(:empty)) h2 { margin-bottom: 2px }
  .budget-count { margin-bottom: 12px; font-size: 13px }
  .budget-layout { --rail-h: 44px; gap: 8px }
  .budget-handle { width: 50px; height: 60px }
  .budget-handle strong { font-size: 10px }
  .budget-unlimited { font-size: 11px; padding: 0 10px }
}

/* ==== The tooltip on a thin or dead tile ===================================================== */
/* Placed against the viewport by questionnaire.js after measuring, so it cannot be clipped by the
   tile or fall into the row below on a short screen. */
.option-tip {
  position: fixed;
  z-index: 100;
  width: 236px;
  max-width: calc(100vw - 24px);
  padding: 10px 12px;
  background: var(--ink);
  color: var(--raised);
  border-radius: 9px;
  box-shadow: 0 10px 30px #1c191733;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0;
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s ease;
}
.option-tip.is-visible { opacity: 1 }
/* An action tip is meant to be reached and clicked, so it takes the pointer (the plain tip stays
   pass-through). The button reverses the tip's dark fill to stand out against it. */
.option-tip.has-action { pointer-events: auto }
.tip-action {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 6px 10px;
  border: 0;
  border-radius: 6px;
  background: var(--raised);
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.tip-action:hover { background: #fff }
.option-tip:after {
  content: '';
  position: absolute;
  left: var(--arrow-x, 50%);
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border: 6px solid transparent;
}
.option-tip:not(.below):after { bottom: -12px; border-top-color: var(--ink) }
.option-tip.below:after { top: -12px; border-bottom-color: var(--ink) }
/* Moved clear of the sheet on a phone (questionnaire.js positionTip): an arrow would point at nothing. */
.option-tip.detached:after { display: none }

/* ==== The results button ===================================================================== */
.result-actions {
  min-height: 68px;
  width: 100%;
  display: flex;
  gap: 8px;
}
#best {
  flex: 1;
  padding: 12px;
  border: 0;
  border-radius: 8px;
  background: var(--ink);
  color: var(--raised);
  font-weight: 600;
}
#best:hover { background: var(--ink-hover) }
/* The no-results rescue reuses the same slot but is a "go back", not the primary action, so it
   reads as neutral rather than as the black confirm. */
#best.rescue { background: var(--tile); color: var(--ink) }
#best.rescue:hover { background: var(--tile-hover) }

/* ==== The results sheet ====================================================================== */
/* A transparent full-screen dialog holding the list view (layouts.css). */
dialog {
  border: 1px solid var(--line);
  border-radius: 18px;
  padding: 32px;
  width: min(470px, calc(100% - 28px));
  box-shadow: none;
}
dialog::backdrop {
  background: #29252442;
  backdrop-filter: blur(8px);
}
/* A closed dialog must stay hidden even if .wide lingers: #details.wide sets display:flex, which
   outweighs the user-agent rule that hides a closed dialog. */
#details:not([open]) { display: none !important }
dialog h2 {
  font-size: 26px;
  font-weight: 400;
  letter-spacing: -.6px;
}
dialog p {
  color: var(--muted);
  font-size: 13px;
  line-height: 1.7;
}
dialog a { color: var(--muted-strong) }
dialog img {
  display: block;
  width: 200px;
  height: 200px;
  margin: 0 auto;
  object-fit: contain;
}
dialog img.diagram {
  width: 100%;
  height: auto;
  max-width: 100%;
  margin: 2px 0 16px;
}
.close {
  position: absolute;
  right: 15px;
  top: 10px;
  font-size: 27px;
  color: var(--muted);
}

#details.wide {
  width: 100vw;
  max-width: none;
  height: 100vh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
}
#details.wide .close { display: none }
/* The scene is hidden behind the open sheet: its nodes use mix-blend-mode, which some browsers
   paint above a top-layer dialog, bleeding sharp mice over the sheet. It cannot merely stay
   visible-and-blurred either — any isolation that would stop the bleed also cuts the multiply blend
   off from the page and leaves white photo boxes. */
body.gallery-open #scene { opacity: .55; pointer-events: none }

/* The price sits where the eye already is, above the specs. The row is absent rather than empty
   when there is no price; with a budget set it says "Price unknown" instead, so the gap reads as an
   absence rather than as a number. */
.price-row {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin: -8px 0 0;
  padding-bottom: 6px;
}
.price { font-size: 23px; letter-spacing: -.7px }
.price-unknown { font-size: 14px; color: var(--muted) }

/* The tabs under the price are a segmented switch: a grey track with the open tab lifted onto a
   white pill. They used to be three grey words over an underline, and readers took them for a
   heading and never found Specs or Where to buy; a filled track reads as something to press. */
.tabs {
  position: relative;
  display: flex;
  margin-top: 8px;
  padding: 3px;
  background: var(--tile);
  border-radius: 10px;
}
/* Each tab takes an equal share of the track and centres its label. */
.tab {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  padding: 7px 0;
  border: 0;
  border-radius: 7px;
  text-align: center;
  background: none;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .1px;
  color: var(--muted-strong);
  cursor: pointer;
  transition: color .18s;
}
.tab:hover { color: var(--ink) }
.tab.is-on { color: var(--ink) }
/* The pill is one shape drawn by the track, not a background on each tab: it slides from one tab to
   the next when the reader switches, which swapping backgrounds could never do. 220ms on the shared
   ease-out, the same travel the rest of the page uses, and the reduced-motion kill below covers it
   with everything else. A card has two tabs or three (Fit only when there is something to check),
   so the track carries its count in --tabs and the pressed tab's position in data-on; a translate of
   100% is one pill width, which is one tab. */
.tabs::after {
  content: "";
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc((100% - 6px) / var(--tabs, 2));
  background: var(--surface);
  border-radius: 7px;
  box-shadow: 0 1px 2px rgb(41 37 36 / 12%), 0 0 0 .5px rgb(41 37 36 / 6%);
  transition: transform 220ms var(--ease-out);
}
.tabs[data-on="1"]::after { transform: translateX(100%) }
.tabs[data-on="2"]::after { transform: translateX(200%) }

/* Label over value, three to a row, so the whole spec block stays short. */
.specs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px 16px;
  margin: 12px 0 4px;
}
.spec {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.spec .k { font-size: 12px; letter-spacing: .2px; color: var(--muted) }
.spec .v { font-size: 14px; line-height: 1.3; overflow-wrap: anywhere }
/* Connection spans the grid: its value is the one that does not fit a single column, and it has to
   stay on one line. */
.spec-wide { grid-column: 1 / -1 }
.spec-wide .v { white-space: nowrap; overflow-wrap: normal }

/* The Amazon search, directly under the tabs and above the shop list. The label is centred in the
   whole button and the mark is parked at the left edge, out of flow, so the one does not push the
   other off centre. */
.buy-btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 13px 16px;
  border-radius: 10px;
  background: var(--ink);
  color: var(--raised);
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  transition: background .18s;
}
.buy-btn:hover { background: var(--ink-deep) }
.buy-btn.shop-search { margin-top: 12px }
/* Every shop after Amazon is grey: one black button per card, so the eye is not asked to choose
   between equals. They stack under it, one per shop that carries the shell. */
.buy-btn.is-shop {
  margin-top: 8px;
  /* A step under --tile: the tile grey is the page ground, and on the white card it read as a
     patch that needed a border to be a button. This shade holds its edge on its own. */
  background: #eeedec;
  color: var(--ink);
}
.buy-btn.is-shop:hover { background: var(--tile-hover) }
/* The grey buttons under Amazon stand two to a row: a card can carry five shops and AliExpress, and
   six full-width buttons made the shop tab the tallest panel, which set the height of the whole card
   and pushed it past a laptop screen. Each button carries the shop's name alone (gallery.js), with
   its icon beside the name rather than parked on the edge, so a long name has the width to wrap. */
.shop-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 12px;
}
.shop-grid .buy-btn.is-shop {
  margin-top: 0;
  gap: 8px;
  padding: 11px 10px;
  line-height: 1.2;
}
.shop-grid .buy-btn.is-shop:last-child:nth-child(odd) { grid-column: 1 / -1 }
/* `100vh` on a phone is the viewport with the browser's address bar counted out, so the sheet came
   out taller than what is actually on screen and its centred card ran off the bottom. */
@supports (height: 100dvh) {
  #details.wide { height: 100dvh }
}

@media (max-width: 700px) {
  /* The sheet itself never scrolls on a phone; what scrolls is inside it (layouts.css). */
  #details.wide {
    overflow: hidden;
    justify-content: center;
    overscroll-behavior: contain;
  }
  #details.wide #detail-content { flex: 0 0 auto; margin-block: 0 }
  #details.wide .close { right: 14px; top: 10px }
  /* One notch down across the board, so the card fits a phone without scrolling. */
  .price { font-size: 21px }
  .tab { padding: 7px 0; font-size: 13px }
  .buy-btn { padding: 12px 14px; font-size: 13px }
  /* Three columns here too: at two the spec block ran seven rows and set the card's height. */
  .specs {
    column-gap: 12px;
    row-gap: 9px;
    margin: 12px 0 2px;
  }
  .spec .k { font-size: 11px }
  .spec .v { font-size: 13px }
  .spec-wide .v { font-size: 13px }
}
/* On the narrowest phones the full "2.4 GHz wireless · Wired · Bluetooth" line needs a touch less. */
@media (max-width: 360px) {
  .spec-wide .v { font-size: 13px }
}

/* ==== Reduced motion ========================================================================= */
/* Everything stops travelling. The Continue button keeps a short fade, because its arrival is what
   tells you the row opened, and this rule is more specific than the blanket kill above it. */
@media (prefers-reduced-motion: reduce) {
  *, *:before, *:after {
    transition: none !important;
    animation: none !important;
  }
  #multi-actions #next {
    transform: none;
    transition: opacity 150ms ease !important;
  }
  .option-tip { transition: none }
  .mouse:hover img { transform: none }
}
