/* Colours are tokens, never literals in the rules below: the line colour code
   is an open arbitration (decision 25) and must be changeable in one place. */
:root {
  --bg: #f4f6f8;
  --surface: #ffffff;
  --surface-2: #eef1f5;
  --ink: #131820;
  --muted: #5c6875;
  --border: #d7dde4;
  --metal: #cfd6de;
  --metal-edge: #8e9aa8;

  --line-discharge: #d4462f;
  --line-liquid: #1f9257;
  --line-expansion: #6bb6e2;
  --line-suction: #2f6ccc;

  /* Le bleu d'action, celui de la vitrine : le simulateur n'en avait pas,
     faute d'avoir jamais eu de bouton principal. */
  --accent: #2f6ccc;
  --ok: #1f9257;
  /* Les fonds du verdict. Posés en dur : `color-mix` les aurait calculés,
     mais il n'arrive qu'en 2023 et le §11 tient le plancher à 2020. */
  --ok-tint: #e7f4ed;
  --warn-tint: #fbf3e4;
  --alert-tint: #fbeae7;
  --warn: #c67b12;
  --alert: #d4462f;
  --frost: #ffffff;
  --fouling: #8a6a3d;
  /* Le voile de la loupe et le fond de l'organe agrandi. En dur, comme les
     teintes du verdict : `color-mix` n'arrive qu'en 2023 et le §11 tient le
     plancher à 2020. */
  /* Le JAUNE de l'indicateur d'humidité, et rien d'autre. L'anneau prenait
     l'orange des avertissements, trop proche du rouge des alertes pour se
     distinguer d'un coup d'œil — et un voyant se lit d'un coup d'œil.
     C'est aussi ce que fait la vraie pastille : elle vire au jaune. */
  --moisture: #e0a800;
  --veil: rgb(233 238 243 / 78%);
  --stage-bg: #f2f5f8;

  --lcd-bg: #10231c;
  --lcd-ink: #5df0b4;
  --lcd-dim: #3d8f74;

  --radius: 10px;
  --pipe-width: 7px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0e1218;
    --surface: #161c25;
    --surface-2: #1e2631;
    --ink: #e7edf4;
    --muted: #93a1b1;
    --border: #2a3441;
    --metal: #3a4653;
    --metal-edge: #6d7d8e;

    --line-discharge: #f4705a;
    --line-liquid: #3fc383;
    --line-expansion: #8fcdf0;
    --line-suction: #6e9ff0;

    --accent: #6e9ff0;
    --ok: #3fc383;
    --ok-tint: #14291f;
    --warn-tint: #2a2318;
    --alert-tint: #2a1a16;
    --warn: #e5a13c;
    --alert: #f4705a;
    --moisture: #f5c542;
    --veil: rgb(14 19 26 / 74%);
    --stage-bg: #161c24;
    --frost: #dbe9f5;
    --fouling: #b08a52;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

.bar {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.bar-title h1 { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -0.01em; }
.bar-title p { margin: 2px 0 0; font-size: 13px; color: var(--muted); }

/* La ligne d'accès : présente, lisible, et pas une bannière. On dit une fois
   ce qui est ouvert, on ne le répète pas et on ne clignote pas. */
.access-note {
  flex-basis: 100%;
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--muted);
}
.access-note[hidden] { display: none; }

.bar-picker { display: flex; align-items: center; gap: 8px; }
.bar-picker label { font-size: 13px; color: var(--muted); }

select {
  font: inherit;
  max-width: 100%;
  min-width: 0;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 7px 10px;
  min-height: 40px;
}

/* The bag stands on the left, upright, so that it costs the drawing width
   rather than height — a schematic is wide, and height is what it lacks. */
main {
  display: grid;
  /* 112 px suffisaient tant que la colonne de gauche ne portait que trois
     vignettes d'instrument. Les essais y sont arrivés avec des intitulés de
     six mots et des précautions de deux lignes : à cette largeur, « Vitesse
     d'équilibrage à l'arrêt » se lisait sur trois lignes de deux mots. */
  grid-template-columns: 184px minmax(0, 1fr) 320px;
  gap: 16px;
  padding: 16px;
  align-items: start;
}

@media (max-width: 900px) {
  main { grid-template-columns: minmax(0, 1fr); }
}

/* A whole machine shrunk to phone width is unreadable: 960 units in 414
   pixels turns a 13px label into 5px. Better to keep the drawing at a size
   that can be read and let it scroll sideways — the usual way a schematic is
   consulted on a small screen. */
@media (max-width: 700px) {
  .bar { flex-direction: column; align-items: stretch; gap: 10px; }
  .bar-picker { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 10px; }
  .stage { overflow-x: auto; }
  .schematic { min-width: 680px; }
  .tools { flex-direction: row; flex-wrap: wrap; }
  .tool { flex: 0 0 auto; padding: 8px 12px 6px; }
}

@media (min-width: 901px) and (max-width: 1200px) {
  main { grid-template-columns: minmax(0, 1fr) 280px; }
}

/* On anything desktop-sized the page is exactly one screen tall and the
   schematic takes what the bars and the panel leave. Nothing scrolls, and
   nothing is cut off. `dvh` when the browser has it, `vh` otherwise — which
   keeps the floor at a 2020 browser, see section 6 bis. */
@media (min-width: 901px) {
  body { height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
  /* `minmax(0, 1fr)` et non `1fr` : sans le minimum à zéro, la rangée de
     grille se dimensionne sur son contenu le plus haut, le panneau pousse,
     et son `max-height: 100%` ne borne plus rien. Le bouton du diagnostic se
     retrouvait alors hors de l'écran sur une fenêtre basse. */
  main { flex: 1 1 auto; min-height: 0; align-items: stretch;
         grid-template-rows: minmax(0, 1fr); }
  .stage { display: flex; min-height: 0; }
  .schematic { height: 100%; max-height: none; }
  .panel, .bag, .stage { min-height: 0; }
  .panel, .bag { overflow-y: auto; }
}

.stage, .panel, .bag {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.stage { padding: 8px; position: relative; }
/* Pas de rembourrage EN BAS : c'est le bouton collant qui l'assure.
   Tant que le panneau en avait un, la zone défilante s'arrêtait au-dessus, et
   l'élément collé — quelles que soient ses marges — ne pouvait pas peindre
   cette bande. Le texte continuait d'y défiler, sous le bouton. Le dernier
   enfant du panneau est toujours `.panel-action`, qui porte sa propre marge
   basse : rien ne touche le bord. */
.panel {
  padding: 16px 16px 0;
  /* Colonne, pour que le bouton descende AU BAS du panneau quand le contenu
     ne le remplit pas. « Collant » ne suffisait pas : un élément collant se
     pose à sa place naturelle tant qu'on ne défile pas, et depuis que la
     loupe a emporté la liste des constats, cette place est au milieu. Les
     deux vont ensemble — `margin-top: auto` le descend, `sticky` l'y retient
     quand le contenu déborde. */
  display: flex;
  flex-direction: column;
  /* La grille aligne ses colonnes en HAUT — c'est juste pour la sacoche, qui
     ne doit pas s'étirer. Le panneau, lui, doit descendre jusqu'au bas du
     synoptique, sans quoi « je pose mon diagnostic » flotte au milieu du
     vide depuis que la loupe a emporté la liste des constats. */
  align-self: stretch;
}
.bag { padding: 12px 10px; }
.bag h2 {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.tools { display: flex; flex-direction: column; gap: 8px; }

.tool {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 4px 6px;
  font: inherit;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.tool:hover { border-color: var(--metal-edge); }
.tool[aria-pressed="true"] {
  border-color: var(--line-suction);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line-suction);
}
.tool-name { font-size: 12px; font-weight: 600; }
.tool-cost { font-size: 10px; color: var(--muted); }

.bag-icon { display: block; width: 42px; height: 42px; }
.icon-body { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 2; }
.icon-screen { fill: var(--lcd-bg); }
.icon-line { fill: none; stroke: var(--metal-edge); stroke-width: 3; stroke-linecap: round; }
.icon-lp { stroke: var(--line-suction); }
.icon-hp { stroke: var(--line-discharge); }
.icon-tip { fill: var(--metal-edge); }

.bag-hint { margin: 10px 0 0; font-size: 13px; color: var(--muted); }

.clock { font-size: 13px; color: var(--muted); white-space: nowrap; }
.clock strong { color: var(--ink); }

.readings { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.reading {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 0;
  font-size: 13px;
  border-bottom: 1px solid var(--border);
}
.reading:last-child { border-bottom: 0; }
.reading-label { color: var(--muted); }
.reading-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.reading[data-taken] .reading-label { color: var(--ink); }
.reading[data-taken] .reading-value { color: var(--line-suction); }
.reading[data-missing] .reading-value { color: var(--warn); font-weight: 500; font-style: italic; }
.small { font-size: 12px; margin: 0 0 8px; }

.panel h2 {
  margin: 20px 0 8px;
  font-size: 12px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.panel h2:first-of-type { margin-top: 12px; }

.context { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0; }
.context dt { font-size: 12px; color: var(--muted); }
.context dd { margin: 0; font-size: 13px; }

blockquote {
  margin: 0;
  padding: 10px 12px;
  background: var(--surface-2);
  border-left: 3px solid var(--line-suction);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 14px;
}

.signs { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }

.sign {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 10px;
  background: var(--surface-2);
  border-left: 3px solid var(--border);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 13px;
}
.sign[data-tone="ok"] { border-left-color: var(--ok); }
.sign[data-tone="warn"] { border-left-color: var(--warn); }
.sign[data-tone="alert"] { border-left-color: var(--alert); }
.sign-part { color: var(--muted); flex: 0 0 auto; }
.sign-state { text-align: right; }
.sign-label { display: block; font-weight: 600; }

/* The verdict is written, not only coloured: a finding must say whether it
   is normal, and colour alone would not say it to everyone. */
.sign-verdict {
  display: block;
  margin-top: 1px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.sign[data-tone="ok"] .sign-verdict { color: var(--ok); }
.sign[data-tone="warn"] .sign-verdict { color: var(--warn); }
.sign[data-tone="alert"] .sign-verdict { color: var(--alert); }
.sign-empty { color: var(--muted); font-size: 13px; font-style: italic; }

.notes { font-size: 13px; color: var(--muted); margin: 0; }
.muted { color: var(--muted); }
.bar-footer { border-bottom: 0; border-top: 1px solid var(--border); font-size: 12px; }
.bar-footer p { margin: 0; }

/* ---------------------------------------------------------------- schematic
   Inline SVG animated with CSS. Transform and opacity wherever possible:
   those are the two properties a browser can animate without repainting,
   which is what keeps a training-centre tablet from heating up. */

/* The drawing is a viewBox, so it scales to whatever box it is given. On a
   wide screen the box is the window minus the chrome, and `meet` letterboxes
   rather than crops: the whole machine is always visible, however many
   components it carries. */
.schematic { display: block; width: 100%; height: auto; max-height: 72vh; }

.body { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 2; }
.coil { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 2; }
.fins path { stroke: var(--metal-edge); stroke-width: 1; opacity: 0.45; }
.serpentine { fill: none; stroke: var(--metal-edge); stroke-width: 2.5; opacity: 0.8; }

.label {
  fill: var(--ink);
  font: 600 13px/1 system-ui, sans-serif;
  text-anchor: middle;
}
.label-right { text-anchor: end; }
.label-left { text-anchor: start; }
.label-small { font-size: 11px; font-weight: 500; fill: var(--muted); }

/* Pipes -------------------------------------------------------------------- */

.pipe { fill: none; stroke-width: var(--pipe-width); stroke-linecap: round; stroke-linejoin: round; }
.pipe-discharge { stroke: var(--line-discharge); }
.pipe-liquid { stroke: var(--line-liquid); }
.pipe-expansion { stroke: var(--line-expansion); }
.pipe-suction { stroke: var(--line-suction); }

/* The moving dashes ride on top of the pipe in the surface colour, so the
   pipe keeps its own colour and only the motion is added. */
.flow {
  fill: none;
  stroke: var(--surface);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 3 26;
  opacity: 0.85;
  visibility: hidden;
}
[data-flow="on"] .flow { visibility: visible; animation: flow 1.1s linear infinite; }

@keyframes flow { to { stroke-dashoffset: -29; } }

.capillary {
  fill: none;
  stroke: var(--metal-edge);
  stroke-width: 1.6;
  stroke-dasharray: 5 4;
  opacity: 0.7;
}

.frost-line {
  fill: none;
  stroke: var(--frost);
  stroke-width: 13;
  stroke-linecap: round;
  opacity: 0;
}
.frost-line[data-frost="partial"] { opacity: 0.55; stroke-dasharray: 60 240; }
.frost-line[data-frost="present"] { opacity: 0.6; }
.frost-line[data-frost="to_compressor"] { opacity: 0.75; }

/* Compressor --------------------------------------------------------------- */

.compressor-shell { fill: var(--metal); stroke: none; }
.compressor-shell-edge { fill: none; stroke: var(--metal-edge); stroke-width: 2; }
.compressor-seam { stroke: var(--metal-edge); stroke-width: 2.5; stroke-linecap: round; }
.compressor-terminal { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 2; }
.compressor-stub { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 1.6; }
.compressor-base { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 1.8; }
.compressor-foot { fill: var(--metal-edge); opacity: 0.85; }
.compressor-sheen { fill: none; stroke: var(--surface); stroke-width: 5; stroke-linecap: round; opacity: 0.35; }

/* Semi-hermetic. Cast iron rather than pressed steel: squarer, heavier, and
   the head comes off — which is why it is not a cosmetic variant. */
.semi-body { fill: var(--metal); stroke: none; }
.semi-body-edge { fill: none; stroke: var(--metal-edge); stroke-width: 2; }
.semi-motor { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 2; }
.semi-fins path { stroke: var(--metal-edge); stroke-width: 1.4; opacity: 0.55; }
.semi-terminal { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 2; }
.semi-head { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 2; }
.semi-bolts circle { fill: var(--metal-edge); opacity: 0.8; }
.semi-valve { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 1.8; }
.semi-valve-cap { fill: var(--metal-edge); opacity: 0.85; }
.semi-oil-ring { fill: var(--surface); stroke: var(--metal-edge); stroke-width: 2.2; }
.semi-oil-level { fill: var(--fouling); opacity: 0.55; }

/* The oil sits in the bottom of the shell: that is the part a hand touches,
   and the part the observation speaks about. */
.compressor-crankcase { fill: var(--metal-edge); opacity: 0.22; }
.compressor[data-crankcase="cold"] .compressor-crankcase { fill: var(--line-expansion); opacity: 0.75; }
.compressor[data-crankcase="hot"] .compressor-crankcase { fill: var(--line-discharge); opacity: 0.6; }
/* Givré et brûlant sont les deux bouts de l'échelle, et c'est justement par
   eux que se séparent des pannes voisines : « brûlant, pas seulement chaud »
   est le discriminant du cas 029. Le dessin les ignorait tous les deux. */
.compressor[data-crankcase="frosted"] .compressor-crankcase { fill: var(--line-suction); opacity: 0.95; }
.compressor[data-crankcase="burning"] .compressor-crankcase { fill: var(--alert); opacity: 0.8; }

.compressor[data-motion="continuous"] .shake { animation: shake 0.16s linear infinite; }
.compressor[data-motion="cycling"] .shake { animation: shake 0.16s linear infinite; }
.compressor[data-motion="short_cycling"] .shake { animation: burst 3.2s steps(1) infinite; }

@keyframes shake {
  0%, 100% { transform: translate(0, 0); }
  25% { transform: translate(0.7px, -0.5px); }
  50% { transform: translate(-0.5px, 0.6px); }
  75% { transform: translate(0.4px, 0.5px); }
}

/* Starts, runs a moment, stops. The point is that it is visibly not settled. */
@keyframes burst {
  0%, 40% { transform: translate(0.6px, -0.4px); }
  41%, 100% { transform: translate(0, 0); }
}

/* Le bruit du compresseur ---------------------------------------------------
   Idée de Bruno, le 21 septembre 2026 : des ondes au-dessus du compresseur
   pour qu'un bruit se voie. Ce qu'un frigoriste entend en poussant la porte
   du local ne se mesure avec aucun instrument du sac, et le simulateur n'a
   pas d'oreille — il lui faut donc un équivalent visuel.

   Les deux bruits partagent la même couleur et ne diffèrent que par le
   mouvement, ce qui est volontaire : le sifflement s'écoule, le cognement
   frappe. Le mot exact reste dans la liste des constats. Il faut donc
   regarder pour distinguer, comme il faut écouter sur le terrain. */
.sound-waves { opacity: 0; pointer-events: none; }
.sound-waves path {
  fill: none;
  stroke: var(--alert);
  stroke-width: 2.2;
  stroke-linecap: round;
}

/* Le sifflement : continu, régulier, chaque arc part après le précédent. */
.compressor[data-sound="hissing"] .sound-waves { opacity: 1; }
.compressor[data-sound="hissing"] .sound-waves path {
  animation: hiss 1.5s ease-out infinite;
}
.compressor[data-sound="hissing"] .sound-waves path:nth-child(2) { animation-delay: 0.25s; }
.compressor[data-sound="hissing"] .sound-waves path:nth-child(3) { animation-delay: 0.5s; }

@keyframes hiss {
  0%   { opacity: 0; transform: scale(0.72); }
  35%  { opacity: 0.85; }
  100% { opacity: 0; transform: scale(1.08); }
}

/* Le cognement : sec et espacé, les trois arcs frappent ensemble. Mais ils ne
   s'éteignent JAMAIS complètement entre deux coups. Première version écrite
   ici : opacité 0.9 pendant 12 % du cycle, puis zéro. Le signe était donc
   absent de l'écran près de neuf dixièmes du temps, et la capture permanente
   l'a attrapé éteint — un élève qui jette un œil au mauvais moment ne voit
   rien. Un constat doit être là quand on le cherche ; c'est son RYTHME qui
   dit le cognement, pas son absence. */
.compressor[data-sound="knocking"] .sound-waves { opacity: 1; }
.compressor[data-sound="knocking"] .sound-waves path {
  animation: knock 0.9s steps(1) infinite;
}

@keyframes knock {
  0%, 11%   { opacity: 1; stroke-width: 3.2; }
  12%, 100% { opacity: 0.34; stroke-width: 2.2; }
}

/* Les ondes se déploient depuis leur centre, au-dessus du compresseur. */
.sound-waves path { transform-origin: 140px 92px; }

/* La ceinture de carter. Toujours dessinée : toute machine de ce type en
   porte une, et une pièce qu'on demande de remplacer doit se voir. Sa couleur
   ne dit rien tant que le cas ne déclare rien — une clé absente veut dire
   « pas regardé », pas « en bon état ». */
.heater-band .band, .heater-band .band-lead { fill: var(--border); }
.crankcase-band[data-heater="ok"] .band { fill: var(--warn); }
.crankcase-band[data-heater="failed"] .band { fill: var(--alert); }

.oil-traces, .alarm-badge { opacity: 0; pointer-events: none; }
.oil-traces[data-visible] { opacity: 1; }
.oil-traces ellipse { fill: var(--fouling); opacity: 0.8; }
.alarm-badge[data-visible] { opacity: 1; }
.alarm-badge circle { fill: var(--alert); }
.alarm-badge path { stroke: var(--surface); stroke-width: 2.4; stroke-linecap: round; }

/* Fans --------------------------------------------------------------------- */

.fan-ring { fill: var(--surface); stroke: var(--metal-edge); stroke-width: 2; }
.fan-axis { fill: none; stroke: none; }
.fan-blade { fill: var(--metal-edge); opacity: 0.9; }
.fan-hub { fill: var(--metal-edge); }
.fan-blades { transform-box: fill-box; transform-origin: center; }

.fan[data-fan="running"] .fan-blades { animation: spin 0.85s linear infinite; }
.fan[data-fan="slow"] .fan-blades { animation: spin 3.4s linear infinite; }

@keyframes spin { to { transform: rotate(360deg); } }

/* Static, so that a stopped fan stays legible without motion — but only when
   the case declares it stopped. A fan at rest because the machine is off is
   not a finding and carries no alarm. */
.fan-stopped-mark { stroke: var(--alert); stroke-width: 3.5; stroke-linecap: round; opacity: 0; }
.fan[data-fan="stopped"][data-declared] .fan-stopped-mark { opacity: 1; }
.fan[data-fan="stopped"][data-declared] .fan-blade { opacity: 0.45; }

.air-arrows path {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.6;
  stroke-linecap: round;
  opacity: 0.55;
}

/* Exchangers --------------------------------------------------------------- */

.fouling { fill: url(#fouling); opacity: 0; }
.fouling[data-visible] { opacity: 1; }
.fouling-dot { fill: var(--fouling); }

.frost { fill: var(--frost); opacity: 0; }
.frost[data-frost="partial"] { opacity: 0.4; }
/* La glace du bac et de la vidange. « overflowing » est plus marqué que
   « iced » : le bac n'est plus seulement pris, il déborde dans la chambre. */
.frost[data-frost="iced"] { opacity: 0.75; }
.frost[data-frost="overflowing"] { opacity: 0.95; }
.frost[data-frost="blocked"] { opacity: 0.8; }

/* Only the fed part of the coil is cold. Nothing is computed: the case says
   whether the feed is full or partial, and the drawing says it too. */
.feed-cold { fill: var(--line-expansion); opacity: 0; }
.feed-cold[data-feed="full"] { opacity: 0.3; }
.feed-cold[data-feed="partial"] { opacity: 0.35; }

.heater { fill: var(--metal-edge); opacity: 0.5; }
.heaters[data-heaters] .heater { fill: var(--alert); opacity: 0.35; }
.heaters[data-heaters="partial"] .heater[data-heater="0"] { fill: var(--warn); opacity: 1; }

/* Le style de l'ÉTAT s'accroche à des classes, plus à des identifiants.
   Un identifiant est unique par définition ; la loupe, elle, affiche un CLONE
   de l'organe, et un clone ne peut pas porter le même. En retirant les
   identifiants du clone — il le fallait — on lui retirait aussi sa couleur de
   carter, sa vibration et ses ondes de bruit : il s'affichait éteint alors que
   la machine tourne. L'auteur l'a vu du premier coup, le 24 septembre 2026.
   Quinze règles, toutes sur le compresseur. */

/* Drier and sight glass ---------------------------------------------------- */

.drier-mesh { stroke: var(--metal-edge); stroke-width: 1.4; opacity: 0.6; }
.cold-outlet {
  fill: none;
  stroke: var(--line-expansion);
  stroke-width: var(--pipe-width);
  stroke-linecap: round;
  opacity: 0;
}
.cold-outlet[data-visible] { opacity: 1; }

.sight-fluid { fill: var(--line-liquid); opacity: 0.35; }
.sight-ring { fill: none; stroke: var(--metal-edge); stroke-width: 4; }
.sight-ring[data-sight="clear"] { stroke: var(--ok); }
.sight-ring[data-sight="bubbling"], .sight-ring[data-sight="flashing"] { stroke: var(--alert); }
/* Deux états DÉCLARÉS gardaient l'anneau gris, c'est-à-dire l'apparence de ce
   qui n'a pas été regardé : l'humidité et le voyant vide. Le premier est un
   avertissement — l'indicateur vire au jaune, le fluide est humide mais la
   machine tourne ; le second est une alerte, il n'y a plus de liquide du tout.
   Trouvé le 24 septembre 2026 en répondant à l'auteur, qui demandait pourquoi
   le voyant est tantôt vert et tantôt gris. */
.sight-ring[data-sight="moisture"] { stroke: var(--moisture); }
.sight-ring[data-sight="empty"] { stroke: var(--alert); }

.bubbles { opacity: 0; }
.bubbles[data-sight="bubbling"], .bubbles[data-sight="flashing"] { opacity: 1; }
.bubbles circle {
  fill: var(--surface);
  opacity: 0.9;
  animation: rise 1.6s ease-in infinite;
  /* SANS CES DEUX LIGNES, LES BULLES S'EN VONT. En SVG, une transformation CSS
     s'applique par défaut autour de l'origine du repère — le coin (0,0) du
     dessin — et non autour de l'élément. Le « scale(0.5) » de l'animation
     ramenait donc chaque bulle à mi-chemin du coin supérieur gauche : mesurées
     le 24 septembre 2026, elles occupaient 221 × 210 pixels à deux cents
     pixels du voyant, qui en fait 23.
     Personne ne l'avait vu parce qu'une capture d'écran fige un instant et que
     les bulles sont petites et pâles : on les prenait pour du bruit de rendu.
     `fill-box` fait tourner la transformation autour de la bulle elle-même. */
  transform-box: fill-box;
  transform-origin: center;
}

@keyframes rise {
  0% { transform: translateY(6px) scale(0.5); opacity: 0; }
  30% { opacity: 0.95; }
  100% { transform: translateY(-13px) scale(1); opacity: 0; }
}

/* Expansion valve and bulb ------------------------------------------------- */

.valve-body { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 2; }
.capillary-coil { fill: none; stroke: var(--metal-edge); stroke-width: 3; stroke-linecap: round; }
.receiver-level { stroke: var(--metal-edge); stroke-width: 1.6; opacity: 0.7; }
.solenoid-coil { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 1.8; }
.separator-baffle { stroke: var(--metal-edge); stroke-width: 1.6; opacity: 0.65; }

/* Le clapet anti-retour : le cône du sens de passage, et son siège. Sans ces
   deux règles, le cône sortait en noir plein — la couleur par défaut d'un
   `path` rempli — et sautait aux yeux au milieu d'un dessin tout en gris. */
/* Le bac à condensats et sa vidange. Le bac est une tôle en trapèze sous la
   batterie, la vidange un tube qui en descend. Les deux rectangles de givre
   qu'ils portent sont pilotés par les constats, comme le givre de la batterie
   au-dessus. */
.tray { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 2; }
.drain-pipe { stroke: var(--metal-edge); stroke-width: 6; stroke-linecap: round; fill: none; }

.valve-cone { fill: var(--metal-edge); stroke: none; }
.valve-seat { stroke: var(--metal-edge); stroke-width: 2.4; stroke-linecap: round; }
.accumulator-tube { fill: none; stroke: var(--metal-edge); stroke-width: 2; opacity: 0.75; }
/* `fill: none` n'est pas une précaution de style : un chemin ouvert se
   remplit en noir par défaut, et deux segments disjoints font apparaître un
   triangle noir entre eux. Le clapet vient de le montrer. */
.valve-stem { stroke: var(--metal-edge); stroke-width: 2.5; stroke-linecap: round; fill: none; }
.bulb-body { fill: var(--metal); }

/* Measurement anchors of section 4.1 quater. Invisible until an instrument
   is out of the bag, then they light up and become the targets. */
.point { fill: var(--muted); opacity: 0.1; pointer-events: none; }

/* Lit only when the instrument in hand can be placed there. A thermometer
   does not go on a bornier, and a multimeter does not go on a pipe. */
.point[data-live] {
  fill: var(--line-suction);
  opacity: 0.4;
  pointer-events: all;
  cursor: pointer;
}
.point[data-live]:hover { opacity: 0.85; }


/* The instruments, drawn on the machine. The value lives on the display and
   nowhere else — see section 6 ter. */
.instrument { cursor: pointer; }
.instrument-body {
  fill: var(--metal);
  stroke: var(--metal-edge);
  stroke-width: 2;
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.22));
}
.instrument:hover .instrument-body { stroke: var(--line-suction); }
.instrument-port { fill: var(--metal-edge); }
/* A note the reading needs stays on screen; the how-to only shows when the
   pointer is on the instrument, so two tools out do not shout at once. */
.instrument-note, .instrument-hint {
  font: 500 11px/1 system-ui, sans-serif;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--surface);
  stroke-width: 4;
  stroke-linejoin: round;
}
.instrument-note { fill: var(--warn); font-weight: 600; }
.instrument-hint { fill: var(--muted); opacity: 0; transition: opacity 0.15s; }
.instrument:hover .instrument-hint { opacity: 1; }

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

.lcd { fill: var(--lcd-bg); }
.lcd-label {
  fill: var(--lcd-dim);
  font: 600 13px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.lcd-value {
  fill: var(--lcd-ink);
  font: 600 16px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}
.lcd-dash {
  fill: var(--lcd-dim);
  font: 600 16px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* Hoses and leads are scenery: they must never intercept a click meant for
   the point underneath. */
.hose, .probe-lead { pointer-events: none; }

/* The electrical cabinet. Grey box, grey door, and the few parts one
   recognises when it is open: contactor, controller, a lamp. */
.panel-box { fill: var(--metal-edge); stroke: var(--metal-edge); stroke-width: 2; }
.panel-door { fill: var(--surface-2); stroke: var(--metal-edge); stroke-width: 1.5; }
.panel-rail { fill: var(--metal-edge); opacity: 0.45; }
.panel-part { fill: var(--metal); stroke: var(--metal-edge); stroke-width: 1.4; }
.panel-contactor { fill: var(--metal-edge); opacity: 0.8; }
.panel-controller { fill: var(--lcd-bg); stroke: var(--metal-edge); stroke-width: 1.4; }
.panel-led { fill: var(--ok); }

/* A live lead is not a temperature probe: red, and the tip with it. */
.probe-lead-live { stroke: var(--line-discharge); }
.probe-tip-live { fill: var(--line-discharge); }

.lcd-word {
  fill: var(--lcd-ink);
  font: 600 15px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.icon-dial { fill: none; stroke: var(--metal-edge); stroke-width: 2.4; }
.hose { fill: none; stroke-width: 6; stroke-linecap: round; opacity: 0.9; }
.hose-lp { stroke: var(--line-suction); }
.hose-hp { stroke: var(--line-discharge); }
.probe-lead { stroke: var(--metal-edge); stroke-width: 2.5; stroke-linecap: round; }
.probe-tip { fill: var(--line-suction); }

/* Accessibility ------------------------------------------------------------
   No information may live in movement alone. With animation switched off the
   state is still readable: stopped fans keep their red mark, the sight glass
   keeps its ring colour, frost and fouling are static, and the panel on the
   right says everything in words. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  [data-flow="on"] .flow { visibility: hidden; }
}

/* La barre d'action colle au bas de la colonne, et ne porte plus qu'un seul
   bouton : celui qui engage. `sticky` ne remonte jamais un élément au-delà de
   son parent, d'où cette barre en dernier enfant du conteneur qui défile. */
.panel-action {
  position: sticky;
  /* `bottom: 0`, et surtout PAS une valeur négative.
     Avec `bottom: -16px`, l'élément se colle seize pixels SOUS le bas du
     panneau : le rembourrage bas compense à l'œil pendant qu'on défile, mais
     arrivé au bout, l'élément retrouve sa place naturelle et le bouton
     remonte d'un coup. Le rembourrage n'était pas le problème, l'ancrage
     l'était.
     À zéro, la zone peinte — rembourrage compris, car un fond se peint sous
     le padding — descend jusqu'au bas de la zone défilante. Le bouton se
     trouve donc toujours à la même distance du bord, en cours de défilement
     comme à la fin.
     Cela ne suffisait pourtant pas à couvrir le contenu : encore fallait-il
     que le bas de la zone défilante soit le bas visible du panneau. D'où le
     rembourrage retiré plus haut. */
  bottom: 0;
  /* `auto` et non vingt pixels : le bouton doit DESCENDRE au bas du panneau
     quand le contenu ne le remplit pas, et depuis que la loupe a emporté la
     liste des constats c'est le cas le plus courant. Quand le contenu déborde,
     `auto` ne vaut rien et c'est le rembourrage haut qui écarte la coupure. */
  margin-top: auto;
  /* Le contenu défile SOUS ce bouton, et il arrivait au ras : la dernière
     ligne se coupait contre son bord, ce qui se lit comme un défaut
     d'affichage plutôt que comme une fin de liste. Le rembourrage haut écarte
     la coupure du bouton, et l'ombre qui le précède la rend douce. */
  padding-top: 16px;
  padding-bottom: 16px;
  background: var(--surface);
  box-shadow: 0 -18px 18px -8px var(--surface);
}
#open-diagnosis, #answer-submit, #verdict-next, #answer-cancel {
  display: block;
  width: 100%;
  padding: 12px;
  font: inherit;
  font-weight: 650;
  color: var(--surface);
  background: var(--accent);
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}
#answer-cancel { color: var(--ink); background: var(--surface-2); font-weight: 500; }
/* Grisé dès qu'on a ouvert la fenêtre, et il le reste après le verdict : on
   ne repose pas un diagnostic sur un cas déjà tranché. */
#open-diagnosis:disabled {
  color: var(--muted); background: var(--surface-2); cursor: not-allowed;
}
/* `display: block` ci-dessus l'emporterait sur l'attribut `hidden`, qui ne
   cacherait donc plus rien : les trois boutons occupaient la grille en même
   temps, dont deux en blanc sur blanc. */
#open-diagnosis[hidden], #answer-submit[hidden],
#verdict-next[hidden], #answer-cancel[hidden] { display: none; }

/* La fenêtre du diagnostic. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgb(10 14 20 / 55%);
}
.modal[hidden] { display: none; }
.modal-card {
  /* Large plutôt qu'étroite : le débriefing tient en quatre paragraphes, et
     à 520 px ils s'étirent sur assez de lignes pour faire apparaître un
     ascenseur. À 720 le même texte perdait déjà 85 px de hauteur.

     Porté à 940 le 21 septembre 2026 : les deux blocs de sortie — l'action et
     les voisines — ont pris la place qu'ils méritaient, et l'ascenseur est
     revenu avec eux. Un verdict qu'il faut dérouler se lit mal, et c'est le
     dernier écran du cas : c'est là que l'élève apprend. */
  width: min(940px, 100%);
  max-height: 100%;
  overflow-y: auto;
  padding: 20px 28px;
  background: var(--surface);
  border-radius: 16px;
  box-shadow: 0 24px 60px rgb(0 0 0 / 35%);
}
.modal-card h2 { margin-top: 0; margin-bottom: 12px; }
/* Le débriefing est dense : on lui rend de la hauteur là où elle ne coûte
   pas en lisibilité, pour qu'un verdict n'ait jamais à se dérouler. */
#verdict h3 { margin: 14px 0 4px; }
#verdict p { margin: 7px 0; }
.modal-card label { display: block; margin: 12px 0 4px; font-size: 13px; color: var(--muted); }
.modal-card select {
  width: 100%; font: inherit; padding: 9px 10px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
}
.modal-action { display: grid; gap: 8px; margin-top: 14px; }

/* Ce qui manque se voit — bordure et fond, pas seulement la bordure : un
   liseré rouge sur un champ vide se remarque mal au milieu de trois autres. */
.modal-card select.missing {
  border-color: var(--alert);
  border-width: 2px;
  background: var(--alert-tint);
}
.answer-missing {
  margin: 14px 0 0;
  padding: 10px 13px;
  font-size: 14px;
  color: var(--alert);
  background: var(--alert-tint);
  border: 1px solid var(--alert);
  border-radius: 8px;
}
.answer-missing[hidden] { display: none; }

/* Le verdict se lit d'un coup d'œil : trouvé, trouvé mais mal fait, raté.
   Un bandeau plein plutôt qu'un simple texte coloré — c'est la première chose
   que l'élève cherche des yeux en ouvrant la fenêtre, et elle doit se voir
   avant d'être lue. */
.verdict-line {
  margin: 0 0 16px;
  padding: 12px 15px;
  font-size: 18px;
  font-weight: 650;
  border: 1px solid;
  border-radius: 10px;
}
#verdict[data-status="right"] .verdict-line {
  color: var(--ok); background: var(--ok-tint); border-color: var(--ok);
}
#verdict[data-status="partial"] .verdict-line {
  color: var(--warn); background: var(--warn-tint); border-color: var(--warn);
}
#verdict[data-status="wrong"] .verdict-line {
  color: var(--alert); background: var(--alert-tint); border-color: var(--alert);
}
.if-left {
  margin: 12px 0;
  padding: 10px 13px;
  font-size: 14px;
  color: var(--warn);
  background: var(--warn-tint);
  border-radius: 8px;
}
.if-left[hidden] { display: none; }
.discriminator {
  padding: 10px 12px; background: var(--surface-2);
  border-left: 3px solid var(--accent); border-radius: 0 7px 7px 0;
}

/* Les deux blocs de sortie du verdict ----------------------------------------
   Ce qu'il fallait faire, et ce qu'on confondra la prochaine fois. C'est tout
   ce que l'élève emporte du cas ; ils étaient écrits en gris clair, plus
   petits que le reste, sous le débriefing.

   L'action porte la teinte du « juste » quel que soit le verdict : elle dit la
   bonne réponse, pas la note obtenue — c'est la bande colorée en haut de la
   fenêtre qui juge, et elle le fait déjà sans ambiguïté. */
.debrief-block {
  margin-top: 10px;
  padding: 11px 14px;
  border-radius: 9px;
}
.debrief-block[hidden] { display: none; }
.debrief-block h4 {
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.debrief-block p { margin: 0; font-size: 15px; }

.debrief-action { background: var(--ok-tint); border: 1px solid var(--ok); }
.debrief-action h4 { color: var(--ok); }

.debrief-near { background: var(--surface-2); border: 1px solid var(--border); }
.debrief-near h4 { color: var(--muted); }
.debrief-near p { font-weight: 500; }


/* ── Les essais ────────────────────────────────────────────────────────────
   Sous la sacoche, et de même nature : un geste qui coûte du temps et rend
   une information. La différence tient en un mot — un instrument se pose et
   se retire, un essai ne se fait qu'une fois. D'où le bouton qui se désactive
   et le résultat qui reste sous lui : on peut le relire sans le repayer. */
.tests { margin: 8px 0 0; padding: 0; list-style: none; }
.tests li { margin-bottom: 10px; }

.test {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: 8px 10px;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}
.test:hover:not(:disabled) { border-color: var(--accent); }
.test:disabled { cursor: default; opacity: 0.65; }
.test-name { font-size: 13px; line-height: 1.25; }
.test-cost { font-size: 11px; color: var(--muted); }

/* La précaution se lit AVANT de cliquer, pas après : « on ouvre le circuit »
   n'est pas un commentaire, c'est ce qui doit faire hésiter. */
.test-precaution {
  margin: 4px 0 0;
  font-size: 11px;
  font-style: italic;
  line-height: 1.35;
  color: var(--muted);
}
.test-result {
  margin: 4px 0 0;
  padding: 6px 8px;
  font-size: 12px;
  line-height: 1.35;
  background: var(--surface-2);
  border-left: 2px solid var(--accent);
  border-radius: 0 4px 4px 0;
}
.tests-empty { font-size: 12px; font-style: italic; color: var(--muted); }

/* La loupe, §6 quater ------------------------------------------------------
   « On ne mâche pas le travail de l'élève » : l'état d'un organe ne se lit
   plus à côté du dessin, il se regarde. Le curseur l'annonce, le clic
   agrandit. */

/* L'enveloppe du dessin est TRANSPARENTE à la mise en page. Elle n'existe que
   pour qu'afficher un cas — qui réécrit tout son contenu — n'emporte pas la
   loupe, qui est sa sœur. Sans `display: contents`, elle s'interpose entre la
   scène et le dessin : mesuré, le synoptique tombait de 850 à 300 pixels de
   large. */
#scene { display: contents; }

[data-inspect] { cursor: zoom-in; }
[data-inspect] .body,
[data-inspect] .coil,
[data-inspect] .tray,
[data-inspect] .fan-ring { transition: filter 120ms ease-out; }
[data-inspect]:hover .body,
[data-inspect]:hover .coil,
[data-inspect]:hover .tray,
[data-inspect]:hover .fan-ring { filter: brightness(0.94); }
[data-inspect]:hover text.label { font-weight: 600; }

.inspect {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--veil);
}
/* `display: flex` écrase le `display: none` que l'attribut HTML `hidden`
   apporte : le voile restait posé sur tout le synoptique, invisible et
   cliquable, et plus aucun organe ne recevait le clic. Il faut le dire. */
.inspect[hidden] { display: none; }

.inspect-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(420px, 82%);
  padding: 20px;
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 18px 48px rgb(20 28 38 / 28%);
  /* Le zoom part de l'organe cliqué : `transform-origin` est posé en
     JavaScript, à sa place dans la scène. */
  animation: loupe-ouvre 220ms cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}
.inspect[data-state="closing"] .inspect-card {
  animation: loupe-ferme 180ms ease-in both;
}

@keyframes loupe-ouvre {
  from { transform: scale(0.12); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
@keyframes loupe-ferme {
  from { transform: scale(1); opacity: 1; }
  to { transform: scale(0.12); opacity: 0; }
}

.inspect-card h2 {
  margin: 0;
  font-size: 1.05rem;
  color: var(--ink);
}

/* L'organe agrandi. Une hauteur fixe pour que la fiche ne saute pas d'un
   organe à l'autre : un condenseur et un voyant n'ont pas la même taille, et
   c'est le cadre qui s'adapte, pas l'inverse. */
.inspect-view {
  height: 240px;
  padding: 10px;
  border-radius: 10px;
  background: var(--stage-bg, var(--surface-sunken, #f6f8fa));
}
.inspect-view svg { width: 100%; height: 100%; }
/* Rien ne se clique dans l'image de la fiche : c'est une image. La ceinture
   par-dessus les bretelles du `data-inspect` retiré au clone. */
.inspect-view { pointer-events: none; }

.inspect-card button {
  align-self: flex-end;
  padding: 8px 18px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.inspect-card button:hover { filter: brightness(1.08); }

@media (prefers-reduced-motion: reduce) {
  .inspect-card { animation: none; }
}
