/* Advantech Selector — shared styles for the wizard and the admin console. */

/* The palette is Advantech's, from the corporate video guideline: the blue is
   the brand, the orange and the green are the two accents, and the grey is the
   secondary. The divisional colours are kept as their own tokens because they
   name a business unit rather than a shade -- Networks & Communication is the
   connectivity product line, and a line's colour should be the one its division
   actually uses.

   Everything else here is derived from those, not invented beside them: the
   soft variants are the brand at low opacity over the surface, and the status
   colours are the guideline's green and orange darkened enough to read as
   text on white. */
:root {
  color-scheme: light dark;

  /* Advantech corporate */
  --adv-blue: #004280;
  --adv-grey: #8c8c8c;
  --adv-orange: #f39800;
  --adv-green: #79bd28;

  /* Divisions, for anything that stands for one */
  --adv-ia: #008039;          /* Industrial Automation */
  --adv-logistics: #cc2931;   /* Digital Logistics & Fleet Management */
  --adv-iot: #80cc29;         /* WebAccess & IoT 4.0 */
  --adv-health: #5ca3e5;      /* Digital Healthcare */
  --adv-ecc: #801a7a;         /* Embedded Core Computing */
  --adv-retail: #e6a800;      /* iRetail */
  --adv-network: #0085cc;     /* Networks & Communication */

  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #fafbfc;
  --line: #dfe3e8;
  --line-soft: #eceff3;
  --ink: #1b2027;
  --ink-2: #5a636e;
  --ink-3: #8b949f;
  --brand: var(--adv-blue);
  --brand-ink: #ffffff;
  --brand-soft: #e6eef6;
  /* A brand blue that still reads on a dark chip or a coloured bar. */
  --brand-lift: #0a5ca8;
  --ok: #007033;
  --ok-soft: #e6f3eb;
  --warn: #9a6100;
  --warn-soft: #fdf2df;
  /* Text that has to read as a warning on the page's own background. */
  --warn-ink: #6b4300;
  --bad: #b3261e;
  --bad-soft: #fdeceb;
  /* Corner radius was thirteen different numbers -- 2, 4, 5, 7, 9, 16, 20 and
     the rest -- picked one component at a time. Six steps cover every case,
     and a pill is round rather than 20px round. --radius is kept because it is
     the middle step and half the file already names it. */
  --r-xs: 3px;          /* inline code, tiny swatches */
  --r-sm: 6px;          /* small controls inside a control */
  --r-md: 8px;          /* inputs, chips, buttons */
  --radius: 10px;       /* popovers, boxes */
  --r-lg: 12px;         /* cards and panels */
  --r-xl: 14px;         /* floating panels and dialogs */
  --r-pill: 999px;
  --shadow: 0 1px 2px rgba(16,24,40,.06), 0 8px 24px -16px rgba(16,24,40,.28);
  --tour-dim: rgba(12, 16, 24, .55);
  /* How much room the detail drawer takes. Named once so the panel, the page
     it pushes and everything pinned to the right edge cannot drift apart. */
  --dtl-w: min(560px, 46vw);
  /* The guideline names Noto Sans and Noto Sans CJK. They are listed first and
     the old stack stays behind them, so a machine that has them uses them and
     one that does not still renders -- nothing is fetched from a font host,
     which this server has no business depending on. */
  --font: "Noto Sans", "Noto Sans TC", "Noto Sans JP", "Noto Sans CJK TC",
          "Noto Sans CJK JP", "Inter", -apple-system, BlinkMacSystemFont,
          "Segoe UI", "Hiragino Sans", "Yu Gothic UI", "Microsoft JhengHei", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #101317;
    --surface: #191d23;
    --surface-2: #1f242b;
    --line: #2c333c;
    --line-soft: #242a31;
    --ink: #e8ebef;
    --ink-2: #a6b0bb;
    --ink-3: #78828d;
    /* The corporate blue is too dark to be a brand colour on a dark surface,
       so the dark theme uses a lighter tint of the same hue -- the division
       blue, which is the same family and is legible here. */
    --brand: #4aa3e0;
    --brand-ink: #04202f;
    --brand-soft: #16303f;
    --brand-lift: #79bde8;
    --ok: #8fd94f;
    --ok-soft: #1a2712;
    --warn: #f0b33c;
    --warn-soft: #2c2314;
    --warn-ink: #f0b93c;
    --bad: #ff8177;
    --bad-soft: #2e1817;
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px -18px rgba(0,0,0,.9);
    --tour-dim: rgba(0, 0, 0, .66);
  }
}

* { box-sizing: border-box; }

/* A class that sets `display` outranks the browser's own [hidden] rule, so the
   hidden attribute silently stops working on .board, .btn and friends. */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }
button, input, select, textarea { font: inherit; color: inherit; }

/* ---------------------------------------------------------------- chrome */

.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap; row-gap: 8px;
  padding: 12px 24px;
  background: color-mix(in srgb, var(--surface) 97%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--line);
}
.brand {
  display: flex; align-items: center; gap: 12px; font-weight: 640;
  letter-spacing: -.01em; text-decoration: none; color: inherit;
}
.brand .logo { height: 22px; width: auto; display: block; flex: none; }
.brand-rule { width: 1px; height: 22px; background: var(--line); flex: none; }

/* Official raster assets usually ship on a white background. Add
   class="logo logo--plate" to the <img> to sit them on a white card so they
   stay legible in the dark theme. */
.brand .logo--plate {
  background: #fff; padding: 4px 7px; border-radius: var(--r-sm); height: 30px;
}
/* The answered count rides on the "All questions" button, which is the thing
   it counts. The bar states it only in the split layout, where that button is
   gone because every question is already on screen. */
.brand small { display: none; font-weight: 450; font-size: 12px; color: var(--ink-3); }
body[data-layout="split"] .brand small { display: block; }
.spacer { flex: 1 1 auto; }

/* On a wide screen the secondary controls are simply part of the bar; the ⋯
   button does not exist. The wrapper only becomes a real box on a phone. */
.navrest { display: contents; }
.topbar .nav-more { display: none; }

.seg { display: flex; gap: 2px; padding: 2px; background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-md); }
.seg button {
  border: 0; background: transparent; padding: 4px 10px; border-radius: var(--r-sm);
  font-size: 13px; color: var(--ink-2); cursor: pointer;
}
.seg button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); font-weight: 560; }

/* The language picker. Three buttons cost the header about 150px, which is
   what pushed it onto two rows on a 1440-wide laptop; a select is one control
   wide and names each language in that language, which is what a reader
   looking for their own language scans for. */
.langsel {
  appearance: none;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  font: inherit; font-size: 13px; line-height: 1.2;
  padding: 6px 26px 6px 11px; border-radius: var(--r-md); cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 12px top 50%, right 7px top 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.langsel:hover { border-color: var(--ink-3); }
.langsel:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* The list itself is drawn by the browser and does not inherit the page's
   colours, so it is told them explicitly or a dark page opens a white menu. */
.langsel option { background: var(--surface); color: var(--ink); }

.btn {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line); background: var(--surface);
  padding: 7px 13px; border-radius: var(--r-md); cursor: pointer; font-size: 13.5px;
  text-decoration: none; white-space: nowrap;
}
.btn:hover { border-color: var(--ink-3); }
.btn.primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); font-weight: 560; }
/* brightness() on a colour this dark barely moves; the lifted blue is a real
   step and is the same hue. */
.btn.primary:hover { background: var(--brand-lift); border-color: var(--brand-lift); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn.ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn.danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); background: transparent; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---------------------------------------------------------------- layout */

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}

.shell { max-width: 1600px; margin: 0 auto; padding: 0 24px; }
main.shell { padding-top: 22px; padding-bottom: 48px; }

/* The questions in a column on the left, the products beside them. The
   wide layout (a bar of condition chips above a full-width grid) is gone;
   body[data-layout="split"] is always set. */
.stage { display: block; }
.qcol { display: none; }

body[data-layout="split"] .stage {
  display: grid; grid-template-columns: 380px minmax(0, 1fr);
  gap: 22px; align-items: start;
}
body[data-layout="split"] .qcol {
  display: flex; flex-direction: column; gap: 12px;
  position: sticky; top: 112px; max-height: calc(100vh - 132px); overflow: auto;
  padding-right: 2px;
}
body[data-layout="split"] .conds { display: none; }
body[data-layout="split"] #btn-board { display: none; }
body[data-layout="split"] .condwrap { justify-content: flex-end; align-items: center; padding-top: 8px; padding-bottom: 8px; }
/* The filters above the products (wizard.js, renderFilters): one small pill
   per field, a label and a native select -- a select is what a phone already
   knows how to open. */
.filters { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; flex: 1 1 auto; min-width: 0; }
.filters[hidden] { display: none; }
.ftitle { font-size: 12px; font-weight: 650; color: var(--ink-3); letter-spacing: .04em; margin-right: 2px; }
.fchip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 4px 3px 11px;
  border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--surface);
  font-size: 12.5px; color: var(--ink-2); cursor: pointer;
}
.fchip.on { border-color: var(--brand); background: var(--brand-soft); color: var(--ink); }
/* In force because of an answer on the left, not chosen here. */
.fchip.byq { border-style: dashed; }
.fchip .fl { white-space: nowrap; }
.fchip select {
  border: 0; background: transparent; color: inherit; font: inherit; font-weight: 600;
  padding: 2px 4px; max-width: 160px; cursor: pointer;
}
.fchip select:focus-visible { outline: 2px solid var(--brand); border-radius: 6px; }
.fclear { font-size: 12.5px; padding: 3px 8px; }
/* A filter that takes several values: a button that opens a checklist. */
.fchip.fbtn { font: inherit; font-size: 12.5px; padding: 3px 10px 3px 11px; }
.fchip .fv { font-weight: 600; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fchip .fcaret { font-size: 10px; color: var(--ink-3); }
.fchip .fmore { flex: none; font-size: 11px; font-weight: 650; padding: 0 6px; border-radius: var(--r-pill); background: var(--brand); color: var(--brand-ink); }
.fchip.fbtn:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.fpop {
  position: fixed; z-index: 80; display: flex; flex-direction: column; gap: 8px; padding: 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 48px -18px rgba(8, 10, 14, .45); overflow: hidden;
}
.fp-head { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.fp-mode { margin-left: auto; font-size: 11.5px; color: var(--ink-3); }
.fp-q { display: flex; flex-wrap: wrap; gap: 6px; }
.fp-q .pill { font-size: 12px; }
.fp-search { width: 100%; padding: 6px 9px; border: 1px solid var(--line); border-radius: 8px; font: inherit; font-size: 12.5px; background: var(--surface); color: var(--ink); }
.fp-list { list-style: none; margin: 0; padding: 0; overflow: auto; flex: 1 1 auto; min-height: 0; }
.fp-list li[hidden] { display: none; }
.fp-list label { display: flex; align-items: center; gap: 8px; padding: 5px 4px; border-radius: 6px; font-size: 13px; cursor: pointer; }
.fp-list label:hover { background: var(--surface-2); }
.fp-foot { display: flex; align-items: center; gap: 8px; padding-top: 6px; border-top: 1px solid var(--line); }
.fp-foot .spacer { flex: 1; }
@media print { .fpop { display: none !important; } }
/* On a phone the filters are one row that scrolls sideways, as the
   condition chips were: six stacked pills would push the products off the
   first screen. */
@media (max-width: 640px) {
  .filters { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
  .filters::-webkit-scrollbar { display: none; }
  .filters > * { flex: none; }
}
@media (max-width: 1100px) {
  body[data-layout="split"] .stage { grid-template-columns: minmax(0, 1fr); }
  body[data-layout="split"] .qcol { position: static; max-height: none; }
}


/* question cards for the split layout */
.q { padding: 15px 16px; }
.q h2 { font-size: 14.5px; margin: 0 0 3px; letter-spacing: -.01em; }
.q .help { margin: 0 0 11px; font-size: 12.5px; color: var(--ink-2); }
.q .tag {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-3); border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 1px 7px; margin-left: 7px; vertical-align: 2px; font-weight: 500;
}

/* The first thing in the tab order, and invisible until it is focused: a way
   past the header and the conditions to the products themselves. It has to
   stay in the layout rather than be display:none, or it could not be focused
   at all. */
.skiplink {
  position: fixed; top: 8px; left: 8px; z-index: 200;
  transform: translateY(-160%);
  background: var(--brand); color: var(--brand-ink);
  padding: 9px 15px; border-radius: var(--r-md); font-size: 13.5px; font-weight: 560;
  text-decoration: none; box-shadow: var(--shadow);
}
.skiplink:focus { transform: none; outline: 2px solid var(--brand-ink); outline-offset: -4px; }
@media (prefers-reduced-motion: no-preference) { .skiplink { transition: transform .12s; } }
@media print { .skiplink { display: none; } }

/* ------------------------------------------------------------ condition bar */

/* The condition bar sits directly under the header. Its offset is the
   header's real height, which JS measures -- the header is two rows on a
   phone and one on a desktop, so a fixed number is wrong on one of them. */
.condbar {
  position: sticky; top: var(--topbar-h, 53px); z-index: 30;
  background: color-mix(in srgb, var(--surface) 97%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--line);
}
.condwrap { display: flex; align-items: flex-start; gap: 14px; padding-top: 11px; padding-bottom: 11px; }
.conds { display: flex; flex-wrap: wrap; gap: 7px; flex: 1 1 auto; min-width: 0; }
.condwrap > .btn { flex: none; }

.cond {
  display: inline-flex; align-items: baseline; gap: 7px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2);
  border-radius: var(--r-pill); padding: 5px 13px; max-width: 100%;
}
.cond:hover { border-color: var(--ink-3); }
.cond[aria-expanded="true"] { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.cond .ck { font-size: 11.5px; color: var(--ink-3); white-space: nowrap; }
.cond .cv {
  font-size: 13px; font-weight: 560; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 22ch;
}
.cond.empty { border-style: dashed; background: transparent; }
.cond.empty .cv { font-weight: 450; color: var(--ink-3); }
/* Answered and unanswered were a dashed border apart, which is nothing at a
   glance. A condition you have set now carries a filled dot and says its value
   in the brand colour, so the strip reads as a list of decisions rather than a
   row of identical pills. */
.cond:not(.empty) {
  border-color: color-mix(in srgb, var(--brand) 38%, var(--line));
  background: var(--surface);
}
.cond:not(.empty)::before {
  content: ""; flex: none; align-self: center;
  width: 6px; height: 6px; border-radius: 50%; background: var(--brand);
}
.cond:not(.empty) .cv { color: var(--brand); }
.cond.lead { border-color: var(--brand); background: var(--brand-soft); }
.cond.lead .ck { color: color-mix(in srgb, var(--brand) 75%, var(--ink)); }
.cond.lead .cv { color: var(--brand); font-weight: 600; }

.sizebtn { flex: none; font-variant-numeric: tabular-nums; }
.bq-n { font-variant-numeric: tabular-nums; color: var(--ink-2); }
.sizebtn b { color: var(--brand); }

/* popover holding one question's options */
.pop {
  position: fixed; z-index: 60; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 18px 48px -18px rgba(8, 10, 14, .45); width: min(560px, calc(100vw - 24px));
  padding: 16px 18px 18px; max-height: min(74vh, 620px); overflow: auto;
}
.pop h2 { font-size: 15px; margin: 0 0 3px; letter-spacing: -.01em; }
.pop .help { margin: 0 0 13px; font-size: 12.5px; color: var(--ink-2); }
.pop .tag {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-3); border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 1px 7px; margin-left: 7px; vertical-align: 2px; font-weight: 500;
}
.pop .clear { margin-top: 12px; }

.pills { display: flex; flex-wrap: wrap; gap: 7px; }
.pill {
  display: inline-flex; flex-direction: column; gap: 2px;
  border: 1px solid var(--line); background: var(--surface-2);
  border-radius: var(--r-md); padding: 7px 12px; cursor: pointer; text-align: left; max-width: 100%;
}
.pill:hover { border-color: var(--ink-3); }
.pill[aria-pressed="true"] { border-color: var(--brand); background: var(--brand-soft); box-shadow: inset 0 0 0 1px var(--brand); }
.pill .l { font-size: 13.5px; }
.pill .h { font-size: 11.5px; color: var(--ink-2); }

.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 9px; }
.opt-card {
  border: 1px solid var(--line); background: var(--surface-2);
  border-radius: var(--radius); padding: 11px; cursor: pointer; text-align: left;
  display: flex; flex-direction: column; gap: 7px;
}
.opt-card:hover { border-color: var(--ink-3); }
.opt-card[aria-pressed="true"] { border-color: var(--brand); background: var(--brand-soft); box-shadow: inset 0 0 0 1px var(--brand); }
.opt-card .l { font-size: 13px; font-weight: 560; line-height: 1.35; }
.opt-card .h { font-size: 11.5px; color: var(--ink-2); line-height: 1.45; }

svg.art { width: 100%; height: auto; display: block; }
svg.art-icon { width: 40px; height: 36px; }
.a-frame { fill: color-mix(in srgb, var(--ink) 4%, transparent); stroke: var(--line); }
.a-corner { stroke: var(--ink-3); stroke-width: 1.5; fill: none; }
.a-fill { fill: color-mix(in srgb, var(--ink) 55%, transparent); }
.a-bg { fill: color-mix(in srgb, var(--surface) 90%, transparent); }
.a-box { fill: none; stroke: var(--brand); stroke-width: 2; stroke-dasharray: 4 3; }
.a-stroke { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.opt-card .a-stroke { color: var(--ink-2); }
.opt-card[aria-pressed="true"] .a-stroke { color: var(--brand); }
.opt-card[aria-pressed="true"] .a-fill { fill: var(--brand); }

/* ------------------------------------------------- floating question board */

.board {
  /* Its corner is set from JS, not here -- see place() in wizard.js, which
     keeps it clear of the sticky bars above it whatever height those are. */
  position: fixed; z-index: 70;
  width: 430px; height: min(600px, 78vh);
  min-width: 300px; min-height: 160px;
  max-width: calc(100vw - 20px); max-height: calc(100vh - 20px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 26px 70px -22px rgba(8, 10, 14, .55), 0 2px 8px rgba(8, 10, 14, .12);
  resize: both;
}
.board.min { height: auto !important; min-height: 0; resize: none; }
.board.dragging { user-select: none; }

.board-head {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 10px 9px 12px; border-bottom: 1px solid var(--line);
  background: var(--surface-2); cursor: grab; touch-action: none; flex: none;
}
.board.dragging .board-head { cursor: grabbing; }
.board.min .board-head { border-bottom: 0; }
.board-head strong { font-size: 13.5px; }
.board-head .btn { padding: 2px 9px; font-size: 14px; line-height: 1.2; }
.board-prog { font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.grip {
  width: 11px; height: 14px; flex: none; opacity: .5;
  background-image: radial-gradient(currentColor 1px, transparent 1.2px);
  background-size: 5px 5px; color: var(--ink-3);
}

.board-body { padding: 4px 14px 14px; overflow: auto; flex: 1; }
.board.min .board-body { display: none; }

.bq { padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
.bq:last-child { border-bottom: 0; }
.bq h3 {
  font-size: 12px; margin: 0 0 6px; font-weight: 620;
  display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
}
.bq h3 .full { font-size: 11px; color: var(--ink-3); font-weight: 400; }
.bq.done h3 { color: var(--ink); }
.bq.todo h3 { color: var(--ink-2); }
.bq.todo h3::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--brand); flex: none; align-self: center;
}
.bopts { display: flex; flex-wrap: wrap; gap: 5px; }
.bopt {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2);
  border-radius: var(--r-md); padding: 3px 9px; font-size: 12px; text-align: left;
}
.bopt:hover { border-color: var(--ink-3); }
.bopt[aria-pressed="true"] {
  border-color: var(--brand); background: var(--brand-soft);
  color: var(--brand); font-weight: 560;
}
.bopt svg.art { width: 30px; height: 21px; flex: none; }
.bopt svg.art-icon { width: 18px; height: 16px; flex: none; }

@media (max-width: 640px) {
  .board { width: calc(100vw - 20px); height: min(70vh, 560px); resize: none; }
}

/* ---------------------------------------------------------------- results */

.sechead {
  display: flex; align-items: baseline; gap: 10px;
  margin: 26px 0 13px; font-size: 15px; font-weight: 620; letter-spacing: -.01em;
}
.sechead:first-child { margin-top: 4px; }
.sechead .n { font-size: 13px; font-weight: 450; color: var(--ink-3); }
.sechead .why { font-size: 12.5px; font-weight: 450; color: var(--ink-2); }

.pgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(252px, 1fr));
  gap: 16px; align-items: stretch;
}

.pcard {
  position: relative;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column;
}
.pcard:hover { border-color: var(--ink-3); }
.pcard.blocked { opacity: .78; }
.pcard.blocked:hover { opacity: 1; }
/* the product link stretches over the whole card */
.pcard .plink { text-decoration: none; color: inherit; }
.pcard .plink::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.pcard:hover .plink .nm { color: var(--brand); }
.pcard .pfoot { position: relative; z-index: 2; }

.shot {
  --shot-h: 168px;
  height: var(--shot-h); background: #fff; display: grid; place-items: center;
  border-bottom: 1px solid var(--line-soft); position: relative; overflow: hidden;
}
/* The height cap has to be an absolute length. A percentage max-height resolves
   against the grid area, whose row is content-sized and therefore indefinite, so
   the browser drops the cap entirely -- a square product photo then renders at
   its natural 220px inside a 168px box and gets clipped top and bottom. Only the
   landscape shots happened to fit, which is why the bug looked selective. */
.shot img {
  max-width: 84%; max-height: calc(var(--shot-h) * 0.84);
  width: auto; height: auto; object-fit: contain; display: block;
}
.rank {
  position: absolute; top: 9px; left: 9px; z-index: 1;
  background: var(--ink); color: var(--bg); border-radius: var(--r-pill);
  font-size: 11px; font-weight: 650; padding: 2px 8px; font-variant-numeric: tabular-nums;
}

.pbody { padding: 14px 15px 13px; display: flex; flex-direction: column; gap: 9px; flex: 1; }
.pcard .phead { display: flex; align-items: flex-start; gap: 8px; }
.pcard .pttl { min-width: 0; }
.pcard .nm { font-weight: 620; font-size: 14px; line-height: 1.35; display: block; }
.pcard .sku { font-family: var(--mono); font-size: 11px; color: var(--ink-3); display: block; margin-top: 3px; }
.pcard .cat { font-size: 11.5px; color: var(--ink-3); }
/* The top three used to be numbered in the image box. Without one the number
   goes where the eye already is -- beside the name. */
.rankin {
  flex: none; margin-top: 1px; background: var(--ink); color: var(--bg);
  border-radius: var(--r-pill); font-size: 11px; font-weight: 650; padding: 1px 7px;
  font-variant-numeric: tabular-nums; line-height: 1.5;
}

.pspecs { display: flex; flex-wrap: wrap; gap: 5px; }
.pspecs span {
  font-size: 11px; border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 1px 8px; color: var(--ink-2); background: var(--surface-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pspecs b { color: var(--ink); font-weight: 600; }

.score { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 3px; }
/* No bar, because there was nothing for it to compare. The number keeps the
   row it had so the cards in a row still line up. */
.score.flat { justify-content: flex-end; }
.score .sl, .dtlscore .sl { font-size: 11.5px; color: var(--ink-3); white-space: nowrap; cursor: help; }
.score.flat .sl { margin-right: auto; }
.fitnote { margin: -6px 0 12px; font-size: 12px; line-height: 1.5; color: var(--ink-3); }
.score.flat .pct { color: var(--ink-3); }
.bar { flex: 1; height: 5px; border-radius: var(--r-pill); background: var(--line-soft); overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--ok); border-radius: var(--r-pill); }
.pcard.blocked .bar i { background: var(--ink-3); }
.score .pct { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--ink-2); min-width: 32px; text-align: right; }

.blockers { font-size: 11.5px; color: var(--bad); line-height: 1.45; }
.perfnote {
  font-size: 11px; color: var(--ink-3); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.modfit { font-size: 11.5px; color: var(--ink-2); }
.modfit.ok { color: var(--ok); }

.pfoot {
  border-top: 1px solid var(--line-soft); padding: 9px 13px 10px;
  display: flex; flex-direction: column; gap: 6px; background: var(--surface-2);
}
/* One row: the two buttons on the left, the datasheet pushed to the far edge
   where a link belongs. They wrap together rather than stacking one per line. */
.pacts { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
/* A borderless button in a footer reads as a caption. These are the two things
   you can do with the product, so they look like things you can press. */
.pacts .btn {
  padding: 4px 11px; font-size: 12px; border-radius: var(--r-md);
  border-color: var(--line); background: var(--surface); color: var(--ink-2);
}
.pacts .btn:hover { border-color: var(--brand); color: var(--brand); }

.pacts .dslink {
  margin-left: auto; font-size: 11.5px; color: var(--ink-2);
  text-decoration: none; border-bottom: 1px solid var(--line);
}
.pacts .dslink:hover { color: var(--brand); border-bottom-color: var(--brand); }
details.why summary {
  font-size: 11.5px; color: var(--ink-3); cursor: pointer; list-style: none;
}
details.why summary::-webkit-details-marker { display: none; }
details.why summary::before { content: "▸ "; }
details.why[open] summary::before { content: "▾ "; }
.reason { display: flex; gap: 7px; font-size: 11.5px; padding: 3px 0; align-items: baseline; }
.reason .dot { width: 6px; height: 6px; border-radius: 50%; flex: none; margin-top: 4px; }
.reason.pass .dot { background: var(--ok); }
.reason.fail .dot { background: var(--bad); }
.reason.unknown .dot { background: var(--ink-3); opacity: .5; }
.reason .rk { color: var(--ink-2); }
.reason .rv { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--ink-3); text-align: right; }
.reason.fail .rv { color: var(--bad); }

/* This box says one sentence. It used to say it in 130 pixels. */
.splash {
  border: 1px dashed var(--line); border-radius: var(--radius); padding: 12px 16px;
  color: var(--ink-2); font-size: 13px; line-height: 1.55; margin-bottom: 16px;
  background: var(--surface-2);
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.splash strong { color: var(--ink); font-size: 13.5px; letter-spacing: -.01em; }
/* The page when its own server cannot be reached. It is the whole content
   area, because there is nothing else to show, and it is the one screen that
   must render from the stylesheet alone. */
.down {
  max-width: 460px; margin: 12vh auto 0; text-align: center;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 28px 26px; box-shadow: var(--shadow);
}
.down strong { display: block; font-size: 17px; letter-spacing: -.01em; margin-bottom: 8px; }
.down p { margin: 0 0 18px; font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
.down .btn { min-width: 120px; justify-content: center; }
.downtech {
  margin: 16px 0 0 !important; font-family: var(--mono); font-size: 11px;
  color: var(--ink-3); word-break: break-all;
}
/* A refresh failed but the old list is still on screen: say which it is. */
.stalebar {
  margin: 0 0 14px; padding: 9px 13px; border-radius: var(--r-md);
  background: var(--warn-soft); border: 1px solid color-mix(in srgb, var(--warn) 32%, transparent);
  color: var(--ink); font-size: 12.5px; line-height: 1.5;
}
.empty { color: var(--ink-2); font-size: 13.5px; line-height: 1.6; }
.empty strong { display: block; color: var(--ink); margin-bottom: 4px; }

/* --------------------------------------------------------- testing mode

   One switch in the console, and every page says so. The banner is a strip
   the page is pushed down by rather than something floating over it: a notice
   that covers the thing it is warning about is a notice somebody scrolls away
   from. The watermark sits above everything and receives nothing -- no
   pointer, no selection, no print. */

/* Turned down after the first version, which was a full amber strip with an
   uppercase badge on it -- the loudest thing on a page whose job is to show
   products. The notice has to be findable, not first. So: a hairline of the
   warning colour along the top edge instead of a filled band, the page's own
   surface behind it, and the mark itself as small caps in the muted ink with a
   dot in front. It is still the first thing in the document for a screen
   reader, and the sheet that gets printed and the enquiry that gets sent still
   say it in plain words -- those are the copies that leave the building, and
   they are where being unmissable is worth the noise. */
.testbar {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 5px 24px;
  background: var(--bg);
  border-top: 2px solid color-mix(in srgb, var(--warn) 60%, transparent);
  border-bottom: 1px solid var(--line);
  color: var(--ink-2); font-size: 11.5px; line-height: 1.45;
}
.testbar b {
  flex: none; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  color: color-mix(in srgb, var(--warn-ink, #6b4300) 82%, var(--ink-2));
}
/* The dot carries the colour, so the words do not have to. */
.testbar b::before {
  content: ''; display: inline-block; vertical-align: 1px;
  width: 6px; height: 6px; margin-right: 6px; border-radius: 50%;
  background: var(--warn);
}
.testbar span { min-width: 0; }
/* The installation's own sentence sits with the standard one rather than
   across the page from it -- it is the more specific half of the same notice. */
.testbar em { font-style: normal; color: var(--ink-3, var(--ink-2)); min-width: 0; }

/* The page's own sticky things start below the banner. --topbar-h is measured
   by JS from the header's real box, so the offsets below it follow by
   themselves; this only has to move the header. */
body.testing .topbar { top: var(--testbar-h, 32px); }

.testwm {
  position: fixed; inset: 0; z-index: 210;
  pointer-events: none; user-select: none;
  background-repeat: repeat;
}

/* Paper carries the warning as words, not as a wash: a background image is
   not printed by default, and a sheet that quietly loses its "not a quotation"
   is the exact thing this mode exists to prevent. */
@media print {
  .testwm { display: none; }
  .testbar { display: none; }
}

/* On a phone the same three sentences are 108px of an 844px screen, and the
   banner is sticky, so it would hold that for the whole visit. There it scrolls
   away once it has been read -- the watermark is fixed and everywhere, and
   carries the message from then on. */
@media (max-width: 640px) {
  .testbar {
    position: static;
    padding: 5px 14px 6px; font-size: 11px; gap: 3px 8px;
  }
  .testbar b { font-size: 10px; }
  body.testing .topbar { top: 0; }
}

/* ------------------------------------------------------- the detail panel

   A drawer beside the results on a desktop, a sheet over the bottom of the
   screen on a phone. The list is pushed rather than covered, because the other
   candidates and the conditions that produced them are the reason the visitor
   is looking at this one. */

.dtl { position: fixed; inset: 0; z-index: 70; pointer-events: none; }
/* The mask only exists on a phone, where the sheet does cover the page. On a
   desktop the list beside it stays live and clickable. */
.dtlmask { position: absolute; inset: 0; background: var(--tour-dim); opacity: 0; }

.dtlpanel {
  /* Below the header, not over it. Pushing the header aside instead made it
     wrap onto two rows at 1440 with the drawer open, and covering it would
     take away the language switcher and Contact sales. */
  position: absolute; top: var(--topbar-h, 53px); right: 0; bottom: 0;
  width: var(--dtl-w);
  display: flex; flex-direction: column; pointer-events: auto;
  background: var(--surface); border-left: 1px solid var(--line);
  box-shadow: -18px 0 48px -28px rgba(8, 10, 14, .5);
  transform: translateX(100%);
}
body.dtl-on .dtlpanel { transform: none; }

/* The page makes room -- the header and the condition bar as well as the
   list, so the drawer takes a column of the window rather than lying on top of
   the controls. Anything pinned to the right edge moves with it. */
body.dtl-on .shell { margin-right: var(--dtl-w); max-width: none; }
body.dtl-on .cmptray { transform: translateX(-50%) translateX(calc(var(--dtl-w) / -2)); }
body.dtl-on .asst:not(.moved) { right: calc(20px + var(--dtl-w)); }

@media (prefers-reduced-motion: no-preference) {
  .dtlpanel { transition: transform .24s cubic-bezier(.22, .61, .36, 1); }
  .dtlmask { transition: opacity .24s; }
  .shell, .cmptray, .asst { transition: margin-right .24s cubic-bezier(.22, .61, .36, 1),
    transform .24s cubic-bezier(.22, .61, .36, 1), right .24s cubic-bezier(.22, .61, .36, 1); }
}

.dtlhead {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 14px 14px 12px 18px; border-bottom: 1px solid var(--line);
}
.dtlname { flex: 1 1 auto; min-width: 0; }
.dtlname b { display: block; font-size: 15px; line-height: 1.35; letter-spacing: -.01em; }
.dtlsku { display: block; margin-top: 3px; font-size: 11.5px; color: var(--ink-3); font-family: var(--mono); }
.dtlx {
  flex: none; border: 1px solid transparent; background: transparent; cursor: pointer;
  color: var(--ink-3); font: inherit; font-size: 15px; line-height: 1;
  width: 30px; height: 30px; display: grid; place-items: center; border-radius: var(--r-sm);
}
.dtlx:hover { color: var(--ink); background: var(--surface-2); border-color: var(--line); }

.dtltabs { display: flex; gap: 2px; padding: 0 12px; border-bottom: 1px solid var(--line); }
.dtltabs button {
  border: 0; background: transparent; cursor: pointer; font: inherit; font-size: 13px;
  color: var(--ink-2); padding: 9px 12px; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.dtltabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--brand); font-weight: 560; }

.dtlbody { flex: 1 1 auto; overflow: auto; padding: 16px 18px; }
.dtlshot {
  height: 180px; background: #fff; border: 1px solid var(--line-soft);
  border-radius: var(--r-md); display: grid; place-items: center; margin-bottom: 14px;
  overflow: hidden;
}
.dtlshot img { max-width: 86%; max-height: 152px; object-fit: contain; display: block; }
.dtldesc { margin: 0 0 14px; font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
.dtlscore { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
.dtlscore .bar { flex: 1; height: 6px; }
.dtlscore .pct { font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; }

.dtlspecs { display: flex; flex-direction: column; }
.dtlrow {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 12px;
  padding: 7px 0; border-bottom: 1px solid var(--line-soft); font-size: 13px;
}
.dtlrow:last-child { border-bottom: 0; }
.dtlrow .k { color: var(--ink-2); }
.dtlrow .v { color: var(--ink); font-weight: 560; }

/* The frame tabs fill the body and lose its padding: a document wants the
   whole width it can get. */
.dtl .dtlbody:has(> .dtlframe) { padding: 0; }
.dtlframe { position: relative; height: 100%; min-height: 320px; background: var(--surface-2); }
.dtlframe iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Under the frame rather than over it: it has to be there before anyone knows
   whether the frame worked, because nothing inside the page can tell. */
.dtlframe:has(> .dtlnote) { display: flex; flex-direction: column; }
.dtlframe:has(> .dtlnote) iframe { flex: 1 1 auto; height: auto; }
.dtlnote {
  flex: none; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; border-top: 1px solid var(--line); background: var(--surface);
  font-size: 12px; line-height: 1.5; color: var(--ink-2);
}
.dtlnote span { flex: 1 1 14ch; min-width: 0; }

.dtlfoot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 11px 14px; border-top: 1px solid var(--line); background: var(--surface-2);
}
.dtlfoot .btn.primary { margin-left: auto; }

/* No beside on a phone: the panel becomes a sheet, the mask comes up with it,
   and the list underneath does not move. */
@media (max-width: 899px) {
  :root { --dtl-w: 0px; }
  .dtlmask { pointer-events: auto; }
  body.dtl-on .dtlmask { opacity: 1; }
  .dtlpanel {
    top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 88vh;
    border-left: 0; border-top: 1px solid var(--line);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    transform: translateY(100%);
  }
  body.dtl-on .shell { margin-right: 0; }
  body.dtl-on .cmptray { transform: none; }
  .dtlpanel { top: auto; }
  /* A handle, so it reads as something that came up from the bottom and can
     go back down. */
  .dtlhead::before {
    content: ""; position: absolute; top: 7px; left: 50%; transform: translateX(-50%);
    width: 36px; height: 4px; border-radius: var(--r-pill); background: var(--line);
  }
  .dtlhead { position: relative; padding-top: 18px; }
  .dtlshot { height: 150px; }
  .dtlshot img { max-height: 126px; }
}

/* -------------------------------------------------- sizing report (dialog) */

.metric { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
.metric:last-child { border-bottom: 0; }
.metric .k { font-size: 13px; color: var(--ink-2); }
.metric .v { font-size: 14px; font-weight: 620; font-variant-numeric: tabular-nums; text-align: right; }
.metric .v .u { font-weight: 450; font-size: 12px; color: var(--ink-3); margin-left: 3px; }
.metric .n { grid-column: 1 / -1; font-size: 12px; color: var(--ink-3); line-height: 1.5; }
.dlgsec { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); font-weight: 620; margin: 22px 0 6px; }
.dlgsec:first-child { margin-top: 0; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font-size: 12px; border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 3px 9px; background: var(--surface-2); color: var(--ink-2);
}
.chip b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }

.warnlist { display: flex; flex-direction: column; gap: 8px; }
.warnitem {
  font-size: 13px; line-height: 1.5; color: var(--ink);
  background: var(--warn-soft); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  border-radius: var(--r-md); padding: 9px 11px;
}

/* ------------------------------------------------------- the narrow header

   Eleven controls need a lot of room, and how much depends on the language
   and on how many solutions the switcher shows: Japanese and English need far
   more than Chinese. A fixed breakpoint guessed wrong in both directions --
   the bar wrapped into two rows on a 1600-1700px screen in Japanese. So the
   page measures (fitHeader in wizard.js): when the controls do not fit on one
   row the bar gets .compact, which keeps what a visitor came for (the line
   switcher and Contact sales) and moves everything else behind ⋯. */
.topbar.compact { flex-wrap: nowrap; }
.topbar.compact .nav-more { display: inline-flex; padding: 7px 11px; font-size: 15px; line-height: 1; }
.topbar.compact .navrest {
  display: none;
  position: absolute; top: calc(100% + 6px); right: 12px; z-index: 41;
  flex-direction: column; align-items: stretch; gap: 6px;
  padding: 10px; min-width: 190px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: 0 12px 30px rgb(0 0 0 / .28);
}
.topbar.compact .navrest[data-open] { display: flex; }
/* Inside the menu every row is a full-width line, so the eye runs down one
   edge instead of hunting across a grid of pills. */
.topbar.compact .navrest .btn, .topbar.compact .navrest .langsel { justify-content: flex-start; text-align: left; width: 100%; }
.topbar.compact #nav-help::after { content: " " attr(data-label); }
.topbar .btn, .topbar .langsel, #lines button { white-space: nowrap; }

/* The wide/split switch has nothing to switch below 1100px -- the split layout
   already collapses to one column there -- so it does not earn a row. */
@media (max-width: 1100px) {
}

@media (max-width: 820px) {
  .topbar { gap: 10px; padding: 10px 14px; }
  .brand .logo { height: 19px; }
  .brand small { font-size: 11px; }
  #lines button { padding: 4px 8px; font-size: 12.5px; }

  /* The line switcher gets a row of its own. Left to wrap on its own terms it
     lands wherever the width happens to allow, which is two rows in Chinese and
     three in English -- "Intelligent connectivity" is simply longer than
     「網路與通訊」. Given the whole row it is two rows in every language, and it
     scrolls sideways rather than stacking when the names are long. */
  #lines {
    order: 10; flex-basis: 100%; max-width: 100%;
    overflow-x: auto; scrollbar-width: none;
  }
  #lines::-webkit-scrollbar { display: none; }
  #lines button { flex: none; white-space: nowrap; }
}

@media (max-width: 640px) {
  /* The name is readable in the address bar, and the count now rides on the
     "All questions" button -- which is what it counts -- so the brand on a
     phone is the logo alone. */
  .brand #brand-title, .brand small,
  body[data-layout="split"] .brand small { display: none; }
  .brand-rule { display: none; }
  /* The first row carries the logo, Contact sales and ⋯ -- which fit at 390px
     only once they stop being desktop-sized. At the old size "Contact sales"
     overshot by four pixels and pushed ⋯ onto a row of its own, which is how
     English ended up a row taller than Chinese. */
  .topbar { gap: 8px; }
  .topbar .btn { padding: 6px 10px; font-size: 12.5px; }
  .topbar .nav-more { padding: 6px 9px; }
}

/* ------------------------------------------------- the conditions on a phone

   Wrapped, the chips took three rows and about 135px of a 664px screen. In one
   swipeable row they take 46px, and "All questions" is deliberately outside the
   scroller so it stays put no matter how far along the row you are -- it is the
   way to see everything at once, so it must never be the thing that scrolled
   off. The strip fades at its right edge, which is what says there is more. */
@media (max-width: 1100px) {
  .condwrap { align-items: center; gap: 9px; }
  .conds {
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x proximity; scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-right: 6px;
    mask-image: linear-gradient(to right, #000 calc(100% - 38px), transparent 100%);
  }
  .conds::-webkit-scrollbar { display: none; }
  .conds > * { flex: none; scroll-snap-align: start; }
}

/* On a phone every pixel the two pinned buttons give up is a pixel of visible
   strip, so the sizing button keeps its number and drops its label. */
@media (max-width: 640px) {
  .sizebtn .sz-l { display: none; }
  .bq-l { display: none; }
  .bq-n::before { content: "?"; margin-right: 5px; color: var(--ink-3); font-weight: 600; }
  .condwrap > .btn { padding: 6px 11px; font-size: 13px; }
  .cond { padding: 4px 11px; }
  .cond .ck { font-size: 11px; }
  .cond .cv { font-size: 12.5px; max-width: 11ch; }
}

@media (max-width: 640px) {
  .gate-top { top: 10px; right: 10px; gap: 7px; }
  .gate-inner { padding-top: 40px; }
  .shell { padding: 0 14px; }
  .cond .cv { max-width: 14ch; }
  /* Two columns on a 390px screen gave each card about 165px: every spec chip
     wrapped and the names broke across three lines. One column is wider than
     two halves of the same screen, and the list is scrolled either way. */
  .pgrid { grid-template-columns: 1fr; gap: 10px; }
  .shot { --shot-h: 140px; }
  .pbody { padding: 12px 13px 11px; }
  .pacts .dslink { margin-left: auto; }
}

/* ---------------------------------------------------------------- admin */

.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 18px; }
.tabs button {
  border: 0; background: transparent; padding: 9px 14px; cursor: pointer;
  font-size: 14px; color: var(--ink-2); border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--brand); font-weight: 560; }
.tabgroup.on .tabbtn { color: var(--ink); border-bottom-color: var(--brand); }
.tabgroup.on .tabbtn .gnow { color: var(--ink-3); font-size: 12.5px; }
.tabgroup.open .tabbtn .gcaret { transform: rotate(180deg); display: inline-block; }
.tabgroup.open .tabmenu { display: block; }

table.grid { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.grid th, table.grid td { text-align: left; padding: 7px 12px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
table.grid th { vertical-align: bottom; }
table.grid th { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); font-weight: 600; }
table.grid tr:hover td { background: var(--surface-2); }
/* A numeric column is right-aligned -- and so is its heading. Aligning only
   the cells put every heading at the left of a wide column and every value at
   the right of it, which reads as a row that has slipped one column across. */
table.grid td.num, table.grid th.num { text-align: right; }
table.grid td.num { font-variant-numeric: tabular-nums; }

.field { display: block; margin-bottom: 12px; }
.field > span { display: block; font-size: 12.5px; color: var(--ink-2); margin-bottom: 4px; }
.field input[type=text], .field input[type=number], .field input[type=password],
.field input[type=email], .field input[type=url], .field input[type=tel],
.field select, .field textarea {
  width: 100%; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: var(--surface-2);
}
.field textarea { min-height: 74px; resize: vertical; font-family: var(--mono); font-size: 12.5px; }
.field.row { display: flex; align-items: center; gap: 8px; }
.field.row > span { margin: 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
@media (max-width: 700px) { .grid2, .grid3 { grid-template-columns: 1fr; } }

dialog {
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0;
  background: var(--surface); color: var(--ink); max-width: 860px; width: calc(100% - 32px);
  box-shadow: 0 24px 64px -24px rgba(0,0,0,.5);
}
dialog::backdrop { background: rgba(8,10,14,.5); backdrop-filter: blur(2px); }
dialog .dlg-head { padding: 16px 20px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 12px; }
dialog .dlg-head h2 { margin: 0; font-size: 16px; }
dialog .dlg-body { padding: 20px; max-height: min(68vh, 640px); overflow: auto; }
dialog.wide { max-width: 980px; }

/* ------------------------------------------------ first-run help and tour */

#intro { max-width: 760px; }
.intro-head { padding: 26px 28px 4px; }
.intro-head h2 { margin: 0 0 7px; font-size: 21px; letter-spacing: -.02em; }
.intro-head p { margin: 0; color: var(--ink-2); font-size: 14px; line-height: 1.6; }
.intro-steps { padding: 18px 28px 4px; display: flex; flex-direction: column; gap: 14px; }
.intro-step { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 14px; align-items: start; }
.intro-step .n {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--brand); color: var(--brand-ink); font-size: 13px; font-weight: 700;
  margin-top: 1px;
}
.intro-step h3 { margin: 3px 0 4px; font-size: 14.5px; }
.intro-step p { margin: 0; font-size: 13px; color: var(--ink-2); line-height: 1.6; }
.intro-note {
  margin: 16px 28px 0; padding: 10px 13px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line-soft);
  font-size: 12.5px; color: var(--ink-2); line-height: 1.55;
}
.intro-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 18px 28px 22px;
}
@media (max-width: 560px) {
  .intro-head, .intro-steps, .intro-foot { padding-left: 18px; padding-right: 18px; }
  .intro-note { margin-left: 18px; margin-right: 18px; }
  .intro-foot { flex-wrap: wrap; }
}

/* The tour dims everything but the step's target. The dimming is the huge
   box-shadow spread of #tourhole, a fixed box laid over the target: a shadow
   paints over any stacking context without lifting the target out of its own,
   never affects layout or scrollbars, and is not hit-tested, so the page below
   stays clickable. The hole itself carries the ring. */
.tourmask { position: fixed; inset: 0; z-index: 87; cursor: pointer; }
.tourhole {
  position: fixed; z-index: 88; pointer-events: none; border-radius: var(--r-lg);
  box-shadow: 0 0 0 100vmax var(--tour-dim), 0 0 0 1px rgba(255, 255, 255, .28) inset;
  outline: 3px solid var(--brand); outline-offset: 0;
  animation: tourpulse 1.8s ease-in-out infinite;
  transition: top .2s ease, left .2s ease, width .2s ease, height .2s ease;
}
@keyframes tourpulse {
  0%, 100% { outline-color: var(--brand); }
  50% { outline-color: color-mix(in srgb, var(--brand) 40%, transparent); }
}
/* Marks the element the current step points at; the spotlight does the work. */
.tour-target { border-radius: var(--radius); }
@media (prefers-reduced-motion: reduce) {
  .tourhole { animation: none; transition: none; }
}

.tourtip {
  position: fixed; z-index: 95; width: min(370px, calc(100vw - 24px));
  padding: 15px 17px 13px;
  box-shadow: 0 20px 52px -18px rgba(8, 10, 14, .5), 0 2px 8px rgba(8, 10, 14, .14);
}
.tourtip .tour-step { font-size: 11px; color: var(--ink-3); letter-spacing: .04em; }
.tourtip h3 { margin: 5px 0 6px; font-size: 15px; letter-spacing: -.01em; }
.tourtip p { margin: 0; font-size: 13px; color: var(--ink-2); line-height: 1.6; }
.tour-hint { margin: 9px 0 0; font-size: 11.5px; color: var(--ink-3); }
.tour-foot { display: flex; align-items: center; gap: 7px; margin-top: 12px; }
.cfg-cap { padding: 14px 20px 0; }
.cfg-foot {
  padding: 13px 20px 16px; border-top: 1px solid var(--line);
  background: var(--surface-2); display: flex; flex-direction: column; gap: 8px;
}
.cfg-row {
  display: flex; align-items: center; gap: 14px;
  padding: 8px 0; border-bottom: 1px solid var(--line-soft);
}
.cfg-row:last-child { border-bottom: 0; }
.cfg-row.nofit { opacity: .55; }
.cfg-name { flex: 1; min-width: 0; font-size: 13.5px; }
.cfg-name a { text-decoration: none; }
.cfg-name a:hover { color: var(--brand); }
.cfg-name .sku { font-family: var(--mono); font-size: 11px; color: var(--ink-3); margin-left: 7px; }
.cfg-meta { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; }
.cfg-why { font-size: 11.5px; color: var(--bad); margin-top: 2px; }
.stepper { display: flex; align-items: center; gap: 4px; flex: none; width: 92px; justify-content: flex-end; }
.stepper button {
  width: 26px; height: 26px; border-radius: var(--r-md); border: 1px solid var(--line);
  background: var(--surface); cursor: pointer; font-size: 15px; line-height: 1;
}
.stepper button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.stepper button:disabled { opacity: .4; cursor: not-allowed; }
.stepper b { min-width: 20px; text-align: center; font-variant-numeric: tabular-nums; }
.cfg-err {
  font-size: 12.5px; line-height: 1.5; color: var(--bad);
  background: var(--bad-soft); border: 1px solid color-mix(in srgb, var(--bad) 28%, transparent);
  border-radius: var(--r-md); padding: 8px 11px;
}
.cfg-err code { font-size: 11.5px; margin-right: 5px; }
.cfg-ok {
  font-size: 12.5px; color: var(--ok); background: var(--ok-soft);
  border: 1px solid color-mix(in srgb, var(--ok) 28%, transparent);
  border-radius: var(--r-md); padding: 8px 11px; font-weight: 560;
}
.pfoot .cfgbtn { padding: 3px 9px; font-size: 11.5px; align-self: flex-start; }

/* The share dialog. The QR is black on white whatever the theme is -- a reader
   looks for contrast, and a dark-mode QR is a QR half the phones give up on --
   so it sits on its own white card rather than on the dialog's surface. */
.sh-sub { margin: 0 0 14px; color: var(--ink-2); font-size: 13.5px; line-height: 1.6; }
.sh-qr {
  display: flex; justify-content: center; margin: 0 0 16px;
  padding: 12px; border-radius: var(--r-lg); background: #fff;
  border: 1px solid var(--line);
}
.sh-qr svg { display: block; width: min(232px, 60vw); height: auto; }
.sh-toolong { margin: 24px 12px; color: var(--ink-2); font-size: 13px; text-align: center; }
#share-url { font-family: var(--mono); font-size: 12.5px; }
.sh-note { margin: 12px 0 0; color: var(--ink-3); font-size: 12.5px; }
.sh-mail { margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--line); }
.sh-mailrow { display: flex; gap: 8px; }
.sh-mail .field { margin-bottom: 0; }
.sh-mailrow input {
  flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: var(--surface-2); color: var(--ink); font: inherit;
}
.sh-mailrow .btn { flex: none; }
.sh-mailnote { margin: 8px 0 0; color: var(--ink-3); font-size: 12px; line-height: 1.55; }
.sh-mailnote:empty, .sh-mailmsg:empty { display: none; }
.sh-mailmsg { margin: 8px 0 0; font-size: 13px; color: var(--ok, #1a7f37); }
.sh-mailmsg.err { color: var(--err, #b42318); }
#share { max-width: 460px; }

/* The console's assistant floats over the bottom-right corner, so every page
   keeps a little room under its last card. */
#app { padding-bottom: 72px; }
.repbody .card {
  background: var(--surface-2); border: 0; border-bottom: 1px solid var(--line);
  border-radius: 0; margin: 0; padding: 16px 18px; color: var(--ink);
}
.repbody .card:last-child { border-bottom: 0; }
.repbody .sub { color: var(--ink-3); font-size: 12.5px; margin: 0 0 14px; }
.repbody .tiles { display: flex; flex-wrap: wrap; gap: 10px; }
.repbody .tile {
  flex: 1 1 110px; border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px;
  background: var(--surface);
}
.repbody .tile b { display: block; font-size: 20px; }
.repbody .tile span { font-size: 12px; color: var(--ink-2); }
.repbody td.n, .repbody th.n { text-align: right; font-variant-numeric: tabular-nums; }
.repbody .q { margin: 0 0 6px; padding: 7px 10px; background: var(--surface); border-radius: var(--r-md); }
.repbody .chip { margin: 0 5px 5px 0; }
.repbody .foot { display: none; }
@media (max-width: 760px) { .glimits { grid-template-columns: 1fr; } }

/* Side-by-side comparison. The tray is the only thing on screen that says how
   many are ticked, so it stays put above the corner the assistant lives in. */
.cmptray {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 18px; z-index: 55;
  display: flex; align-items: center; gap: 10px; max-width: min(92vw, 860px);
  padding: 10px 14px; border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 10px 34px rgba(0, 0, 0, .16);
}
.cmptray .cmpcount { font-size: 13px; color: var(--ink-2); white-space: nowrap; }
/* On a phone the tray is as wide as the screen and the two corner pills are
   directly below it, so it moves up by their height instead of landing on them. */
@media (max-width: 640px) {
  .cmptray {
    left: 12px; right: 12px; transform: none; max-width: none;
    bottom: calc(14px + 52px + env(safe-area-inset-bottom));
    padding: 9px 11px; gap: 8px;
  }
}
.cmpchips { display: flex; gap: 6px; overflow: auto; }
.cmpchip {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: 4px 10px; border-radius: var(--r-pill); font: inherit; font-size: 12.5px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  cursor: pointer;
}
.cmpchip:hover { border-color: var(--bad); color: var(--bad); }
.cmpbtn.on, .pacts .cmpbtn.on {
  border-color: var(--brand); color: var(--brand); font-weight: 560;
  background: color-mix(in srgb, var(--brand) 10%, transparent);
}
.cmpbtn.on::before { content: '✓ '; }
.cmponly { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-2); }
.cmpai {
  margin: 0 0 16px; padding: 14px; border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--line);
}
.cmpaihead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cmpaihead b { font-size: 14px; }
.cmpaisub { flex: 1 1 200px; font-size: 12.5px; color: var(--ink-2); }
.cmpaibusy, .cmpaierr { margin: 8px 0 0; font-size: 13px; color: var(--ink-2); }
.cmpaierr { color: var(--bad); }
.cmpaisum { margin: 10px 0 0; font-size: 13.5px; line-height: 1.6; }
.cmpaigrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-top: 12px; }
.cmpaicard {
  padding: 10px 12px; border-radius: var(--radius); background: var(--surface);
  border: 1px solid var(--line);
}
.cmpaicard b { font-family: var(--mono); font-size: 12px; color: var(--brand); }
.cmpaicard p { margin: 5px 0 0; font-size: 13px; line-height: 1.55; }
.cmpaicard .watch { color: var(--warn); }
.cmpaiask { margin-top: 12px; font-size: 13px; }
.cmpaiask span { color: var(--ink-2); }
.cmpaiask ul { margin: 4px 0 0; padding-left: 20px; }
.cmpainote { margin: 12px 0 0; font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }
.cmpwrap { overflow: auto; }
.cmptable { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.cmptable th, .cmptable td {
  padding: 9px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top;
}
.cmptable thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
.cmptable thead th .sku { display: block; font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.cmptable .cmprow { width: 26%; color: var(--ink-2); font-weight: 500; background: var(--surface); }
.cmptable tr.same td, .cmptable tr.same .cmprow { color: var(--ink-3); }
.cmptable tr.diff td { font-weight: 600; }
.cmptable td.best { color: var(--brand); }
.cmptable td.best::after { content: ' ▲'; font-size: 10px; vertical-align: 2px; }
/* The remove button used to float, so on a column whose name wrapped it fell
   to the bottom-right corner of the cell, level with nothing. It is part of
   the heading, so it sits in the heading's own row. */
.cmphd { display: flex; align-items: flex-start; gap: 8px; }
.cmpnm { flex: 1 1 auto; min-width: 0; }
.cmpdrop {
  flex: none; border: 0; background: none; color: var(--ink-3); cursor: pointer;
  font: inherit; font-size: 12px; line-height: 1;
  width: 22px; height: 22px; display: grid; place-items: center;
  border-radius: var(--r-sm); margin: -2px -4px 0 0;
}
.cmpdrop:hover { background: var(--bad-soft); }
.cmpdrop:hover { color: var(--bad); }
.cmpnote { margin: 12px 0 0; font-size: 12px; color: var(--ink-3); }
@media (max-width: 640px) {
  .cmptray { left: 12px; right: 12px; transform: none; max-width: none; }
  .cmptable .cmprow { width: 38%; }
}
.ubar .ul { font-size: 13px; }
.ubar .us { color: var(--ink-3); font-size: 11.5px; }
@media (max-width: 900px) {
  .utiles { grid-template-columns: repeat(2, 1fr); }
  .grid2cards { grid-template-columns: 1fr; }
}
.enqtabs .n { opacity: .6; font-variant-numeric: tabular-nums; }
.enqst.new { border-color: color-mix(in srgb, var(--brand) 40%, var(--line));
  color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, transparent); }
.enqst.closed { opacity: .75; }
.enqwork .field.wide { grid-column: 1 / -1; }
@media (max-width: 560px) { .enqwork { grid-template-columns: 1fr; } }

dialog .dlg-foot { padding: 14px 20px; border-top: 1px solid var(--line); display: flex; gap: 8px; justify-content: flex-end; }

/* ---------------------------------------------------------------- the gate */

/* The front door is a surface of its own, dark in both themes. On a light
   system the gate used to be a near-white full-bleed page, which reads as an
   empty document rather than as a front door -- and the photograph, held down
   to 7% so it would not smear, was invisible. These tokens are scoped to
   .gate, so only the front door changes: everything the visitor works in
   afterwards still follows their own light or dark setting. */
.gate {
  --bg: #0d141f;
  --surface: #18202c;
  --surface-2: #1e2836;
  --line: #2b3644;
  --line-soft: #232d3a;
  --ink: #eef1f5;
  --ink-2: #a8b3c1;
  --ink-3: #7f8b99;
  --brand: #4aa3e0;
  --brand-ink: #04202f;
  --brand-soft: #16303f;
  --brand-lift: #79bde8;
  --shadow: 0 1px 2px rgba(0,0,0,.45), 0 20px 44px -26px rgba(0,0,0,.95);
  color-scheme: dark;
  position: fixed; inset: 0; z-index: 120; overflow: auto;
  color: var(--ink);
  background:
    radial-gradient(ellipse 110% 78% at 50% -12%,
      #17273f 0%, rgba(23,39,63,0) 62%),
    linear-gradient(180deg, #101a28 0%, #0a1018 100%);
  display: grid; place-items: center; padding: 40px 20px;
  isolation: isolate;
}
/* The front door carries a picture of what these products are for; the working
   screens stay plain. Two layers keep it a background rather than a picture:
   the photograph at a low opacity, and a wash of the page colour poured over
   the middle so the wordmark, the heading and the cards sit on clean ground
   and the image survives only around the edges. Both are fixed to the viewport
   so scrolling a narrow window does not drag the image with the content. */
.gate::before, .gate::after {
  content: ""; position: fixed; inset: 0; pointer-events: none;
}
.gate::before {
  background: url("bg.jpg") center / cover no-repeat;
  opacity: .3; filter: saturate(.7) contrast(.94);
  z-index: -2;
}
.gate::after {
  background: radial-gradient(ellipse 76% 70% at 50% 44%,
    var(--bg) 0%, color-mix(in srgb, var(--bg) 84%, transparent) 50%,
    color-mix(in srgb, var(--bg) 34%, transparent) 100%);
  z-index: -1;
}

.gate-inner { width: 100%; max-width: 900px; text-align: center; position: relative; }

/* A way into the admin console from the front door, so nobody has to remember
   the /admin path. Quiet on purpose: it is for the two people who maintain the
   catalog, not for the visitor the page is built around. */
/* Both front-door controls sit together in the top right: the language the
   page is read in, and the way into the console for whoever maintains it. */
.gate-top {
  position: absolute; top: 18px; right: 20px; z-index: 2;
  display: flex; align-items: center; gap: 10px;
}
.gate-top .langsel {
  background-color: color-mix(in srgb, var(--surface) 70%, transparent);
  backdrop-filter: blur(6px);
}
.gate-admin {
  font-size: 13px; color: var(--ink-3); text-decoration: none;
  padding: 7px 13px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  backdrop-filter: blur(6px);
}
.gate-admin:hover, .gate-admin:focus-visible {
  color: var(--ink); border-color: var(--ink-3); outline: none;
}
.gate-logo { width: 132px; height: auto; margin: 0 auto 26px; display: block; }
.gate-inner h1 { margin: 0 0 8px; font-size: 26px; letter-spacing: -.02em; }
.gate-inner > p, .gate-inner p { margin: 0 0 30px; color: var(--ink-2); font-size: 14.5px; }
.gate-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.gate-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
  padding: 24px 22px 20px; text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--surface);
  color: inherit; font: inherit; box-shadow: var(--shadow);
  transition: border-color .15s, transform .15s;
}
.gate-card:hover, .gate-card:focus-visible {
  border-color: var(--brand); transform: translateY(-2px); outline: none;
}
.gate-icon {
  font-size: 26px; line-height: 1; color: var(--brand);
  display: block; height: 34px;
}
.gate-icon svg.art-line { width: 38px; height: 34px; }
/* The line marks follow the brand colour, like the glyphs they replace. */
svg.art-line .a-fill { fill: currentColor; }
svg.art-line .a-stroke { stroke: currentColor; }
/* Dots punched out of the filled die, so they read at 38px. */
svg.art-line .a-bg-dot { fill: var(--surface); }
/* In the top bar the mark sits inline with the line's name. svg.art is a block
   by default, which would put the mark on a line of its own and make the whole
   header tall enough to wrap. */
.seg svg.art-line {
  display: inline-block; width: 16px; height: 15px;
  vertical-align: -2px; margin-right: 5px;
}
.gate-card strong { font-size: 17px; letter-spacing: -.01em; }
.gate-help { font-size: 13px; color: var(--ink-2); line-height: 1.6; flex: 1; }
.gate-go {
  margin-top: 10px; font-size: 13px; font-weight: 560; color: var(--brand);
}
.gate-card:hover .gate-go::after, .gate-card:focus-visible .gate-go::after { content: " \2192"; }
/* Nothing behind the gate should be reachable while it is up. */
body.gated { overflow: hidden; }

/* ----------------------------------------------------------- the tour button

   Bottom right, above everything, out of the reading path. It hides whenever
   it would be in the way -- while the gate is up, while the tour it starts is
   running, and on paper. */
/* It used to share the bottom-right corner with the assistant launcher, which
   is draggable, so on a phone the two pills sat on top of each other. They get
   a corner each: the assistant is the one people reach for, so it keeps the
   right, and the tour takes the left. */
.tourfab {
  position: fixed; z-index: 55;
  left: max(20px, env(safe-area-inset-left));
  bottom: max(20px, env(safe-area-inset-bottom));
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--surface);
  box-shadow: 0 10px 26px -8px rgb(0 0 0 / .45), 0 2px 6px rgb(0 0 0 / .18);
  transition: transform .15s, border-color .15s;
}
.tourfab:hover, .tourfab:focus-within { border-color: var(--brand); transform: translateY(-2px); }
.tourfab button {
  border: 0; background: transparent; color: var(--ink);
  font: inherit; font-size: 13.5px; font-weight: 560; cursor: pointer;
}
.tourfab-go {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 6px 10px 14px; border-radius: var(--r-pill) 0 0 var(--r-pill);
}
.tourfab-i { color: var(--brand); font-size: 15px; line-height: 1; }
/* The way out is part of the thing itself: no hunting in a settings page for
   how to stop something that is floating over your work. */
.tourfab-x {
  padding: 10px 13px 10px 8px; border-radius: 0 var(--r-pill) var(--r-pill) 0;
  color: var(--ink-3); font-size: 16px; line-height: 1;
}
.tourfab-x:hover { color: var(--ink); }
.tourfab-go:focus-visible, .tourfab-x:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
body.gated .tourfab { display: none; }

/* On a phone the label goes: down there it would sit across a product card,
   and the mark plus its aria-label says enough. */
@media (max-width: 640px) {
  .tourfab { left: 14px; bottom: 14px; }
  .tourfab #tourfab-l { display: none; }
  /* Two halves in one small pill: each needs a finger's worth of width, or
     "start the tour" and "never show this again" are one slip apart. */
  .tourfab button { min-width: 42px; min-height: 44px; justify-content: center; }
  .tourfab-go { padding: 0; gap: 0; }
  .tourfab-x { padding: 0; font-size: 17px; }
  .tourfab-i { font-size: 17px; }
  /* A hairline between them, so the two targets read as two. */
  .tourfab-x { border-left: 1px solid var(--line); }
}

@media print { .tourfab { display: none; } }

/* ------------------------------------------------------ contact sales form */

#contact { max-width: 620px; }
.ct-sub { margin: 0 0 12px; font-size: 13.5px; color: var(--ink-2); line-height: 1.6; }
.ct-what { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 16px; }
.ct-note { margin: 12px 0 0; font-size: 12px; color: var(--ink-3); }
#contact textarea {
  width: 100%; padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); color: inherit; font: inherit; resize: vertical;
}
#contact textarea:focus, #contact input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
/* The honeypot has to be reachable by a naive bot and invisible to a person:
   off-screen rather than display:none, which some bots skip. */
.ct-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Completeness: a model is usable once its required fields are in. */
.needs {
  margin-left: 7px; font-size: 10.5px; padding: 1px 7px; border-radius: var(--r-pill);
  background: var(--warn-soft); color: var(--warn); white-space: nowrap;
}
.corebar.done { background: var(--ok-soft); color: var(--ok); }
.tally.ok { border-color: var(--ok); color: var(--ok); background: var(--ok-soft); }
.tally.bad { border-color: var(--bad); color: var(--bad); background: var(--bad-soft); }
.tag-add, .tag-update, .tag-delete, .tag-error {
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 8px; border-radius: var(--r-pill); white-space: nowrap;
}
.tag-add { background: var(--ok-soft); color: var(--ok); }
.tag-update { background: var(--brand-soft); color: var(--brand); }
.tag-delete { background: var(--bad-soft); color: var(--bad); }
.tag-error { background: var(--warn-soft); color: var(--warn); }

/* -------------------------------------------------------------- sign-in */

.login { max-width: 400px; margin: 8vh auto; padding: 24px; }
.login h2 { margin: 0 0 14px; font-size: 17px; }
.btn.wide { width: 100%; justify-content: center; margin-top: 8px; }
.rcpt-groups label.on { border-color: var(--brand); background: var(--brand-soft); color: var(--brand); }
.rcpt-groups label.on .rcpt-gc { color: var(--brand); border-color: currentColor; }
.urlover .field { margin-bottom: 6px; }
.skuadd { display: flex; gap: 8px; margin-bottom: 10px; }
.skuadd input { flex: 1; padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); color: inherit; }
table.grid input[type="email"] {
  width: 100%; padding: 5px 8px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); color: inherit; font-size: 13px;
}
.dn.on {
  border-left-color: var(--brand); background: var(--brand-soft);
  color: var(--brand); font-weight: 560;
}
.dn.on .dn-i { color: var(--brand); }
.dn-n.none { opacity: .5; }
.dn.on .dn-n { color: var(--brand); }
.dlgsec, .attrgroup, .dlgsec-h { scroll-margin-top: 12px; }

/* Too narrow for two columns: the contents run across the top instead. */
@media (max-width: 820px) {
  .dlgsplit { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .dlgtoc {
    position: static; margin: 0; padding: 0 0 12px;
    flex-direction: row; flex-wrap: wrap; gap: 6px;
    border-bottom: 1px solid var(--line);
  }
  .dlgtoc h4 { width: 100%; margin-left: 0; }
  .dn {
    width: auto; border-left: 0; border-radius: var(--r-pill);
    border: 1px solid var(--line); background: var(--surface-2); padding: 4px 10px;
  }
  .dn.on { border-color: var(--brand); }
  .dn-act { margin: 0; }
  .dlgmain { border-left: 0; padding-left: 0; }
}

/* Rows in the admin tables open their editor; the buttons at the end still do
   their own thing. */
table.grid tr.rowlink { cursor: pointer; }
table.grid tr.rowlink:focus-visible {
  outline: 2px solid var(--brand); outline-offset: -2px;
}
.seg.kindbar { flex-wrap: wrap; width: fit-content; max-width: 100%; }
.seg.kindbar button { font-size: 12.5px; }

/* colour-coded against what the automatic slot rules say */
.cardset-grp label.fit code,
.cardset-grp label.fit .cs-name { color: var(--ok); }
.cardset-grp label.nofit code,
.cardset-grp label.nofit .cs-name { color: var(--bad); opacity: .85; }
.cs-key.fit::before { background: var(--ok); }
.cs-key.nofit::before { background: var(--bad); }

.toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: var(--ink); color: var(--bg); padding: 9px 16px; border-radius: var(--r-pill);
  font-size: 13.5px; box-shadow: var(--shadow); z-index: 90;
}
.muted { color: var(--ink-3); }
.err { color: var(--bad); font-size: 13px; }
table.grid td:has(> .rowbtns) { white-space: nowrap; width: 1%; }

/* The picture the selector will use for this model, at list size. The column
   is as wide as the square and no wider. */
table.grid th.thumbh, table.grid td.thumbc { width: 1%; padding-right: 0; }
/* No picture, or an address that no longer serves one. Dashed rather than
   blank, because an empty cell reads as "nothing to say here" and this cell
   has something to say. */
.thumb.none {
  border-style: dashed; background: var(--surface-2);
  border-color: var(--line);
}
.thumb.none::after {
  content: ""; width: 14px; height: 11px;
  border: 1.5px solid var(--ink-3); border-radius: 2px; opacity: .45;
}
.thumb.none.broken { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); }
.thumb.none.broken::after { border-color: var(--warn); opacity: .8; }

/* ------------------------------------------------------------------ print
   Paper gets its own document (#printsheet, built by renderPrintSheet): the
   answers as text, then the sizing derivation, then the products as tables.
   Everything the screen uses is hidden rather than reflowed. */

#printsheet { display: none; }

/* A printed sheet loses the watermark -- browsers do not print background
   images -- so on paper the warning is a band of words at the top instead. It
   is the first thing on the page for the same reason it is the first thing on
   screen: somebody who reads only the header has still read it. */
.ps-test {
  border: 2px solid #000; padding: 6pt 8pt; margin-bottom: 8pt;
  font-weight: 700; font-size: 9.5pt; line-height: 1.4; text-align: center;
}

@media print {
  @page { size: A4; margin: 14mm 12mm 12mm; }

  body { background: #fff; color: #000; }
  body > *, dialog { display: none !important; }
  #printsheet {
    display: block !important; font-size: 10pt; line-height: 1.45;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  .ps-top {
    display: flex; align-items: flex-start; gap: 14px;
    border-bottom: 1.5pt solid #000; padding-bottom: 8px; margin-bottom: 14px;
  }
  .ps-logo { width: 108px; height: auto; flex: none; margin-top: 2px; }
  .ps-title { flex: 1 1 auto; }
  .ps-title h1 { margin: 0; font-size: 15pt; letter-spacing: -.01em; }
  .ps-meta { margin-top: 3px; font-size: 8.5pt; color: #444; }
  /* Configurator builds. */
  .ps-ok, .ps-bad { margin-left: 8px; font-size: 8pt; font-weight: 600; }
  .ps-ok::before { content: "✓ "; }
  .ps-bad::before { content: "✗ "; }
  .ps-build tbody th { width: 80%; }

  /* The QR back to this result, beside the sign-off. */
  .ps-qr { flex: none; width: 24mm; text-align: center; }
  .ps-qr svg { display: block; width: 22mm; height: 22mm; margin: 0 auto 2px; }
  .ps-qr span { display: block; font-size: 6.5pt; line-height: 1.25; color: #444; }
  .ps-sign { flex: none; width: 46mm; font-size: 8pt; color: #444; }
  .ps-sign div { border-bottom: .6pt solid #666; padding: 2px 0 12px; }
  .ps-sign div + div { margin-top: 6px; }

  .ps-h {
    display: flex; align-items: baseline; gap: 8px;
    margin: 16px 0 7px; font-size: 12pt; break-after: avoid;
    border-bottom: .8pt solid #000; padding-bottom: 4px;
  }
  .ps-h:first-of-type { margin-top: 0; }
  /* Outlined, not filled: browsers print backgrounds only when the user ticks
     "background graphics", and a white numeral on a dropped black disc would
     vanish. A border always prints. */
  .ps-n {
    flex: none; width: 16px; height: 16px; border-radius: 50%;
    border: .8pt solid #000; color: #000; font-weight: 700;
    font-size: 8pt; line-height: 15px; text-align: center;
  }
  .ps-h small { margin-left: auto; font-weight: 400; font-size: 8.5pt; color: #555; }
  .ps-h3 {
    margin: 11px 0 5px; font-size: 10pt; break-after: avoid;
    padding-left: 7px; border-left: 2.5pt solid #444;
  }

  /* Horizontal rules only. A full grid of hairlines is unreliable on paper:
     print pipelines snap 1px lines to the device grid, and the vertical ones
     land between pixels and disappear while the horizontal ones survive, which
     is what made the earlier grid look half-drawn. Rules are specified in pt
     and dark enough to render on any printer; columns are separated by space
     instead of lines. */
  .ps-tbl {
    width: 100%; border-collapse: collapse; margin-bottom: 8px;
    font-size: 9pt; break-inside: auto;
  }
  .ps-tbl thead th {
    padding: 4px 10px 4px 0;
    border-top: 1pt solid #000; border-bottom: .6pt solid #000;
    text-align: left; font-size: 8pt; font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em;
  }
  .ps-tbl tbody th, .ps-tbl tbody td {
    padding: 4px 10px 4px 0; border-bottom: .5pt solid #8a8a8a;
    text-align: left; vertical-align: top;
  }
  .ps-tbl tbody tr:last-child > * { border-bottom: 1pt solid #000; }
  /* Tables built without a head still need an opening rule. */
  .ps-bare tbody tr:first-child > * { border-top: 1pt solid #000; }
  .ps-tbl thead th:last-child,
  .ps-tbl tbody th:last-child, .ps-tbl tbody td:last-child { padding-right: 0; }
  .ps-tbl tbody th { font-weight: 600; }
  .ps-tbl tbody tr { break-inside: avoid; }
  .ps-tbl tbody th small {
    display: block; font-weight: 400; font-size: 7.5pt; color: #555; margin-top: 1px;
  }
  /* A repeated head keeps a table that spans pages readable. */
  .ps-tbl thead { display: table-header-group; }
  .ps-num { text-align: right !important; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .ps-url {
    display: block; margin-top: 1px; font-weight: 400; font-size: 7.5pt;
    color: #555 !important; text-decoration: none;
  }
  .ps-muted { color: #666; font-size: 9pt; }
  .ps-off th, .ps-off td { color: #777; }

  .ps-cond th:first-child { width: 40%; }
  .ps-calc th:first-child { width: 60%; }
  .ps-prod td.ps-num:first-child { width: 6mm; }
  .ps-prod tbody th { width: 31%; }
  .ps-prod tbody td.ps-num { width: 12mm; }
  .ps-note { font-size: 8pt; color: #333; }

  .ps-why { margin: 8px 0 10px; break-inside: avoid; }
  .ps-why h4 { margin: 0 0 4px; font-size: 9pt; }
  .ps-r-fail > * { background: #f0f0f0; }
  .ps-r-fail th::after { content: " ✗"; }
  .ps-r-pass th::after { content: " ✓"; }

  .ps-warn { margin: 0 0 8px; padding-left: 16px; font-size: 9pt; }
  .ps-warn li { margin-bottom: 2px; break-inside: avoid; }

  /* 展場模式: boxes to write the visitor's details in by hand -- sized for a
     pen (a line is about 12 mm), drawn with borders because borders always
     print, and kept on one page. */
  .ps-booth { break-inside: avoid; margin-bottom: 16px; }
  .pb-grid {
    display: grid; grid-template-columns: 1fr 1fr;
    border-top: 1pt solid #000; border-left: .8pt solid #000;
  }
  .pb-box {
    position: relative; min-height: 15mm; padding: 3px 6px;
    border-right: .8pt solid #000; border-bottom: .8pt solid #000;
  }
  .pb-box > span { font-size: 7.5pt; font-weight: 700; color: #333; letter-spacing: .02em; }
  .pb-wide { grid-column: 1 / -1; }
  /* The needs box: ruled lines to write along. Elements with borders, not a
     background, because backgrounds print only when the user asks. */
  .pb-lines i { display: block; height: 11mm; border-bottom: .5pt solid #9a9a9a; }
  .pb-lines i:last-child { border-bottom: 0; }
  .pb-next { min-height: 13mm; }
  .pb-next > div { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 5px; font-size: 10pt; }
  .pb-tick { white-space: nowrap; }

  .ps-foot {
    margin-top: 16px; padding-top: 6px; border-top: .5pt solid #8a8a8a;
    font-size: 7.5pt; color: #666; word-break: break-all;
  }
}
.linetick .tick {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; padding: 5px 9px; border: 1px solid var(--line);
  border-radius: var(--r-md); cursor: pointer;
}
.linetick .tick:has(input:disabled) { opacity: .5; cursor: default; }
.linetick .tick input { margin: 0; }
.visstate.on { color: var(--ok); }
.visstate.off { color: var(--warn); }
.visbox:has(.visstate.off) { border-left-color: var(--warn); }

.chip.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); background: var(--ok-soft); }
.chip.off { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); background: var(--warn-soft); }
.aiform .btn { height: 38px; }
@media (max-width: 900px) { .aiform { grid-template-columns: 1fr; } }
/* A field the model proposed, so a reviewer can tell it from one that was
   already there or is still empty. */
.field.aifill > span::after {
  content: "AI"; margin-left: 6px; font-size: 9.5px; letter-spacing: .06em;
  padding: 1px 5px; border-radius: var(--r-pill); vertical-align: 1px;
  color: var(--brand); background: var(--brand-soft);
  border: 1px solid color-mix(in srgb, var(--brand) 35%, transparent);
}
.field.aifill input, .field.aifill select, .field.aifill textarea {
  border-color: color-mix(in srgb, var(--brand) 45%, var(--line));
}

/* A field somebody else's job function owns: visible, because the value is
   often what you need in order to fill in your own fields, and plainly inert
   rather than mysteriously unresponsive. */
.field.locked > span, .field.locked > span * { color: var(--ink-3); }
.field.locked input, .field.locked select, .field.locked textarea,
.field.locked .multibox, .field.locked .cardset {
  background: var(--surface-2); color: var(--ink-2); cursor: not-allowed;
  border-style: dashed;
}
.field.locked .multibox label, .field.locked .cardset label { color: var(--ink-3); }

/* ------------------------------------------------------------------- hints

   A "?" next to the thing it explains. On a device that can hover it opens a
   popover beside the badge; on a touch device the same content rises from the
   bottom of the screen, where a thumb can reach it and a stray tap outside
   dismisses it. */
.hint {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 6px; padding: 0; vertical-align: -2px;
  border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--surface-2);
  color: var(--ink-3); font-size: 10.5px; font-weight: 700; line-height: 1;
  cursor: help; flex: none;
}
.hint:hover, .hint:focus-visible {
  color: var(--brand); border-color: var(--brand); outline: none;
  background: var(--brand-soft);
}
/* On a phone the badge is a tap target, so it gets a tap target's size. */
@media (hover: none) {
  .hint { width: 22px; height: 22px; font-size: 13px; cursor: pointer; }
}

.hintpop {
  position: fixed; z-index: 200; width: min(360px, calc(100vw - 24px));
  padding: 14px 16px 16px; border-radius: var(--r-lg);
  box-shadow: 0 1px 2px rgba(16,24,40,.08), 0 18px 40px -18px rgba(16,24,40,.5);
  opacity: 0; transform: translateY(-4px); transition: opacity .12s, transform .12s;
}
.hintpop.on { opacity: 1; transform: none; }
.hintpop h4, .hintsheet-card h4 { margin: 0 0 6px; font-size: 14px; }
.hintpop p, .hintsheet-card p { margin: 0; font-size: 12.5px; line-height: 1.65; color: var(--ink-2); }

.hint-art {
  margin: -2px 0 12px; padding: 10px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line-soft);
}
.art-hint { display: block; width: 100%; height: auto; color: var(--ink-2); }
.h-box { fill: none; stroke: var(--ink-3); stroke-width: 2; }
.h-line { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.h-fill { fill: color-mix(in srgb, var(--ink) 45%, transparent); }
.h-fill-soft { fill: color-mix(in srgb, var(--ink-3) 22%, transparent); }
.h-fill-brand { fill: var(--brand); }
.h-fill-ok { fill: var(--ok); }
.h-brand { stroke: var(--brand); }
.h-ok { stroke: var(--ok); }
.h-bad { stroke: var(--bad); }
.h-mute { stroke: color-mix(in srgb, var(--ink-3) 60%, transparent); }
.h-dash { stroke-dasharray: 5 4; }

/* The touch half: a sheet, not a popover. A tooltip anchored to a 22px badge
   on a 390px screen has nowhere to go; the bottom of the screen always does. */
.hintsheet { position: fixed; inset: 0; z-index: 220; }
.hintsheet-back {
  position: absolute; inset: 0; background: rgba(8, 12, 18, .45);
  opacity: 0; transition: opacity .18s;
}
.hintsheet.on .hintsheet-back { opacity: 1; }
.hintsheet-card {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: 82vh; overflow: auto; padding: 8px 18px 20px;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  transform: translateY(100%); transition: transform .22s ease;
  padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.hintsheet.on .hintsheet-card { transform: none; }
.hintsheet-grip {
  width: 38px; height: 4px; border-radius: var(--r-pill); background: var(--line);
  margin: 6px auto 14px;
}
.hintsheet-close { width: 100%; margin-top: 16px; justify-content: center; }
body.hint-open { overflow: hidden; }

/* On a phone the console's tables are wider than the screen, and a page wider
   than the screen makes the browser zoom the whole thing out to fit -- which
   is why the console used to arrive shrunk, and why anything positioned
   against the viewport (the hint sheet, for one) landed in the wrong place.
   Each table scrolls inside its own box instead, and the page stays 390px. */
@media (max-width: 720px) {
  #app table.grid { display: block; overflow-x: auto; white-space: nowrap; }
  #app table.grid td, #app table.grid th { white-space: nowrap; }
  .srccell { max-width: 42vw; }
}

/* ------------------------------------------------ the console on a phone

   The tab strip is a dozen controls in one row: the line picker, eight tabs,
   who you are signed in as, and two buttons. At 390px that row was 752px
   wide, and a page wider than the screen makes the browser zoom the whole
   document out to fit -- which is why the console arrived at about half size,
   with the tab labels set one character per line. Nothing here is new
   behaviour; it is the row learning to fold. */
/* Scoped to #app, which only the console has: none of this may reach the
   selector, whose own phone layout was worked out separately. */
@media (max-width: 720px) {
  #app .tabs {
    flex-wrap: wrap; gap: 2px 4px; row-gap: 6px;
    padding-bottom: 2px; align-items: center;
  }
  /* The identity block goes to its own row, so the tabs keep theirs. */
  #app .tabs .spacer { flex-basis: 100%; height: 0; }
  #app .tabs button { padding: 8px 10px; font-size: 13px; white-space: nowrap; }
  #app .tabgroup.on .tabbtn .gnow { display: none; }
  #app .tabmenu { min-width: 150px; }
  #app .whoami { font-size: 12px; }
  #app .linepick { max-width: 46vw; }
  /* Two-column forms are one column at this width. */
  #app .grid2, #app .grid3, dialog .grid2, dialog .grid3 {
    grid-template-columns: minmax(0, 1fr);
  }
  #app .card { padding-left: 12px !important; padding-right: 12px !important; }
}

/* The "?" inside an option button. It is a separate control and has to look
   like one, without becoming the loudest thing on a card whose whole job is to
   be chosen -- so it stays quiet until the pointer is on it. It sits inside
   the label line, which keeps the option's own layout untouched. */
.opt-card .hint, .pill .hint, .bq h3 .hint { vertical-align: 0; }
.pop h2 .hint, .card.q h2 .hint { margin-left: 8px; vertical-align: 3px; }

/* Shown through the popover API, these are top-layer elements: the browser
   gives them its own default box, which has to be cleared for the card and the
   sheet to look like themselves. */
.hintpop:popover-open, .hintsheet:popover-open {
  margin: 0; padding: 0; border: 0; background: none; overflow: visible;
}
.hintpop:popover-open { padding: 14px 16px 16px; background: var(--surface); border: 1px solid var(--line); }
.hintsheet:popover-open { width: 100%; height: 100%; max-width: none; max-height: none; }
.linemark svg.art-line { width: 17px; height: 16px; display: block; }

/* A link's verdict sits under the field it belongs to: green when the address
   answers, amber when the far end refuses robots but the link is probably
   fine, red when it is actually broken. */
.urlf .field { margin-bottom: 2px; }
.urlstate.ok { color: var(--ok); }
.urlstate.bad { color: var(--bad); }
.urlstate.busy { color: var(--ink-3); }

/* The conditions above narrow by what a product must do; these narrow by what
   it is. One row, wrapping, scrolling sideways on a phone rather than growing
   four rows tall. */
.catbar {
  display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 14px;
}
.catchip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2);
  font: inherit; font-size: 12.5px; line-height: 1.2; cursor: pointer;
}
.catchip:hover { border-color: var(--ink-3); color: var(--ink); }
.catchip .n { color: var(--ink-3); font-variant-numeric: tabular-nums; font-size: 11.5px; }
.catchip.on {
  border-color: var(--brand); background: var(--brand-soft); color: var(--brand);
  font-weight: 600;
}
.catchip.on .n { color: color-mix(in srgb, var(--brand) 70%, var(--ink)); }
@media (max-width: 640px) {
  .catbar {
    flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .catbar::-webkit-scrollbar { display: none; }
  .catchip { flex: 0 0 auto; }
}

/* Answering a single-answer question brings the next one up. A list that
   silently moved under you is disorienting, so the one now being asked is
   marked for a moment. */
@keyframes justnow {
  from { box-shadow: 0 0 0 3px var(--brand-soft); }
  to { box-shadow: 0 0 0 3px transparent; }
}
.card.q.justnow, .bq.justnow { animation: justnow 1.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .card.q.justnow, .bq.justnow { animation: none; box-shadow: 0 0 0 3px var(--brand-soft); }
}

/* A question that takes several answers has no natural end, so it says when
   it is done rather than guessing. */
.qnext {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 10px; padding: 6px 12px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2);
  font: inherit; font-size: 12.5px; line-height: 1.2; cursor: pointer;
}
.qnext::after { content: '→'; color: var(--ink-3); }
.qnext:hover { border-color: var(--brand); color: var(--brand); }
.qnext:hover::after { color: var(--brand); }
.calcbar .spacer { flex: 1 1 auto; }
.calcstate.ok { color: var(--ok); }
.calcstate.bad { color: var(--bad); }

.calcpal { border-right: 1px solid var(--line); padding: 12px 11px; }
.calcpal h5 {
  margin: 0 0 9px; font-size: 11px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-3);
}
.calcpal .pcard {
  display: flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left;
  padding: 6px 8px; margin-bottom: 5px; border-radius: var(--r-md); font: inherit;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink); cursor: pointer;
}
.calcpal .pcard:hover { border-color: var(--brand); }
.calcpal .pi {
  flex: 0 0 auto; width: 18px; height: 18px; border-radius: var(--r-sm); margin-top: 2px;
  display: grid; place-items: center; font-size: 10px; color: #fff;
  background: var(--ink-3); font-family: var(--mono);
}
/* The six step kinds have to be told apart at a glance, and they are told
   apart with Advantech's own colours rather than a second palette invented
   beside it: the divisional greens, blues and purple, plus the corporate
   orange for an output. */
.calcpal .pi.k-input { background: var(--adv-ia); }
.calcpal .pi.k-lookup, .calcpal .pi.k-pick { background: var(--adv-network); }
.calcpal .pi.k-band, .calcpal .pi.k-set { background: var(--adv-ecc); }
.calcpal .pi.k-expr, .calcpal .pi.k-round { background: var(--adv-blue); }
.calcpal .pi.k-output { background: var(--adv-orange); }
.calcpal .pi.k-warn { background: var(--adv-logistics); }
.calcpal .pcard b { display: block; font-size: 12px; font-weight: 600; line-height: 1.3; }
.calcpal .pcard span span { display: block; font-size: 10.5px; color: var(--ink-3); line-height: 1.35; }
.calcpal .phint { font-size: 11px; color: var(--ink-3); margin: 10px 0 0; line-height: 1.5; }
.cwire { fill: none; stroke: var(--ink-3); stroke-width: 1.5; opacity: .55; }
.cwire.hot { stroke: var(--brand); stroke-width: 2.2; opacity: 1; }
.cnode.sel { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.cnode.bad { border-color: var(--bad); }
.k-input .cngrip { background: var(--adv-ia); }
.k-lookup .cngrip, .k-pick .cngrip { background: var(--adv-network); }
.k-band .cngrip, .k-set .cngrip { background: var(--adv-ecc); }
.k-expr .cngrip, .k-round .cngrip { background: var(--adv-blue); }
.k-output .cngrip { background: var(--adv-orange); }
.k-warn .cngrip { background: var(--adv-logistics); }
.calcside .ihead .k { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
.calcside .ihead .id { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.calcside .trow input.k { flex: 0 0 70px; font-family: var(--mono); }
.calcside .trow input.v { flex: 1 1 auto; }
.calcside .x { border: 0; background: none; color: var(--ink-3); font: inherit; cursor: pointer; padding: 0 2px; }
.calcside .x:hover { color: var(--brand); }
.calcside .chips { display: flex; flex-wrap: wrap; gap: 4px; margin: -2px 0 8px; }
.calcside .chip {
  font-family: var(--mono); font-size: 10.5px; padding: 2px 7px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-2); cursor: pointer;
}
.calcside .chip:hover { border-color: var(--brand); color: var(--brand); }

.calctests { display: grid; grid-template-columns: 300px minmax(0, 1fr); border-top: 1px solid var(--line); }
@media (max-width: 780px) { .calctests { grid-template-columns: minmax(0, 1fr); } }
.calctests h5 {
  margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-3);
}
.calccases .tcase .m { font-family: var(--mono); font-weight: 700; color: var(--ink-3); }
.calccases .tcase.pass { border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.calccases .tcase.pass .m { color: var(--ok); }
.calccases .tcase.fail .m { color: var(--bad); }
.calccases .tcase .x { margin-left: auto; }
.flowbar .spacer { flex: 1 1 auto; }
.flowstate.ok { color: var(--ok); }
.flowstate.bad { color: var(--bad); }

.flowsplit { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
.fwire.ghost { stroke: var(--brand); stroke-dasharray: 5 4; stroke-width: 2; opacity: 1; }
.qnode.sel { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.qnode.bad { border-color: var(--bad); }
.qnode.drop { outline: 2px dashed var(--brand); outline-offset: 3px; }
.qnode.off { opacity: .55; }
.qnode.root .qgrip { background: var(--brand); }
.qid { display: block; font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.qwhen {
  padding: 5px 10px; border-top: 1px solid var(--line); background: var(--surface-2);
  font-size: 11px; color: var(--ink-2); border-radius: 0 0 var(--r-md) var(--r-md); line-height: 1.45;
}
.qwhen code { font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.qwhen.always { color: var(--brand); }
.qwhen.written { color: var(--warn); background: var(--warn-soft); }
.flowside .ihead .k { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
.flowside .ihead .id { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.flowside .inedge .x {
  border: 0; background: none; color: var(--ink-3); font: inherit; cursor: pointer; padding: 0 2px;
}
.flowside .inedge .x:hover { color: var(--brand); }
.canvasapp .calcsplit,
.canvasapp .flowsplit { flex: 1 1 auto; min-height: 0; }

/* The step palette collapses to a rail of marks. The names are worth having
   the first few times and in the way after that. */
/* The palette buttons borrow the product grid's .pcard, which is a column with
   overflow hidden. In a flex column that makes them shrink to fit the height
   and clip their own labels, so ten steps become ten unlabelled squares. They
   are a row of icon-then-name, and they do not shrink. */
.calcpal .pcard { flex: 0 0 auto; flex-direction: row; overflow: visible; }

/* Both side columns are `auto` grid tracks, so without a floor they give up
   their width to the drawing and the names in them wrap to nothing. */
.calcpal { width: 180px; min-width: 180px; display: flex; flex-direction: column; overflow: auto; }
.calcpal.rail { width: 48px; min-width: 48px; padding: 8px 6px; }
.calcpal.rail h5, .calcpal.rail .phint, .calcpal.rail .pcard span:not(.pi) { display: none; }
.calcpal.rail .pcard { justify-content: center; padding: 6px 0; }
.calcpal.rail .pi { margin-top: 0; width: 22px; height: 22px; font-size: 11px; }
@media (max-width: 1250px) {
  .calcside, .flowside { width: 300px; min-width: 300px; }
}
.calcsplit.noside .calcside, .flowsplit.noside .flowside { display: none; }
.calcside .field span, .flowside .field span { font-size: 11px; }

/* The pinned tests are a drawer: a line when shut, the cases when open. */
.calctests { flex: 0 0 auto; }
.drawerbar .sum b.ok { color: var(--ok); }
.drawerbar .sum b.bad { color: var(--bad); }
.calctests .panes { display: grid; grid-template-columns: 320px minmax(0, 1fr); max-height: 190px; overflow: auto; }
.calctests.shut .panes { display: none; }
@media (max-width: 860px) { .calctests .panes { grid-template-columns: minmax(0, 1fr); } }
body.noscroll main.shell { padding-bottom: 10px; }
body.noscroll .shell { max-width: 1840px; }
.cnode:hover .cardtools, .qnode:hover .cardtools,
.cnode.sel .cardtools, .qnode.sel .cardtools { opacity: 1; }
/* The kind chip moves aside for them rather than being pushed off the card. */
.cnode:hover .cnkind, .qnode:hover .qkind,
.cnode.sel .cnkind, .qnode.sel .qkind { display: none; }
.optrow .top { display: flex; align-items: center; gap: 6px; }
.optrow .top input {
  font: inherit; font-family: var(--mono); font-size: 11.5px; padding: 4px 6px; min-width: 0;
  flex: 1 1 auto; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
}
.optrow .mv, .optrow .x {
  border: 0; background: none; color: var(--ink-3); font: inherit; font-size: 12px;
  cursor: pointer; padding: 2px 4px; border-radius: var(--r-sm);
}
.optrow .x:hover { color: var(--bad); background: var(--surface); }
.optrow .labels { margin-top: 6px; display: grid; gap: 4px; }
.optrow .labels input {
  font: inherit; font-size: 11.5px; padding: 4px 6px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface); color: var(--ink);
}
.optrow textarea.set {
  margin-top: 6px; width: 100%; min-height: 40px; font-family: var(--mono); font-size: 11px;
  padding: 5px 6px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink); resize: vertical;
}
.seth .say {
  margin-left: auto; font: inherit; font-size: 11px; text-transform: none; letter-spacing: 0;
  padding: 2px 8px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.seth .say:hover { border-color: var(--brand); color: var(--brand); }
.seth .say:disabled { opacity: .6; cursor: default; }
.setrow .x {
  flex: 0 0 auto; border: 0; background: none; color: var(--ink-3);
  font: inherit; font-size: 12px; cursor: pointer; padding: 2px 4px; border-radius: var(--r-sm);
}
.setrow .x:hover { color: var(--bad); background: var(--surface); }
@media (max-width: 880px) { .vsnow { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 880px) {
  .vsbox { border-right: 0; border-bottom: 1px solid var(--line); }
}
.vsrow.live { background: color-mix(in srgb, var(--ok) 6%, transparent); }
.vstable .muted { font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }
.vstag.live { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.vsacts .btn { font-size: 11.5px; padding: 3px 8px; }
.vschg.added { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.vschg.added i { color: var(--ok); }
.vschg.changed i { color: var(--brand); }

/* ------------------------------------------------------------ the assistant
   A panel in the corner of the selector. It is a second way to work the same
   page, so it sits beside it rather than over it, and it never covers the
   questions it is filling in. */
.asst { position: fixed; right: 20px; bottom: 20px; z-index: 60; }
/* Once it has been moved it is pinned by its top-left instead of its corner,
   which is the only way dragging can mean what it looks like it means. */
.asst.moved { right: auto; bottom: auto; }
.asst.moving { user-select: none; }
.asstbtn {
  display: flex; align-items: center; gap: 8px; font: inherit; font-size: 13.5px;
  padding: 10px 16px; border-radius: var(--r-pill); cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .13);
}
.asstbtn:hover { border-color: var(--brand); }
/* The launcher is draggable as well as clickable, and says so. */
.asstbtn { touch-action: none; user-select: none; }
.asst.moving .asstbtn { cursor: grabbing; }

/* Shaded: the title bar alone, still where you left it. Closing is a different
   thing and both are worth having. */
.asst.small .asstpanel {
  height: auto; min-height: 0; resize: none;
  width: min(300px, calc(100vw - 32px));
}
.asst.small .asstlog,
.asst.small .asstask,
.asst.small .asstform,
.asst.small .asstnote { display: none; }
.asst.small .asstpanel header { border-bottom: 0; border-radius: var(--r-xl); }
.asst.on .asstbtn { display: none; }
.asstdot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.asstpanel {
  display: flex; flex-direction: column; width: min(380px, calc(100vw - 32px));
  height: min(560px, calc(100vh - 120px));
  resize: both; min-width: 300px; min-height: 320px;
  max-width: calc(100vw - 24px); max-height: calc(100vh - 24px);
  border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--surface);
  box-shadow: 0 12px 40px rgba(0, 0, 0, .18); overflow: hidden;
}
.asstpanel header {
  display: flex; align-items: center; gap: 8px; padding: 11px 14px;
  border-bottom: 1px solid var(--line); background: var(--surface-2);
  cursor: grab; touch-action: none; user-select: none;
}
.asst.moving .asstpanel header { cursor: grabbing; }
/* The grip is the only thing on the bar that says it can be dragged, so it is
   worth the six pixels. */
.asstgrip {
  flex: 0 0 auto; width: 12px; height: 12px; border-radius: var(--r-xs);
  background:
    radial-gradient(currentColor 1px, transparent 1.2px) 0 0 / 5px 5px;
  color: var(--ink-3); opacity: .7;
}
/* The title takes the free space so the three buttons stay together at the
   right. They used to carry `margin-left: auto` each, which spread them across
   the bar and left one of them sitting in the middle of what people grab to
   drag the window. */
.asstpanel header b {
  font-size: 14px; flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.asstx {
  margin-left: 0; border: 0; background: none; color: var(--ink-3);
  font: inherit; font-size: 14px; cursor: pointer; padding: 2px 6px; border-radius: var(--r-sm);
  flex: 0 0 auto;
}
.asstx:hover { color: var(--ink); background: var(--surface); }

.asstlog { flex: 1 1 auto; overflow: auto; padding: 12px 14px; display: grid; gap: 10px; }
.asstmsg p { margin: 0; font-size: 13.5px; line-height: 1.55; }
.asstmsg.assistant > p {
  padding: 9px 12px; border-radius: var(--r-lg) var(--r-lg) var(--r-lg) var(--r-xs);
  background: var(--surface-2); color: var(--ink);
}
.asstmsg.user { justify-self: end; max-width: 85%; }
.asstmsg.user p {
  padding: 9px 12px; border-radius: var(--r-lg) var(--r-lg) var(--r-xs) var(--r-lg);
  background: var(--brand); color: #fff;
}
.asstmsg.busy p { color: var(--ink-3); }
.asstset { margin-top: 5px !important; font-size: 11.5px !important; color: var(--ok) !important; }

.asstask { padding: 0 14px 10px; }
.asstask .btn { width: 100%; }
.asstform { display: flex; gap: 7px; padding: 10px 14px; border-top: 1px solid var(--line); }
.asstform textarea {
  flex: 1 1 auto; font: inherit; font-size: 13.5px; padding: 8px 10px; resize: none;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface-2); color: var(--ink);
}
.asstform .btn { align-self: flex-end; }
.asstnote {
  margin: 0; padding: 0 14px 12px; font-size: 10.5px; line-height: 1.5; color: var(--ink-3);
}
@media (max-width: 560px) {
  /* The panel wants the whole width. The launcher does not: stretched, the
     pill was pinned to the left edge, which is where the tour button lives,
     so the two sat on top of each other. Only the open panel spans. */
  .asst { right: 12px; bottom: 12px; }
  .asst.on { left: 12px; }
  .asst.on .asstpanel { width: 100%; height: min(70vh, 520px); }
}
.enqchat .them { border-color: color-mix(in srgb, var(--brand) 35%, transparent); }
.cassist .asstask { display: flex; gap: 7px; }
.cassist .asstask .btn { width: auto; flex: 1 1 auto; }

/* Maximised: the panel becomes the window. A table of thirty-seven questions
   does not fit in a 380px column at any font size. */
.asst.big { left: 16px; right: 16px; top: 16px; bottom: 16px; }
.asst.big .asstpanel {
  width: 100%; height: 100%; max-width: none; max-height: none; resize: none;
}
.asst.big .asstlog { padding: 16px 20px; }
.asst.big .asstmsg.user { max-width: 62ch; }
/* A table wants the width; a paragraph does not. Eleven words per line is a
   sentence, a hundred and forty characters is a scan line. */
.asst.big .md p, .asst.big .md li { max-width: 92ch; }

/* What a model writes back, once it is rendered rather than printed. */
.md > *:first-child { margin-top: 0; }
.md > *:last-child { margin-bottom: 0; }
.md p { margin: 0 0 8px; font-size: 13.5px; line-height: 1.55; }
.md h4, .md h5, .md h6 {
  margin: 12px 0 6px; font-size: 12.5px; font-weight: 650; color: var(--ink);
}
.md ul, .md ol { margin: 0 0 8px; padding-left: 20px; }
.md li { font-size: 13.5px; line-height: 1.55; margin-bottom: 2px; }
.md code {
  font-family: var(--mono); font-size: 11.5px; padding: 1px 4px; border-radius: var(--r-xs);
  background: var(--surface-2); border: 1px solid var(--line);
}
.md pre {
  margin: 0 0 8px; padding: 9px 11px; border-radius: var(--r-md); overflow: auto;
  background: var(--surface-2); border: 1px solid var(--line);
}
.md pre code { border: 0; padding: 0; background: none; font-size: 11.5px; line-height: 1.5; }
/* A wide table scrolls inside the message rather than stretching the panel. */
.mdtable { overflow-x: auto; margin: 0 0 8px; border: 1px solid var(--line); border-radius: var(--r-md); }
.md table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
.md th, .md td {
  text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}
.md th {
  background: var(--surface-2); font-weight: 600; font-size: 11.5px;
  color: var(--ink-2); position: sticky; top: 0;
}
.md tr:last-child td { border-bottom: 0; }
/* The assistant's own bubble holds the rendered block, so the bubble styling
   moves off the paragraph and onto the block. */
.asstmsg.assistant .md {
  padding: 9px 12px; border-radius: var(--r-lg) var(--r-lg) var(--r-lg) var(--r-xs);
  background: var(--surface-2); color: var(--ink);
}
.asstmsg.assistant .md p { margin-bottom: 6px; }
.asstmsg.assistant .md p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------- the privacy notice

   A strip along the bottom and a dialog behind it. The strip is a notice, not
   a consent gate -- this site sets no cookies for a visitor, so there is
   nothing to ask for -- which is why it is quiet, sits at the bottom out of
   the way of the work, and goes for good on one click. */

.privbar {
  /* Above the floating things at the bottom of the screen, because a notice
     they cover is a notice nobody reads -- and everything that lives down
     there steps up out of its way instead (below). Dialogs are still over it:
     a modal has the screen. */
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 75;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 16px;
  background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: 0 -2px 12px rgb(0 0 0 / 6%);
  font-size: 12.5px; color: var(--ink-2);
}
.privbar span { min-width: 0; flex: 1; }
.privbar .btn { flex: none; padding: 3px 10px; font-size: 12px; }

/* The assistant's launcher and the tour bubble sit in the same two corners,
   so they step up while the strip is there rather than sharing the pixels
   with it. */
body:has(#privbar:not([hidden])) .asst,
body:has(#privbar:not([hidden])) .tourfab { bottom: 58px; }
/* The drawer ends above the strip rather than behind it, so the row of
   buttons along its foot -- compare, datasheet, contact sales -- stays
   reachable while the notice is up. */
body:has(#privbar:not([hidden])) .dtlpanel { bottom: 38px; }

/* Set as a policy document rather than as a page of help text: numbered
   clauses, a measured column width, and a retention table -- because the two
   questions a reader of one of these actually has are "what is taken" and
   "for how long", and the second deserves to be answerable at a glance. */
.privsec { margin: 0 0 22px; }
.privsec h3 {
  display: flex; gap: 10px; align-items: baseline;
  margin: 0 0 8px; font-size: 13.5px; font-weight: 650; letter-spacing: .01em;
  color: var(--ink);
}
.privno {
  flex: none; min-width: 1.4em; font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--ink-3);
}
.privsec p {
  margin: 0 0 9px 0; padding-left: calc(1.4em + 10px);
  font-size: 13px; line-height: 1.75; color: var(--ink-2);
}
.privlede {
  margin: 0 0 8px; font-size: 13.5px; line-height: 1.8; color: var(--ink);
  padding: 14px 16px; background: var(--surface-2);
  border-left: 3px solid var(--brand); border-radius: 0 var(--r-md, 8px) var(--r-md, 8px) 0;
}
.privwho { margin: 0 0 20px; font-size: 12.5px; color: var(--ink-3); }
/* A document, so a document's measure: one column of about seventy
   characters. Wider than that and the eye loses the line it is on between the
   end of one and the start of the next, which is the whole reason a printed
   page is not the width of the desk. */
#privacy { max-width: 720px; }
#privacy-body { max-width: none; }

.privtable { width: 100%; border-collapse: collapse; margin: 2px 0 6px; }
.privtable th, .privtable td {
  text-align: left; vertical-align: top; padding: 7px 10px;
  border-bottom: 1px solid var(--line); font-size: 12.5px; line-height: 1.6;
}
.privtable th { width: 38%; font-weight: 600; color: var(--ink); }
.privtable td { color: var(--ink-2); }
.privsec p:has(> .privtable), .privsec p > .privtable { margin-bottom: 0; }
.privkeys { margin: 6px 0 0; padding-left: 18px; }
.privsec p > .privkeys { padding-left: 20px; }
.privkeys li { font-size: 12.5px; line-height: 1.7; color: var(--ink-2); }
.privkeys code {
  font-size: 11.5px; padding: 1px 5px; border-radius: 4px;
  background: var(--surface-2); color: var(--ink);
}
.privfoot {
  margin: 20px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--ink-3);
}

/* The placeholder that stands in for a frame nobody has asked for yet. It
   names the host it would contact, because "load it" is not a question
   somebody can answer without knowing who they would be talking to. */
.frameask {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; height: 100%; min-height: 220px; padding: 24px;
  text-align: center; background: var(--surface-2);
}
.frameask-t { margin: 0; font-size: 13.5px; color: var(--ink); }
.frameask-h { margin: 0; font-size: 12.5px; color: var(--ink-2); }
.frameask-b { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }

@media print { .privbar { display: none; } }

/* A button that reads as a link. The notice is a dialog rather than a page, so
   the thing that opens it cannot be an anchor with somewhere to go; it should
   still look like the thing a reader expects to be able to click. */
.linkish {
  border: 0; background: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--brand); text-decoration: underline; text-underline-offset: 2px;
}
.linkish:hover { color: var(--brand-ink, var(--brand)); }

/* ------------------------------------------------------------------ motion

   The rules are in web/motion.js; this is the half that belongs in a
   stylesheet. Two principles hold everything together: only transform and
   opacity are animated, because they are the two a browser can move without
   laying the page out again; and every one of these rules is switched off for
   anybody whose system asks for less movement, at the foot of this section.

   The curve is the same everywhere -- quick to leave, slow to settle -- so
   that two things moving at once look like one thing happening. */

:root {
  --e-out: cubic-bezier(.16, 1, .3, 1);
  --e-in-out: cubic-bezier(.65, 0, .35, 1);
  --t-quick: 120ms;
  --t-base: 200ms;
  --t-slow: 320ms;
}

/* Dialogs arrive rather than appear: a short rise and a fade, with the
   backdrop coming up underneath them. ::backdrop is animated separately
   because it is not a child of the dialog. */
@keyframes dlg-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

dialog[open] { animation: dlg-in var(--t-base) var(--e-out) both; }
dialog[open]::backdrop { animation: fade-in var(--t-base) ease both; }

/* The detail drawer already slides; this is the easing it slides with, and
   the content inside it settling a moment behind the panel itself. */
.dtlpanel { transition: transform var(--t-slow) var(--e-out); }
@keyframes panel-body-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.dtl-on .dtlbody { animation: panel-body-in var(--t-base) var(--e-out) 60ms both; }

/* Press feedback. A button that moves under the finger feels like a button;
   one that does not feels like a picture of one. Kept small: 2% is felt more
   than it is seen, which is the point. */
.btn, .cond, .bopt, .pcard .cmpbtn, .askword, .catchip {
  transition: transform var(--t-quick) var(--e-out),
              border-color var(--t-quick) linear,
              background-color var(--t-quick) linear,
              box-shadow var(--t-base) var(--e-out);
}
.btn:active, .cond:active, .bopt:active, .askword:active, .catchip:active { transform: scale(.98); }

/* A card lifts a little under the pointer. The shadow does the work; the
   translate is one pixel, which is the difference between "this is
   interactive" and "this is jumping about". */
.pcard { transition: transform var(--t-base) var(--e-out), box-shadow var(--t-base) var(--e-out),
                     border-color var(--t-quick) linear; }
.pcard:hover { transform: translateY(-1px); box-shadow: 0 10px 30px -18px rgb(0 0 0 / 40%); }

/* The condition that just changed. Not a flash -- a ring that grows out of
   the chip and fades, which is visible from the corner of the eye and gone
   before it can annoy anybody. */
@keyframes just-ring {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand) 55%, transparent); }
  to   { box-shadow: 0 0 0 10px transparent; }
}
.cond.just { animation: just-ring 900ms var(--e-out) both; }

/* The number of matches, while it is rolling. Tabular figures stop the line
   from shuffling sideways as the digits change. */
.cnum { font-variant-numeric: tabular-nums; }

/* The header earns a shadow once there is something above the fold. */
.topbar { transition: box-shadow var(--t-base) var(--e-out); }
body.scrolled .topbar { box-shadow: 0 6px 20px -16px rgb(0 0 0 / 55%); }

/* The assistant's launcher and the tour bubble already move; this is what
   they move with. */
.asst, .tourfab { transition: bottom var(--t-base) var(--e-out), transform var(--t-quick) var(--e-out); }

/* And the way out, for anybody who has asked for it. For some people this
   kind of movement causes nausea; a shortlist of industrial computers is not
   worth that. Everything above degrades to the state it was animating
   towards, which is the state that carries the information. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after, dialog[open], dialog[open]::backdrop {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .pcard:hover, .btn:active, .cond:active, .bopt:active { transform: none; }
}

/* The comparison tray, the toast and the popover: three things that appear
   over the page rather than in it, and all three used to blink into
   existence. They rise from the edge they belong to instead, which is also
   what says where they came from. */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@keyframes pop-in {
  from { opacity: 0; transform: translateY(-6px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
.cmptray:not([hidden]) { animation: rise-in var(--t-base) var(--e-out) both; }
.toast { animation: rise-in var(--t-base) var(--e-out) both; }
#pop:not([hidden]) { animation: pop-in var(--t-quick) var(--e-out) both; }

/* The ticked state of a comparison button, and the category chip that is
   currently filtering: both are switches, and a switch that changes without
   moving is a switch somebody has to verify by reading it. */
.cmpbtn.on { animation: just-tick var(--t-slow) var(--e-out) both; }
@keyframes just-tick {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* The fit bar fills to its score rather than being drawn at it. The width is
   set inline by the card, so the transition is all this needs. */
.score .bar > i, .dtlscore .bar > i { transition: width var(--t-slow) var(--e-out); }

/* A tab in the detail panel, and the underline that follows the chosen one. */
.dtltabs [data-dt] { transition: color var(--t-quick) linear, box-shadow var(--t-base) var(--e-out); }

@media (prefers-reduced-motion: reduce) {
  .cmptray:not([hidden]), .toast, #pop:not([hidden]), .cmpbtn.on { animation: none; }
}

/* A long document in a box needs to say that it is long. Without this the
   text is simply cut off at the panel's edge, mid-line, which reads as
   something broken rather than as something scrollable -- and on a dark
   background, where the edge is low contrast, it reads as broken twice over.
   The mask fades the last few lines out instead, and stops as soon as the
   reader reaches the end. */
.scrollfade {
  --fade: 26px;
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fade)), transparent 100%);
  padding-bottom: 28px;
}
.scrollfade:has(> :last-child:only-child) { mask-image: none; }

/* The scrollbar inside a dialog. The default is a light-grey trough that sits
   on a dark panel looking like a mistake. */
.dlg-body { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.dlg-body::-webkit-scrollbar { width: 10px; }
.dlg-body::-webkit-scrollbar-track { background: transparent; }
.dlg-body::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 99px;
  border: 3px solid transparent; background-clip: content-box;
}
.dlg-body::-webkit-scrollbar-thumb:hover { background: var(--ink-3); background-clip: content-box; }


/* The test-site confirmation on the way in. */
dialog.testack { max-width: 460px; width: calc(100vw - 32px); padding: 0; border-top: 4px solid var(--warn); }
dialog.testack form { padding: 24px 24px 20px; display: grid; gap: 10px; }
.testack-mark {
  width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  background: var(--warn-soft); color: var(--warn); font-weight: 700; font-size: 20px;
}
dialog.testack h2 { margin: 4px 0 0; font-size: 19px; }
dialog.testack p { margin: 0; line-height: 1.6; color: var(--ink-2); }
dialog.testack .testack-note { color: var(--ink); font-weight: 500; }
dialog.testack ul { margin: 2px 0 4px; padding-left: 20px; color: var(--ink-2); line-height: 1.6; font-size: 14px; }
.testack-foot { display: flex; align-items: center; gap: 10px; justify-content: flex-end; flex-wrap: wrap; margin-top: 6px; }
.testack-lang {
  margin-right: auto; font: inherit; font-size: 13px; padding: 6px 8px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface-2); color: var(--ink);
}
.asstusage { margin-top: 5px !important; font-size: 11px !important; color: var(--ink-3) !important; font-family: var(--mono); }

/* "Were these useful?" under the results. */
.fbbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 14px 0 6px;
  padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface);
  font-size: 13.5px; color: var(--ink-2);
}
.fbbar .fbq { margin-right: 4px; }
.fbbar .fbv {
  font-size: 18px; line-height: 1; padding: 5px 10px; border-radius: var(--r-pill); cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2);
}
.fbbar .fbv:hover { border-color: var(--brand); }
.fbbar.open { flex-direction: column; align-items: stretch; }
.fbbar .fbwhy { display: flex; flex-wrap: wrap; gap: 6px; }
.fbbar .fbwhy .chip { cursor: pointer; font: inherit; font-size: 12.5px; padding: 4px 10px; }
.fbbar .fbwhy .chip[aria-pressed="true"] { border-color: var(--brand); background: var(--brand-soft); color: var(--brand); }
.fbbar textarea {
  font: inherit; font-size: 13px; padding: 7px 9px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--ink); resize: vertical;
}
.fbbar .fbact { display: flex; gap: 8px; justify-content: flex-end; }
.fbbar.done { color: var(--ok); }

/* ---------------------------------------------------------------- festive
   The holiday dress (festive.js). A strip above the results, a coloured rule
   under the header, and a short fall of particles that stops by itself. The
   strip's two colours are dark enough to carry white text in either theme. */
:root { --fest-a: #004280; --fest-b: #0a5ca8; } /* set per holiday by festive.js */
html[data-festive] .topbar {
  border-bottom: 2px solid transparent;
  border-image: linear-gradient(90deg, var(--fest-a), var(--fest-b), var(--fest-a)) 1;
}
.festbar {
  display: flex; align-items: center; gap: 14px;
  margin: 14px 0 16px; padding: 10px 12px 10px 14px;
  border-radius: var(--radius);
  color: #fff;
  background:
    radial-gradient(circle at 92% 20%, rgb(255 255 255 / .16), transparent 38%),
    linear-gradient(110deg, var(--fest-a), var(--fest-b));
  box-shadow: 0 6px 18px -10px var(--fest-a);
  position: relative; overflow: hidden;
}
.festbar[hidden] { display: none; }
.festgate { margin: 0 auto 22px; max-width: 720px; text-align: left; }
.festart { flex: none; width: 40px; height: 40px; display: grid; place-items: center;
  background: rgb(255 255 255 / .14); border-radius: 50%; }
.festart svg { width: 28px; height: 28px; }
.festtxt { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; line-height: 1.45; }
.festtxt b { font-size: 15px; letter-spacing: .02em; }
.festtxt span { font-size: 13.5px; opacity: .93; }
.festx {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .35); background: transparent; color: #fff;
  font-size: 18px; line-height: 1; cursor: pointer;
}
.festx:hover { background: rgb(255 255 255 / .15); }
.festx:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.festfx { position: fixed; inset: 0; pointer-events: none; z-index: 125; overflow: hidden;
  transition: opacity 1.1s ease; }
.festfx.done { opacity: 0; }
.fp { --sz: 20px; position: absolute; width: var(--sz); height: var(--sz); opacity: .85;
  animation-timing-function: linear; animation-iteration-count: 1; animation-fill-mode: both; }
.fp svg { width: 100%; height: 100%; display: block; }
.fpin { display: block; width: 100%; height: 100%;
  animation: fest-sway ease-in-out infinite alternate; }
.fp-fall { top: 0; animation-name: fest-fall; }
.fp-rise { top: 0; animation-name: fest-rise; }
.fp-drift { left: 0; animation-name: fest-drift; }
@keyframes fest-fall { from { transform: translateY(-40px); } to { transform: translateY(105vh); } }
@keyframes fest-rise { from { transform: translateY(105vh); } to { transform: translateY(-60px); } }
@keyframes fest-drift { from { transform: translateX(105vw); } to { transform: translateX(-60px); } }
@keyframes fest-sway { from { transform: translateX(-10px) rotate(-14deg); } to { transform: translateX(10px) rotate(14deg); } }

/* April Fools': the logo turns once, which is the whole joke apart from the
   strip. Nothing that changes what the page says about a product. */
@keyframes fest-flip { 0% { transform: none; } 50% { transform: rotateY(180deg) rotate(-8deg); } 100% { transform: rotateY(360deg); } }
html[data-festive-flip] .brand .logo { animation: fest-flip 1.4s ease-in-out 1.2s 2; }

/* The holiday preview sits beside the language selector (festive.js) and
   borrows its look; the 🎉 says what it is without a label taking space. */
.festpick { max-width: 180px; padding-left: 30px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><text x='1' y='19' font-size='18'>%F0%9F%8E%89</text></svg>"),
                    linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: 8px 50%, right 12px top 50%, right 7px top 50%;
  background-size: 16px 16px, 5px 5px, 5px 5px; background-repeat: no-repeat; }

@media (max-width: 640px) {
  .festbar { gap: 10px; padding: 8px 10px; }
  .festart { width: 32px; height: 32px; } .festart svg { width: 22px; height: 22px; }
  .festtxt b { font-size: 14px; } .festtxt span { font-size: 12.5px; }
}
@media (prefers-reduced-motion: reduce) {
  .festfx { display: none; }
  html[data-festive-flip] .brand .logo { animation: none; }
}
@media (max-width: 480px) { .festpick { max-width: 118px; } }
@media print { .festbar, .festfx, .festpick { display: none !important; } html[data-festive] .topbar { border-image: none; } }

/* Public feedback (report.js). */
.rpdlg { width: min(640px, calc(100vw - 24px)); }
.rp-sub { margin: 0 0 12px; color: var(--ink-2); }
.rp-kinds { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.rp-kind { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px; cursor: pointer; font-size: 13.5px; background: var(--surface); }
.rp-kind:has(input:checked) { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); }
.rp-kind input { margin: 0; }
.rp-pics { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; font-size: 12.5px; }
.rp-strip { display: flex; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
.rp-thumb { position: relative; }
.rp-thumb img { width: 96px; height: 72px; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--line); display: block; }
.rp-thumb button { position: absolute; top: -6px; right: -6px; width: 20px; height: 20px; border-radius: 50%; border: 0; background: var(--ink); color: var(--surface); cursor: pointer; padding: 0; line-height: 20px; }
.rp-note { font-size: 12.5px; color: var(--ink-2); margin: 4px 0 10px; }
.rp-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.rp-list { list-style: none; padding: 0; margin: 0; }
.rp-list li { display: flex; justify-content: space-between; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
.rp-st { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 1px 9px; font-size: 12.5px; }
.rp-new { color: var(--brand); border-color: var(--brand); }
.rp-in_progress { color: var(--warn); border-color: var(--warn); background: var(--warn-soft); }
.rp-need_info { color: #7a4fb5; border-color: #7a4fb5; }
.rp-done { color: var(--ok); border-color: var(--ok); background: var(--ok-soft); }
.rp-rejected { color: var(--ink-2); }
.rp-hint { font-size: 13px; color: var(--ink-2); margin: 4px 0 10px; }
.rp-text { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
.rp-mine { background: var(--surface-2); border: 1px solid var(--line-soft); border-radius: var(--r-md); padding: 10px 12px; }
.rp-h { font-size: 14px; margin: 16px 0 6px; }
.rp-evs { list-style: none; padding: 0 0 0 12px; margin: 0 0 12px; border-left: 2px solid var(--line); }
.rp-ev { margin: 0 0 10px; font-size: 13.5px; }
.rp-ev.me .rp-text { color: var(--ink-2); }
@media print { .rpdlg { display: none !important; } }
.modfit.with { color: var(--warn); font-weight: 600; }
.cfg-row.via .cfg-why { color: var(--warn); }
/* A card waiting for an i-Module, and the modules that would take it. */
.cfg-need { margin: 2px 0 6px; }
.cfg-row.waiting .cfg-why { color: var(--bad); font-weight: 560; }
.cfg-row.takes { background: var(--ok-soft); border-radius: var(--r-md); padding-left: 8px; padding-right: 8px; }
.cfg-takes { font-size: 11.5px; color: var(--ok); font-weight: 560; margin-top: 2px; }
.cfg-where { font-size: 12.5px; color: var(--ink-2); margin: 4px 0; }

/* ------------------------------------------------------------ the nudge
   A card in the corner that asks once whether the visitor would like to talk
   to sales (nudge.js). Not modal: the page behind it stays usable. */
.nudge {
  position: fixed; right: max(20px, env(safe-area-inset-right)); bottom: 20px; z-index: 65;
  width: min(330px, calc(100vw - 32px)); padding: 16px 16px 14px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-lg, 14px);
  box-shadow: 0 14px 34px -10px rgb(0 0 0 / .4), 0 2px 8px rgb(0 0 0 / .12);
  opacity: 0; transform: translateY(12px); transition: opacity .25s, transform .25s;
}
.nudge.in { opacity: 1; transform: none; }
.nudge-h { margin: 0 22px 6px 0; font-weight: 650; font-size: 14.5px; }
.nudge-p { margin: 0 0 12px; font-size: 13px; line-height: 1.6; color: var(--ink-2); }
.nudge-b { display: flex; justify-content: flex-end; gap: 8px; }
.nudge-x {
  position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border: 0; border-radius: 50%;
  background: transparent; color: var(--ink-3); cursor: pointer; font-size: 13px;
}
.nudge-x:hover { background: var(--surface-2, rgb(0 0 0 / .05)); color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .nudge { transition: none; } }
@media print { .nudge { display: none !important; } }

/* 展場模式 is on in this browser (booth.js): a faint dot beside the logo,
   there for the person at the stand rather than for visitors. The button
   itself is larger than the dot so it can still be tapped. */
.boothchip {
  flex: none; position: relative; width: 22px; height: 22px; margin-left: 2px; padding: 0;
  border: 0; background: transparent; cursor: pointer; opacity: .35;
}
.boothchip::after {
  content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px;
  margin: -3px 0 0 -3px; border-radius: 50%; background: var(--ink-3);
}
.boothchip:hover, .boothchip:focus-visible { opacity: .9; }
@media print { .boothchip { display: none !important; } }

/* How many cards the visitor has put into this system's configurator. */
.cfgn {
  display: inline-block; min-width: 18px; margin-left: 4px; padding: 0 5px; border-radius: 9px;
  background: var(--brand); color: #fff; font-size: 11px; line-height: 18px; text-align: center;
}

/* The pages the assistant looked another maker's product up on. */
.asstrefs { margin: 6px 0 0; font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; }
.asstrefs span { color: var(--ink-3); }
.asstrefs a { color: var(--brand); word-break: break-all; }
