/*
 * esp32install layout. Three screens on one plate, one action each. Everything technical sits
 * behind <details>. Colours and fonts come from theme.css; nothing here names a colour.
 * The plate is an enamel panel, not a card: one light from above, a soft shadow under the
 * bottom edge, wells sunk into it for the lamps, the hatches and the switches.
 */

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html { height: 100%; }
body {
  margin: 0;
  min-height: 100%;
  /* One spot of light on the field behind the plate, in the colour of the field. Not a decoration. */
  background: radial-gradient(ellipse 70% 50% at 50% 0, var(--field-hi), transparent 70%) var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: 1.5;
  font-feature-settings: "tnum";
  display: flex;
  flex-direction: column;
}

a { color: var(--accent); }
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}
svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ---- header / footer -------------------------------------------------- */

.top, .foot {
  width: min(100% - 32px, var(--maxw));
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
}
.top { padding: 20px 0 8px; }
/* The mark: a signet and a logotype, the same pair as the favicon.
   The signet is drawn in fills on a 2px grid, so it survives 16px; the screen is a hole in the
   body rather than a second outline, and the one spot of accent is the state dot. The logotype
   says the name in one word and two weights, which is the whole of the drawing in it. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  text-decoration: none;
}
.signet { flex: none; fill: currentColor; stroke: none; }
.signet-dot { fill: var(--accent); }
.word { font-family: var(--font-display); font-weight: 400; letter-spacing: -0.02em; }
.word b { font-weight: 800; }
.brand .signet { width: 32px; height: 32px; }
.brand .word { font-size: var(--t-h2); }
/* Theme and language share one segmented switch sunk into the field; the pressed segment sits on the plate colour. */
.switches {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: var(--well);
  box-shadow: inset 0 1px 0 var(--shade-1), 0 0 0 1px var(--track);
}
.theme { display: flex; gap: 2px; }
.theme button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--dim);
  cursor: pointer;
}
.theme button svg { width: 18px; height: 18px; }
.lang { display: flex; gap: 2px; margin-left: 2px; padding-left: 4px; border-left: 1px solid var(--track); }
.lang a {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  line-height: 20px;
  padding: 6px 10px;
  border-radius: 999px;
  text-decoration: none;
  color: var(--dim);
}
.theme button[aria-pressed="true"], .lang a[aria-current="true"] {
  background: var(--bg-2);
  color: var(--ink);
  box-shadow: 0 1px 2px var(--shade-1);
}
.foot {
  padding: 24px 0 32px;
  font: 400 var(--t-micro)/1.5 var(--font-body);
  letter-spacing: 0.02em;
  color: var(--dim);
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s2) 0.35em; /* the licence text is an anonymous flex item: a word space, not a layout gap */
}
.foot a { color: var(--dim); text-decoration-color: var(--line-2); }
.foot a:hover { text-decoration-color: currentColor; }
/* The full footer, drawn from site.json. Without that file none of this exists and the line above
   is the whole footer, which is why nothing here is load-bearing for the install. */
.foot.is-site { display: block; text-align: left; }
.foot-brand { font: 700 var(--t-lead)/1.2 var(--font-display); letter-spacing: -0.01em; color: var(--ink); }
.foot-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--s5) var(--s4); margin-top: var(--s5); }
/* A column title is panel lettering, the same as a hatch label, not a heading on the page. */
.foot-col h2 { margin: 0; font: 700 var(--t-micro) var(--font-display); letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim); }
.foot-col ul { list-style: none; margin: var(--s2) 0 0; padding: 0; display: grid; gap: var(--s1); }
.foot-col li { font-size: var(--t-small); }
.foot-end { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2) var(--s4); margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--line); }
.foot-mark { display: inline-flex; align-items: center; gap: 8px; color: var(--ink); }
.foot-mark .signet { width: 20px; height: 20px; }
.foot-mark .word { font-size: var(--t-small); }
.foot-version { font-variant-numeric: tabular-nums; }
.foot-bottom { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s3); margin: 0 0 0 auto; padding: 0; }

/* ---- the plate ---------------------------------------------------------- */

.app { flex: 1; }
.plate {
  position: relative;
  isolation: isolate;
  width: min(100% - 32px, var(--maxw));
  margin: 8px auto 0;
  background: var(--bg-2);
  border: 0;
  border-radius: var(--radius-plate);
  /* hairline, a light edge along the top, a close shadow, and a deeper one held under the bottom edge */
  box-shadow: 0 0 0 1px var(--line), inset 0 1px 0 var(--edge-hi), 0 1px 2px var(--shade-1), 0 12px 32px -16px var(--shade-2);
  padding: clamp(24px, 5vw, 44px);
  display: flex;
  flex-direction: column;
  gap: 0;
}
/* Grain on the enamel: invisible on its own, visible next to a flat card. Under the content, over the paint. */
.plate::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: var(--grain);
  mix-blend-mode: var(--grain-blend);
}
/* The one label that carries the system's name: small, dense, in the action colour. */
.kicker {
  margin: 0;
  font: 700 var(--t-micro)/1.3 var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}
.kicker:empty { display: none; }
/* A test release wears a small amber tag: caution, not stop. One tag, two places — under the
   kicker on the install screen, and in the row of the list that offers it, so the warning arrives
   before the choice and not after it. */
.pre-label, .sys-pre {
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--well);
  color: var(--warn);
  font: 700 var(--t-micro)/1.3 var(--font-display);
  letter-spacing: 0.04em;
}
.pre-label { align-self: flex-start; margin: var(--s2) 0 0; }

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0;
}
h1 { font-size: var(--t-h1); font-weight: 800; line-height: 1.08; letter-spacing: -0.02em; }
h2 { font-size: var(--t-h2); font-weight: 600; }
h3 { font-size: 17px; margin-top: 8px; }
p { margin: 0; }
.lead { font-size: var(--t-lead); }
/* The gate: a heading, one sentence, and the way out as the action — not as a whisper under a
   button for something else. Whoever reads this screen has already been stopped once. */
.gate { display: flex; flex-direction: column; margin-bottom: var(--s5); }
.gate:last-child { margin-bottom: 0; }
#gate-title { margin: var(--s4) 0; }
#gate-why { margin-top: var(--s3); }
#copy-link { margin-top: var(--s5); }
#gate-link { margin-top: var(--s4); }

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

/* No shared gap: each element carries the distance to its neighbour, so groups read as groups. */
.screen { display: none; flex-direction: column; gap: 0; }
.screen.is-active { display: flex; animation: fade var(--motion-base) ease-out; }
.screen[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

/* The signature: computer, cable, device with a lit screen. Its left edge lines up with the heading. */
.picto {
  width: min(100%, 400px);
  height: auto;
  margin: var(--s5) 0 var(--s5) calc(var(--s2) * -1);
  color: var(--ink);
  stroke-width: 2.5;
  align-self: flex-start;
}
.picto .picto-pc, .picto .picto-body { fill: var(--well); }
.picto .picto-screen { fill: var(--accent-tint); }
.picto .picto-led { fill: var(--accent); stroke: none; }
/* The dashes flow towards the device only while the browser's port window is open (the radios are locked then). */
#screen-prepare:has(#mode-first:disabled) .picto-link { animation: link 900ms linear infinite; }
@keyframes link { to { stroke-dashoffset: -9; } }

/* A heading and its "?" are one line of text, so the line is a block and the heading stays inline:
   the ring then follows the last word. As a flex row it was pushed to the plate's right edge and
   floated there, related to nothing. */
.line { display: block; }
.line > h1, .line > h2 { display: inline; }
/* The question above the doors is a field label, not a heading. */
.door-title { font: 600 var(--t-small)/1.4 var(--font-body); letter-spacing: 0; color: var(--dim); margin: 0; }
.line:has(> .door-title) { margin-top: var(--s6); }

/* "?" hints: a small ring at the end of the sentence it belongs to, opening underneath, never a tooltip. */
.hint {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  margin: 0 0 0 10px;
  width: 30px; height: 30px;
  padding: 0;
  border: 1.5px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--dim);
  font: 600 15px var(--font-display);
  cursor: pointer;
}
/* The ring stays 30px, because a 44px ring beside a sentence is a second headline. The thumb
   gets 44px anyway, centred on it and reaching past it into the margin. */
.hint::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.hint[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
/* Emptied, it still drew its bar: a stripe and a grey box around no words, which is the one
   thing on the page that says "this did not finish loading". Nothing is not a note. */
.hint-text:empty { display: none; }
.hint-text {
  margin-top: var(--s3);
  border-left: 3px solid var(--accent);
  padding: 8px 14px;
  color: var(--dim);
  background: var(--well);
  border-radius: 0 var(--radius-in) var(--radius-in) 0;
}

/* Two doors: a binary choice, equal size on purpose. A radio mark says "one of two" before any hint does. */
.doors { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); margin-top: var(--s3); }
.door {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  padding: var(--s4);
  border: 2px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-2);
  cursor: pointer;
  color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease), background-color var(--motion-fast) var(--ease);
}
.door input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.door::after {
  content: "";
  position: absolute;
  top: 14px; right: 14px;
  width: 22px; height: 22px;
  border-radius: 999px;
  border: 2px solid var(--line-2);
  background: var(--bg-2);
  transition: border-color var(--motion-fast) var(--ease), border-width var(--motion-fast) var(--ease);
}
.door svg { width: 52px; height: 52px; color: var(--ink); flex: none; }
/* The device's screen on the mode doors: the same lit rectangle as on the pictogram. */
#doors .door svg rect:last-of-type { fill: var(--well); }
.door b { display: block; font-family: var(--font-display); font-weight: 700; font-size: var(--t-lead); line-height: 1.2; }
.door small { display: block; font-size: var(--t-small); color: var(--dim); line-height: 1.4; margin-top: 2px; }
.door:hover:not(:has(input:checked)) { border-color: var(--line-2); }
.door:has(input:checked) { border-color: var(--accent); background: var(--accent-tint); }
.door:has(input:checked)::after { border-color: var(--accent); border-width: 7px; }
.door:has(input:checked) svg { color: var(--accent); }
.door:has(input:focus-visible) { outline: 3px solid var(--accent); outline-offset: 3px; }
.door:has(input:disabled) { opacity: 0.6; cursor: default; }
@media (min-width: 701px) {
  .door { flex-direction: row; align-items: center; padding-right: 46px; }
}

/* Before the copy option on a first install: why it starts with a copy, said once, in the quiet voice. */
#backup-first { margin-top: var(--s3); font-size: var(--t-body); color: var(--dim); }
.opt-row { display: flex; align-items: center; gap: 10px; margin-top: var(--s3); }
/* The one checkbox on the page, drawn here rather than by the browser. Native it is two different
   objects — white in light, a muddy square in dark — and neither is on this panel. The input keeps
   its place: still focusable, still announced as a checkbox, only its painting is ours. The label
   is the target, and it is 44px tall. */
.opt { display: flex; gap: 12px; align-items: center; min-height: 44px; color: var(--dim); cursor: pointer; }
.opt input {
  appearance: none;
  width: 24px; height: 24px;
  margin: 0;
  flex: none;
  border: 2px solid var(--line-2);
  border-radius: 6px;
  background: var(--bg-2);
  cursor: pointer;
  display: grid;
  place-content: center;
}
/* The tick is a shape, not a glyph: a font that fails to load must not take the mark with it. */
.opt input::before {
  content: "";
  width: 14px; height: 14px;
  transform: scale(0);
  background: var(--accent-ink);
  clip-path: polygon(13% 46%, 0 61%, 39% 100%, 100% 21%, 84% 8%, 37% 69%);
  transition: transform var(--motion-fast) var(--ease);
}
.opt input:checked { border-color: var(--accent); background: var(--accent); }
.opt input:checked::before { transform: scale(1); }
.opt:hover input:not(:disabled) { border-color: var(--accent); }
.opt input:disabled { opacity: 0.45; cursor: default; }
.opt:has(input:disabled) { cursor: default; }

/* The one button. */
.cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 64px;
  padding: 14px 20px;
  margin-top: var(--s6);
  border: 0;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font: 700 var(--t-lead) var(--font-display);
  text-decoration: none;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 1px 2px var(--shade-1);
  transition: background-color var(--motion-fast) var(--ease);
}
.cta:hover:not(:disabled) { background: var(--accent-2); }
.cta:active:not(:disabled) { box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.18); }
/* Off is the ordinary state on the own-file path, before a file is chosen: the pointer may not
   claim something is running. Only the busy button does that. */
.cta:disabled { opacity: 0.55; cursor: default; }
.cta.is-busy { cursor: progress; }
.note { margin-top: var(--s3); color: var(--dim); font-size: var(--t-small); text-align: center; }
/* One blocking message, one component: a well with a rule down its edge, in the colour of the
   state. The three places a person is stopped used to be three different objects, and the third
   — the line that says two files would land on the same address — was a grey centred whisper
   that switched off the button without looking as if it had. */
#gate-text, #connect-why, .is-error #done-text {
  padding: var(--s3) var(--s4);
  border-left: 3px solid var(--warn);
  border-radius: 0 var(--radius-in) var(--radius-in) 0;
  background: var(--well);
  color: var(--ink);
  font-size: var(--t-body);
  text-align: left;
}
.is-error #done-text { border-left-color: var(--stop); background: var(--stop-tint); }
/* Why the button is off sits right above it, in the button's own gap. */
#connect-why { margin-top: var(--s6); }
#connect-why:not([hidden]) + .cta { margin-top: var(--s2); }
.cta svg { width: 20px; height: 20px; stroke-width: 2.2; }
.quiet {
  align-self: center;
  margin-top: var(--s3);
  color: var(--dim);
  font-size: var(--t-small);
  text-decoration-color: var(--line-2);
}
.quiet:hover { text-decoration-color: currentColor; }

/* ---- screen 2: ring and lamps ------------------------------------------ */

/* The ring is the only hero here: a dial of sixty ticks, the time left in the middle, nothing else. */
.ring-wrap { position: relative; width: 220px; height: 220px; align-self: center; margin-top: var(--s2); }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-track { stroke: var(--track); stroke-width: var(--ring-width); }
.ring-dial { stroke: var(--track); stroke-width: 1; stroke-dasharray: 1 3.61; stroke-linecap: butt; fill: none; }
.ring-arc {
  stroke: var(--accent);
  stroke-width: var(--ring-width);
  stroke-dasharray: 339.292;
  stroke-dashoffset: var(--off, 339.292);
  transition: stroke-dashoffset 400ms var(--ease);
}
.ring.is-busy .ring-arc { animation: turn 2.4s linear infinite; transform-origin: 60px 60px; }
.ring.is-done .ring-arc { stroke: var(--done); }
@keyframes turn { to { transform: rotate(360deg); } }
.ring-center {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  text-align: center;
  padding: 0 28px;
}
.eta { font-family: var(--font-display); font-weight: 800; font-size: var(--t-hero); line-height: 1.1; letter-spacing: -0.02em; color: var(--ink); }
.stage-text { margin-top: var(--s4); text-align: center; font: 600 var(--t-lead)/1.35 var(--font-display); color: var(--ink); min-height: 1.35em; }
#save-backup { margin-top: var(--s5); }

/* Lamps: lenses in bezels on one control strip. Lit = in progress, dark = free. No glow, ever. */
.lamps {
  list-style: none;
  margin: var(--s5) 0 0;
  padding: var(--s3) var(--s4);
  display: flex; flex-direction: column; gap: var(--s3);
  align-self: center;
  width: fit-content;
  max-width: 100%;
  background: var(--well);
  border-radius: var(--radius-in);
}
.lamp { display: flex; align-items: center; gap: 12px; color: var(--dim); font-weight: 600; }
/* A lens in a rim, and one recipe for both themes. It used to be a filled circle inside a ring of
   plate colour inside a second ring — which is exactly the mark a chosen door wears one screen
   earlier, so two lamps read as two ticked options on a screen where nothing may be touched.
   The rim sits on the glass now, and the light comes off the top of the lens, not off a halo. */
.lamp i {
  width: 20px; height: 20px;
  border-radius: 999px;
  border: 2px solid var(--line);
  background: var(--lamp-off);
  box-shadow: inset 0 3px 2px -2px rgb(255 255 255 / 0.7), inset 0 -3px 3px -2px rgb(0 0 0 / 0.35);
  flex: none;
}
.lamp.is-on { color: var(--ink); }
.lamp.is-on i { background: var(--accent); }
.lamp.is-on.is-pulse i { animation: pulse 2s ease-in-out infinite; }
.lamp.is-off i { background: var(--lamp-off); }
.lamp.is-done i { background: var(--done); }
@keyframes pulse { 50% { opacity: 0.35; } }

/* ---- screen 3 ----------------------------------------------------------- */

/* The same ring as on screen 2, now full and green, with a tinted centre. Red only when it stopped. */
.mark { width: 220px; height: 220px; align-self: center; margin-top: var(--s2); }
.mark-fill { fill: var(--done-tint); stroke: none; }
.mark-ring { stroke: var(--done); stroke-width: var(--ring-width); }
.mark-ok { stroke: var(--done); stroke-width: 8; }
.mark-stop { display: none; stroke: var(--stop); stroke-width: 8; }
.is-error .mark-fill { fill: var(--stop-tint); }
.is-error .mark-ring { stroke: var(--stop); }
.is-error .mark-ok { display: none; }
.is-error .mark-stop { display: block; }
#done-title { margin-top: var(--s5); color: var(--done); }
.is-error #done-title { color: var(--ink); }
/* The cable lamp from screen 2, released: the same lens with the same rim and the same reflection,
   now green. It was a bare dot in the dark theme and a haloed ring in the light one — one element,
   two objects, which is the thing rule 7 is for. */
#done-unplug { margin-top: var(--s3); }
#done-unplug::before {
  content: "";
  display: inline-block;
  width: 20px; height: 20px;
  margin-right: 10px;
  vertical-align: -4px;
  border-radius: 999px;
  border: 2px solid var(--line);
  background: var(--done);
  box-shadow: inset 0 3px 2px -2px rgb(255 255 255 / 0.7), inset 0 -3px 3px -2px rgb(0 0 0 / 0.35);
}
#done-text { margin-top: var(--s3); }
#screen-done .cta { margin-top: var(--s5); }

/* ---- technical layer: the service hatch --------------------------------- */

/* Three hatches in one chamber sunk into the plate; the labels read like panel lettering, not links. */
.hatches {
  margin-top: var(--s6);
  padding: var(--s1) var(--s4);
  background: var(--well);
  border-radius: var(--radius-in);
  box-shadow: inset 0 1px 0 var(--shade-1);
}
.hatches:not(:has(details:not([hidden]))) { display: none; } /* the gate hides every hatch; the chamber goes with them */
details { border-top: 1px solid var(--line); padding: 0; }
.hatches details:first-child { border-top: 0; }
summary {
  cursor: pointer;
  list-style: none;
  min-height: 44px;
  font: 700 var(--t-micro) var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  display: flex; align-items: center; gap: 8px;
}
summary::-webkit-details-marker { display: none; }
summary::before {
  content: "";
  order: 2;
  margin-left: auto;
  margin-right: 2px;
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--motion-fast) var(--ease);
}
details[open] > summary::before { transform: rotate(45deg); }
/* An open hatch lays its contents straight on the chamber floor. A card in here would be a third
   background and a second radius on one piece of text, and its side margins read as decoration. */
.hatches details[open] > .facts, .hatches details[open] > .alt {
  padding: var(--s1) 0 var(--s5);
  margin: 0;
}
.facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--s2) var(--s4);
  margin: 0;
  font-size: 16px;
}
.facts dt { color: var(--dim); font: 700 var(--t-micro) var(--font-display); letter-spacing: 0.06em; text-transform: uppercase; align-self: center; }
.facts dd { margin: 0; overflow-wrap: anywhere; }
/* The sentence that stands in for a table of nothing; ui.js hides the rows it has no value for. */
.facts-none { margin-top: var(--s2); color: var(--dim); font-size: 16px; }

pre {
  margin: 0;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-in);
  background: var(--well);
  color: var(--ink);
  font: 14px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* The vendored mono font is asked for only once a hatch is open, so it never loads for the first layer. */
details[open] pre, details[open] code { font-family: var(--font-mono); }
.log pre { max-height: 240px; overflow: auto; min-height: 3em; background: var(--bg-2); }
.log pre:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
/* Every control that is not the one action: an outlined key on the panel, and a ghost for the
   quiet ways out. Neither ever fills with the accent, so the plate keeps one knob. Before this
   they borrowed whatever class was nearest and came out looking like browser defaults. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 10px;
  min-height: 44px; /* the same target as the hatch summaries; Remove used to come out at 39px */
  padding: 10px 18px;
  border: 1.5px solid var(--line-2);
  border-radius: var(--radius-in);
  background: var(--bg-2);
  color: var(--ink);
  font: 600 var(--t-small) var(--font-display);
  cursor: pointer;
}
.btn--ghost { border-color: transparent; background: transparent; color: var(--dim); }
/* The erase key: the one filled amber on the page, so the heaviest choice in the row also looks
   the heaviest. As an outline it weighed the same as the button that changes nothing, and the
   row came out as two keys of equal weight with no way out between them. */
.btn--warn { border-color: var(--warn); background: var(--warn); color: var(--bg); }
.btn--warn:hover:not(:disabled) { background: var(--warn); filter: brightness(0.92); }
.btn:hover:not(:disabled) { background: var(--well); }
.btn--ghost:hover:not(:disabled) { color: var(--ink); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A secondary button is off far more often than it is busy, so the pointer says "not for you";
   `progress` would promise that something is happening when nothing is. */
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.log .btn { margin: var(--s2) var(--s2) var(--s3) 0; }
.alt { display: flex; flex-direction: column; gap: 6px; margin-top: 0; font-size: 16px; }
.alt ul { margin: 4px 0 0; padding-left: 20px; }
.alt li { margin: 4px 0; }
.alt code { font: 13px ui-monospace, "SF Mono", Menlo, monospace; color: var(--dim); overflow-wrap: anywhere; }
.pick { display: flex; flex-direction: column; gap: var(--s3); margin-top: var(--s4); }
.pick-list { list-style: none; margin: var(--s2) 0 0; padding: 0; display: grid; gap: 10px; }
/* One row per system: what it is, what it would install, and its own address to hand to someone
   else's page. Three tiles of nothing but a name were three identical tiles; the version, the
   channel and the newest entry's tint are what tell them apart. */
.sys {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: center;
  gap: var(--s1) var(--s4);
  padding: 12px 16px;
  border: 2px solid var(--line);
  border-radius: var(--radius);
  transition: border-color var(--motion-fast) var(--ease);
}
.sys:hover { border-color: var(--accent); }
/* The row is the action, and now it says so: the chevron of a hatch that opens, in the ink, at
   the end of the row. Before this the only thing on the row shaped like a control was Copy link. */
.sys-arrow {
  width: 9px; height: 9px;
  margin-right: 2px;
  border-right: 2.5px solid var(--ink);
  border-bottom: 2.5px solid var(--ink);
  transform: rotate(-45deg);
  justify-self: end;
}
/* The whole row is the link; the copy button sits above it, later in the paint order. */
.sys-go { color: var(--ink); text-decoration: none; font: 700 19px var(--font-display); }
.sys-go::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.pick-list small { display: block; font: 15px var(--font-body); color: var(--dim); }
.sys-meta { display: flex; align-items: center; gap: var(--s2); white-space: nowrap; }
.sys-newest { font: 700 var(--t-micro) var(--font-display); letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent); }
.sys-version { font: 600 var(--t-small) var(--font-display); color: var(--dim); }
/* Copy link is a tool for whoever runs a page of their own, so it sits a level under the row's
   own action: panel lettering, no box, and on a pointer it waits until the row is under the
   cursor. Touch has no hover, so there it is always out; the keyboard brings it back on focus. */
.sys-copy {
  position: relative;
  margin-top: 0;
  padding: 10px;
  color: var(--dim);
  font: 700 var(--t-micro) var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Visible at rest. The owner asked for shortcuts to the catalogued images by name, and a
     shortcut that appears only under a pointer is not a shortcut. It stays a level quieter than
     the row so the row still reads as the action. */
  opacity: 0.7;
  transition: opacity var(--motion-fast) var(--ease);
}
.sys:hover .sys-copy, .sys-copy:hover, .sys-copy:focus-visible { opacity: 1; }
/* The newest entry wears the same tint as a chosen door, so the list has a first and not a first
   of three — but only while that entry is stable (see showSystems). */
.sys.is-newest { border-color: var(--accent); background: var(--accent-tint); }
/* The test-release tag takes the width of the row, under the name: it is a sentence, not a suffix. */
.sys-pre { grid-column: 1 / -1; justify-self: start; white-space: normal; }
/* Your own file: a dashed card under the list, and a block on the prepare screen. */
.own-card {
  display: block;
  margin-top: 10px;
  padding: 16px 18px;
  border: 2px dashed var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  text-decoration: none;
}
.own-card b { display: block; font-family: var(--font-display); font-weight: 700; font-size: 19px; }
.own-card small { display: block; font: 15px var(--font-body); color: var(--dim); margin-top: 2px; }
.own, .own-read { display: flex; flex-direction: column; gap: var(--gap); }
.own { margin-top: var(--s5); }
.own-read .doors { margin-top: 0; }
/* One row per file, each a well on the plate: label, picker, address and what was read, and a way to drop it. */
.own-parts { display: grid; gap: var(--s3); margin: 0; }
.own-part {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding: var(--s4);
  border-radius: var(--radius-in);
  background: var(--well);
}
.own-part-title { font: 700 var(--t-micro)/1.3 var(--font-display); letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim); }
.own-part .file { margin-top: 0; }
.own-part .fields { align-items: start; }
.own-part .own-hint { font-size: var(--t-small); align-self: end; padding-bottom: 10px; }
.own-part .btn { align-self: flex-start; margin-top: 0; }
#own-add { align-self: flex-start; margin-top: 0; }
.own-hint { color: var(--dim); }
.own-url { display: flex; gap: 10px; align-items: flex-end; }
.own-url .field { flex: 1; }
.own-url .btn { margin: 0; min-height: 48px; white-space: nowrap; } /* the same height as the box it sits beside */
.fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 6px; color: var(--dim); font-size: 16px; }
.field input, .field select {
  width: 100%;
  min-height: 48px;
  padding: 10px 12px;
  border: 2px solid var(--line);
  border-radius: calc(var(--radius) - 4px);
  background: var(--bg-2);
  color: var(--ink);
  font: 17px var(--font-body);
}
/* The device list is our box with our mark on it, not the browser's. Native, it came with the
   platform's own chrome and its own dark grey, which is a second visual language on one screen.
   The mark is the chevron the hatch summaries use, so the page has one way of saying "opens". */
.field:has(select) { position: relative; }
.field select { appearance: none; padding-right: 44px; }
.field:has(select)::after {
  content: "";
  position: absolute;
  right: 18px;
  bottom: 20px;
  width: 7px; height: 7px;
  border-right: 2px solid var(--dim);
  border-bottom: 2px solid var(--dim);
  transform: rotate(45deg);
  pointer-events: none;
}
.field select:hover:not(:disabled) { border-color: var(--line-2); }
.field select:disabled, .field input:disabled { opacity: 0.55; cursor: not-allowed; }
/* ---- dialogs: one sentence, two buttons -------------------------------- */

dialog {
  width: min(100% - 32px, 460px);
  border: 0;
  border-radius: var(--radius-plate);
  box-shadow: 0 0 0 1px var(--line), inset 0 1px 0 var(--edge-hi), 0 12px 32px -16px var(--shade-2);
  background: var(--bg-2);
  color: var(--ink);
  padding: 24px;
  font-size: var(--t-body);
}
/* Fallback inside var(): Chrome 89–121 do not inherit custom properties into ::backdrop. */
dialog::backdrop { background: var(--backdrop, rgb(20 32 43 / 0.55)); }
dialog h2 { margin-bottom: 12px; }
dialog .line h2 { margin-bottom: 0; }
dialog .row { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; margin-top: 16px; }
/* Three answers do not fit on one line of a 460px dialog, and three keys wrapping into a
   right-aligned staircase are not an order. So the one dialog that can take something away lays
   them out as a ladder — the way out at the top, the erase at the bottom, safest to heaviest —
   the same shape the board dialog's choices already have. */
#erase-dialog .row { flex-direction: column; align-items: stretch; }
#erase-dialog .row .btn { margin-top: 0; }
dialog p { margin-top: 8px; }
.choices { display: grid; gap: 8px; margin-top: 12px; }
.choice {
  padding: 12px 14px;
  border: 2px solid var(--line);
  border-radius: calc(var(--radius) - 4px);
  background: transparent;
  color: var(--ink);
  font: 600 17px var(--font-body);
  text-align: left;
  cursor: pointer;
}
.choice:hover { border-color: var(--accent); }
/* The picker draws its own label, because the native input prints the browser's own words in the
   browser's own language and nothing on this page can translate them. The input keeps its place in
   the page — focusable, and read out as a file field — clipped to a point behind that label. */
.file { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 12px; color: var(--dim); cursor: pointer; }
.file > .btn { margin-top: 0; }
.file input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
.file:hover > .btn { background: var(--well); }
.file:has(input:focus-visible) > .btn { outline: 2px solid var(--accent); outline-offset: 2px; }
.file:has(input:disabled) { cursor: default; }
.file:has(input:disabled) > .btn { opacity: 0.45; }

/* ---- small screens ------------------------------------------------------- */

@media (max-width: 700px) {
  body { font-size: 17px; }
  .doors { grid-template-columns: 1fr; }
  .picto { width: min(100%, 320px); }
  .ring-wrap { width: 176px; height: 176px; }
  .mark { width: 176px; height: 176px; }
  .eta { font-size: 22px; }
  .plate { padding: var(--s5) var(--s4); }
  .fields { grid-template-columns: 1fr; }
  .facts { grid-template-columns: 1fr; gap: 2px 0; }
  .sys { grid-template-columns: 1fr auto; }
  .sys-go { grid-column: 1; grid-row: 1; }
  .sys-meta { grid-column: 1; grid-row: 2; }
  .sys-copy { grid-column: 2; grid-row: 2; justify-self: end; margin-right: -10px; }
  .sys-arrow { grid-column: 2; grid-row: 1; justify-self: end; align-self: center; }
  .sys-pre { grid-row: 3; }
  .foot-cols { grid-template-columns: 1fr; gap: var(--s4); }
  .foot-bottom { margin-left: 0; } /* the licence line wraps under the mark instead of off the edge */
  .facts dt { margin-top: 6px; }
  .foot { justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  :root { --motion-fast: 0ms; --motion-base: 0ms; }
  .screen.is-active { animation: none; }
  .ring.is-busy .ring-arc { animation: none; }
  .lamp.is-on.is-pulse i { animation: none; }
  .picto-link { animation: none !important; }
  .ring-arc { transition: none; }
  .door, .door::after, .cta, .sys, .sys-copy, .opt input::before, summary::before { transition: none; }
}

/* ---- Wi-Fi after the install: one heading, two fields, one button ---------- */
/* Shown only once the device has said it takes network details over the cable. */

.wifi { display: flex; flex-direction: column; gap: 12px; margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--line); text-align: left; }
.wifi-form { display: flex; flex-direction: column; gap: 12px; }
.wifi .row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.wifi .row .cta { flex: 1; margin-top: 0; }
.wifi .row .btn { margin-top: 0; }
.wifi-error { color: var(--stop); }
