/* Treatment plan viewer — estate palette and typography.
   Saira is served from this host; nothing loads cross-origin. */
@font-face {
  font-family: "Saira";
  src: url("../fonts/SairaW.woff2") format("woff2");
  font-weight: 100 900;                  /* variable axis, not fixed weights */
  font-display: swap;
}

:root {
  /* Slightly lifted from the estate's #070B14: against near-black the pink
     gingiva reads as if it is glowing. Panels move up with it to keep the
     same separation from the stage. */
  --bg:        #1C2536;
  --bg-raised: #232E42;
  --bg-card:   #253046;
  --line:      rgba(148,178,214,.16);
  --text:      #E6EDF7;
  --text-soft: #A7B4C8;
  --text-dim:  #6B7A92;
  --accent:    #5EEAD4;
  --radius:    14px;
  --ease:      cubic-bezier(.22,.61,.36,1);
}

* { box-sizing: border-box; }
/* ⚠ Must outrank the components below: any rule setting `display` defeats the
   `hidden` attribute, which is how the timeline showed on an empty page. */
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: "Saira", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 15px;
  overflow: hidden;
  -webkit-user-select: none;
  user-select: none;
}

#stage { position: fixed; inset: 0; width: 100%; height: 100%; display: block;
         touch-action: none; cursor: grab; }
#stage:active { cursor: grabbing; }

/* The centre of the VIEW. Fixed to the viewport, never to the model: the case
   is orbited, dragged and zoomed underneath it and this does not move, which
   is what makes it a reference at all.

   ⚠ pointer-events: none IS LOAD-BEARING. It sits directly over the middle of
   the canvas, which is where every drag starts — leave it interactive and it
   swallows the gesture the viewer exists for. It is decoration only.

   ⚠ mix-blend-mode: difference so it stays legible against both the pale
   background and dark enamel. A fixed colour disappears into one or the
   other, and the middle of the model is exactly where the tones vary most. */
#centermark {
  position: fixed; left: 50%; top: 50%;
  width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px;
  pointer-events: none; z-index: 2;
  mix-blend-mode: difference;
}
/* ⚠ 2px ARMS, up from 1px (Daniel, 2026-08-25: "the middle cross i would
   prefer a bit thicker"). A hairline cross disappears over enamel, which is
   the one place it has to be readable. The offsets are half the arm width, so
   changing the 2px means changing the -1px with it. */
#centermark::before, #centermark::after {
  content: ""; position: absolute; background: #fff;
}
#centermark::before { left: 50%; top: 0; width: 2px; height: 100%; margin-left: -1px; }
#centermark::after  { top: 50%; left: 0; height: 2px; width: 100%; margin-top: -1px; }

/* ---- the ruler ----
   ⚠ pointer-events NONE, exactly like #centermark: the overlay covers the
   whole canvas, and the clicks it draws from have to reach the canvas
   underneath. Making it interactive would stop the tool taking its own second
   point. */
#measure {
  position: fixed; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 3;
}
/* The crosshair is the only affordance saying the next click measures rather
   than rotates. */
body.measuring #stage { cursor: crosshair; }
body.measuring #stage:active { cursor: crosshair; }

#measure .mpoint { fill: var(--accent); stroke: #0b1220; stroke-width: 1.5; }
#measure .mline  { stroke: var(--accent); stroke-width: 2; stroke-dasharray: 5 4; }
/* Halo first, fill on top - see measure.js. A single fill vanishes against
   enamel one moment and the dark background the next. */
#measure .mlabel-halo {
  fill: none; stroke: #0b1220; stroke-width: 4.5; stroke-linejoin: round;
  font: 600 .82rem var(--font, system-ui, sans-serif); paint-order: stroke;
}
#measure .mlabel {
  fill: var(--accent);
  font: 600 .82rem var(--font, system-ui, sans-serif);
}

/* Every floating box shares one treatment: raised surface, hairline, radius —
   the rounded text boxes the OnyxCeph viewer never had. */
.panel {
  /* ⚠ FALLBACK FIRST, AND IT MUST STAY. color-mix() needs Safari 16.2 /
     Chrome 111 / Firefox 113, but the WebGL2 gate admits Safari 15 — so
     without this, iOS 15-16.1 loaded the viewer and got TRANSPARENT panels
     with text over the model. Exact same colour: color-mix with transparent
     is premultiplied, so it is #253046 at alpha .88. */
  background: rgba(37, 48, 70, .88);
  background: color-mix(in srgb, var(--bg-card) 88%, transparent);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  -webkit-backdrop-filter: blur(14px);   /* Safari needs the prefix until 18 */
  backdrop-filter: blur(14px);
  box-shadow: 0 10px 34px rgba(0,0,0,.42);
}

.brandbar {
  position: fixed; top: 14px; left: 18px; z-index: 5;
  display: flex; align-items: center; gap: .7rem;
  pointer-events: none;
}
/* The AlignerManager mark from /login. It carries its own gradient rather
   than taking currentColor, so one file works on any background. */
.brandbar .mark { width: 2.75rem; height: 2.75rem; display: block; }
.brandtext { display: flex; flex-direction: column; gap: .18rem; }
/* Saira, matching the nevident.eu brand treatment: tight tracking, weight
   600, optical size well above body text. */
.brandname {
  color: var(--text); font-weight: 600; font-size: 1.42rem;
  letter-spacing: -.015em; line-height: 1;
}
.tagline {
  color: var(--text-soft); font-weight: 400; font-size: .78rem;
  letter-spacing: .005em; line-height: 1;
}
.caseref {
  position: fixed; left: 18px; bottom: 18px; z-index: 5;
  padding: .5rem .82rem .58rem;
  border: 1px solid var(--line); border-radius: 12px;
  background: rgba(37, 48, 70, .82);            /* see .panel — keep first */
  background: color-mix(in srgb, var(--bg-card) 82%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  font-size: .66rem; color: var(--text-dim);
  letter-spacing: .13em; text-transform: uppercase; line-height: 1;
  white-space: nowrap; pointer-events: none;
}
.caseref b {
  display: block; margin-top: .32rem;
  color: var(--text); font-weight: 600; font-size: 1.34rem;
  letter-spacing: .02em; text-transform: none; line-height: 1;
  font-variant-numeric: tabular-nums;
}


.viewcube {
  position: fixed; top: .875rem; right: 1rem; z-index: 5;
  display: flex; gap: 2px; padding: 5px;
}
.viewcube button, .iconbtn {
  -webkit-appearance: none; appearance: none;   /* unprefixed is Safari 15.4+ */
  background: transparent; border: 0; color: var(--text-soft);
  font: inherit; font-size: .8rem; font-weight: 500;
  height: 1.875rem; padding: 0 .68rem; white-space: nowrap;
  border-radius: 9px; cursor: pointer;
  transition: background .18s var(--ease), color .18s var(--ease);
}
/* The two cycling buttons change label with state, so they must not resize
   the toolbar as they do — pin them to their widest word. */
/* ⚠ PINNED PER LANGUAGE, and it has to be. The pin stops the toolbar resizing
   as the label cycles, so it must fit the WIDEST of the three states — which
   is not the same word in every language:

       en  Lower 62px        de  Unterkiefer 95px
       hu  Felső 56px        sk  Horný 62px

   One pin sized for German would leave the English button ~40px of empty
   padding. <html lang> is maintained by I18N.apply(), so CSS can do this
   without a line of JavaScript. Values carry ~10% slack for Safari's and
   Firefox's metrics differing from Chromium's. */
#btnOcclusal { min-width: 4.4rem; }
html[lang="de"] #btnOcclusal { min-width: 6.4rem; }
#btnProj { min-width: 7.2rem; }
.iconbtn { min-width: 2.375rem; padding: 0; }
.viewcube button:hover, .iconbtn:hover { background: rgba(94,234,212,.14); color: var(--accent); }
.viewcube button.on { background: rgba(94,234,212,.20); color: var(--accent); }
/* Dimmed = that layer is switched off. */
.viewcube button.off { opacity: .38; }
/* Working, off-thread — the page stays live, so this is a hint rather than a
   blocking spinner. */
.viewcube button.busy { color: var(--accent); }
.viewcube button.busy::after {
  content: ""; display: inline-block; width: 5px; height: 5px;
  margin-left: .38rem; vertical-align: middle; border-radius: 50%;
  background: currentColor; animation: heatpulse 1s ease-in-out infinite;
}
@keyframes heatpulse { 0%,100% { opacity: .25; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .viewcube button.busy::after { animation: none; opacity: .8; }
}

.tunebtn { position: fixed; top: 3.625rem; right: 1rem; z-index: 5; width: 2.375rem; height: 2.375rem; font-size: 1rem; }
/* Info sits immediately left of the gear: 38px button + 8px gap. */
.infobtn {
  position: fixed; top: 3.625rem; right: 3.875rem; z-index: 5;
  width: 2.375rem; height: 2.375rem;
  font-size: .95rem; font-weight: 700; font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
}

/* The arch rail: left margin, vertically centred, where the clinic remembered
   it. A column rather than the toolbar's row, so it reads as a separate
   setting — which arch, not which angle.

   ⚠ translateY(-50%) is what centres it, NOT a flex parent: the rail is a
   fixed-position sibling of everything else, so there is nothing to centre it
   inside. top:50% puts its TOP edge at the middle; the transform pulls it back
   by half its own height. */
.archrail {
  position: fixed; left: 1rem; top: 50%; transform: translateY(-50%); z-index: 5;
  display: flex; flex-direction: column; gap: 2px; padding: 5px;
}
/* Bigger than the toolbar's buttons on purpose: this is a wayfinding control a
   clinician hits repeatedly, and it is the only icon-only control on the left
   edge, so there is room. */
.archrail button {
  -webkit-appearance: none; appearance: none;   /* unprefixed is Safari 15.4+ */
  background: transparent; border: 0; color: var(--text-soft);
  width: 3rem; height: 3rem; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px; cursor: pointer;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.archrail svg { width: 2.05rem; height: 2.05rem; display: block; }
/* currentColor throughout, so the hover and selected colours drive the icon
   without a second set of rules. */
.archrail svg path {
  fill: none; stroke: currentColor; stroke-width: 2.9;
  stroke-linecap: round;
}
/* ⚠ THE CONTRAST BETWEEN THE TWO ARCHES IS THE WHOLE MESSAGE OF THE ICON, and
   the first attempt was too subtle to read at a glance — .24 opacity alone left
   two curves of the same weight, so the icon said "two arches" rather than
   "this one, not that one".
   Three differences now carry it, not one: much fainter, visibly thinner, and
   dashed. Dashing matters most — it survives a low-contrast screen, a bright
   surgery and a colour-blind reader, none of which opacity alone does.
   The hidden arch is kept rather than dropped because a single curve on its own
   is ambiguous about WHICH jaw it is; the ghost gives it a position to sit in. */
.archrail svg .ghost {
  opacity: .3; stroke-width: 1.5; stroke-dasharray: 2.2 2.6;
}
/* The selected arch of the selected button: full strength, no softening. */
.archrail button.on svg path:not(.ghost) { stroke-width: 3.2; }
.archrail button:hover { background: rgba(94,234,212,.14); color: var(--accent); }
.archrail button.on { background: rgba(94,234,212,.20); color: var(--accent); }
.archrail button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ⚠ A FINGER IS NOT A MOUSE POINTER, AND rem SHRINKS THE TARGET.
   The interface scales by root font size — 13px below 480px — so 2.375rem, a
   comfortable 38px on a desktop, lands at 31px on a phone. Measured at 375x812:
   31x31, under the 44px every touch guideline asks for, on a control a clinician
   taps repeatedly while holding a tablet in the other hand.
   Floor it in PIXELS, not rem, or the scaling defeats the fix. The icon itself
   is left alone — it is the hit area that was short, not the drawing. */
@media (pointer: coarse) {
  .archrail button { min-width: 44px; min-height: 44px; }
}

/* ⚠ The rail shares the left margin with the case reference and the language
   picker, which are pinned to the BOTTOM left. They only collide on a short
   viewport, where a vertically centred rail reaches down into them. Measured:
   three 48px buttons plus padding is 154px, so half of it is 77px below centre;
   below ~600px of height that overlaps the 118px reserved at the bottom. */
@media (max-height: 600px) {
  .archrail { top: 7.5rem; transform: none; }
}

.tune {
  position: fixed; top: 104px; right: 16px; z-index: 6;
  width: 264px; padding: 1rem 1.05rem 1.15rem;
  max-height: calc(100vh - 130px); overflow-y: auto;
}
.tune h2 {
  margin: 0 0 .85rem; font-size: .72rem; font-weight: 400;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim);
}
.tune label { display: block; font-size: .78rem; color: var(--text-soft); margin-bottom: .85rem; }
.tune output { float: right; color: var(--accent); font-variant-numeric: tabular-nums; }
.tune small { display: block; color: var(--text-dim); font-size: .68rem; margin-top: .25rem; }
.tune input[type=range], .track input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 3px; margin-top: .45rem;
  background: rgba(148,178,214,.22); border-radius: 3px; outline: none;
}
/* The timeline rail gets a 20px hit area with a 3px visual line inside it.
   A 3px-tall target is almost impossible to grab, especially on a touchscreen,
   and scrubbing a plan is the single most-used gesture in this viewer. */
/* ⚠ margin: 0 AND NOT margin-top: 0. The browser gives a range input a
   default horizontal margin of 2px, so the thumb's travel is measured inside a
   box inset 2px from .track while .ticks are inset from .track itself — and
   the marks land 2px off, asymmetrically, for a reason nothing in this file
   would explain. Zeroing it makes the rail, the input and the tick layer share
   one origin, which is what lets the half-thumb inset below be exactly right
   instead of approximately right. */
.track input[type=range] {
  height: 20px; margin: 0; background: transparent; cursor: grab;
  position: relative; z-index: 1;
}
.track input[type=range]:active { cursor: grabbing; }
.track input[type=range]::-webkit-slider-runnable-track {
  height: 3px; background: rgba(148,178,214,.22); border-radius: 3px;
}
.track input[type=range]::-moz-range-track {
  height: 3px; background: rgba(148,178,214,.22); border-radius: 3px;
}
/* ⚠ ONE SOURCE FOR THE THUMB SIZE, because two things depend on it and they
   were 8px apart. A range thumb's CENTRE travels from half-a-thumb to
   width-minus-half-a-thumb — it cannot reach the physical ends — while .ticks
   and .archmarks were laid out across the FULL width. So the last tick sat
   8px to the right of the dot it marks, and the first 8px to the left.
   Daniel, 2026-08-25: "the final marking line should be the midline of the
   dot ... same for beginning of the timeline".
   Change this one number and the rail follows it. Hard-code it in a second
   place and the two drift apart again, which is how they got here. */
.track input[type=range]::-webkit-slider-thumb {
  margin-top: -6.5px; width: var(--thumb); height: var(--thumb);
}
.track input[type=range]::-moz-range-thumb { width: var(--thumb); height: var(--thumb); }
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); cursor: pointer; border: 0;
}
input[type=range]::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%; background: var(--accent);
  cursor: pointer; border: 0;
}
button.wide {
  width: 100%; margin-top: .3rem; padding: .5rem;
  background: rgba(94,234,212,.12); color: var(--accent);
  border: 1px solid rgba(94,234,212,.28); border-radius: 8px;
  font: inherit; font-size: .78rem; cursor: pointer;
}
button.wide:hover { background: rgba(94,234,212,.2); }

.timeline {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 18px; z-index: 5;
  display: flex; align-items: center; gap: .95rem;
  padding: 1.35rem 1.1rem .75rem; width: min(1120px, calc(100vw - 32px));
}
.timeline button {
  -webkit-appearance: none; appearance: none;
  background: rgba(94,234,212,.13); color: var(--accent);
  border: 0; width: 34px; height: 34px; border-radius: 50%;
  font-size: .85rem; cursor: pointer; flex: none;
}
/* The step arrows sit either side of Play, quieter than it — Play is the one
   thing a patient is meant to reach for, and three equally loud circles in a
   row would bury it. The chevrons are set larger because ‹ › are small on
   their own em. */
.timeline button.stepbtn {
  background: rgba(94,234,212,.07);
  font-size: 1.25rem; line-height: 1;
}
.timeline button.stepbtn:hover { background: rgba(94,234,212,.22); }
/* ⚠ Disabled at the ends of the plan rather than hidden: a control that
   vanishes moves the two beside it, and Play would jump under the cursor at
   the exact moment somebody is clicking through steps. */
.timeline button.stepbtn:disabled {
  opacity: .3; cursor: default; background: rgba(94,234,212,.05);
}
.timeline button:hover { background: rgba(94,234,212,.22); }
.timeline button.speed {
  background: transparent; border: 1px solid var(--line);
  color: var(--text-soft); font-size: .74rem; font-weight: 500;
  width: auto; min-width: 2.5rem; border-radius: 9px;
  font-variant-numeric: tabular-nums;
}
.timeline button.speed:hover { background: rgba(94,234,212,.12); color: var(--accent); }
.timeline button.speed.on { border-color: rgba(94,234,212,.4); color: var(--accent); }
/* Room under the rail for the step numbers, and under those for the lower
   arch marker. The upper marker sits above the rail, in the timeline's own
   top padding. */
.track {
  position: relative; flex: 1; min-width: 0; padding-bottom: 1.7rem;
  /* The thumb diameter — see the slider-thumb rules above. Everything that has
     to line up with the dot is inset by half of it. */
  --thumb: 16px;
}
/* ⚠ INSET BY HALF A THUMB, both sides. This is the coordinate space the DOT
   moves in, not the one the rail occupies: 0% here is the dot's centre at step
   0 and 100% is its centre at the last step. Set these back to 0 and every
   tick drifts outward again, worst at the two ends. */
.archmarks {
  position: absolute; left: calc(var(--thumb) / 2); right: calc(var(--thumb) / 2);
  top: 0; pointer-events: none;
}
/* Solid triangles drawn with borders — crisper at this size than a glyph, and
   they cannot be shifted by a font substitution. Each points INWARD at the
   step number on the rail: the upper arch marker sits above and points down,
   the lower arch marker sits below and points up. */
.archmarks i {
  position: absolute; width: 0; height: 0;
  transform: translateX(-50%);
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  pointer-events: auto; cursor: help;
}
.archmarks i.up { top: -12px; border-top: 10px solid var(--accent); }
.archmarks i.dn { top: 33px;  border-bottom: 10px solid #F0B36B; }
/* ⚠ pointer-events NONE on the layer, AUTO on the numbers only.
   This overlay covers the rail; leaving it interactive meant every attempt to
   grab the slider thumb hit the tick layer instead and the drag never started.
   Only the numbers themselves need to be clickable. */
/* Same inset as .archmarks, and for the same reason — this is the dot's
   coordinate space, not the rail's. */
.ticks {
  position: absolute; left: calc(var(--thumb) / 2); right: calc(var(--thumb) / 2);
  top: 0; height: 20px; pointer-events: none;
}
/* ⚠ translateX(-50%) SO THE TICK IS CENTRED ON ITS VALUE, not started at it.
   Without it a 2px tick placed at `left: 40%` puts its LEFT EDGE on 40% and
   sits a pixel to the right of the step it marks — uniformly, at every tick,
   which is why it read as "close enough" for so long. It is not close enough
   at the two ends, where the mark is supposed to run through the middle of the
   dot. .archmarks i has always done this; .ticks i was the one that did not.
   The number inside centres itself on the tick (.ticks b), so it follows. */
.ticks i {
  position: absolute; top: 50%; width: 2px; height: 8px; margin-top: -4px;
  transform: translateX(-50%);
  background: rgba(148,178,214,.4); border-radius: 1px;
}
/* The number is the click target, not the 2px tick — a hairline is not a
   hit area anyone can land on. */
.ticks b {
  position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
  font-size: .66rem; font-weight: 400; line-height: 1;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
  padding: .18rem .3rem; border-radius: 5px; cursor: pointer;
  pointer-events: auto; transition: color .15s var(--ease), background .15s var(--ease);
}
.ticks b:hover { color: var(--accent); background: rgba(94,234,212,.12); }
.ticks i.now { background: var(--accent); height: 11px; margin-top: -5px; }
.ticks i.now b { color: var(--accent); font-weight: 500; }
.steplabel {
  flex: none; font-size: .8rem; color: var(--text-soft);
  min-width: 8.5rem; text-align: right;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.boot, .fail {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 20;
  padding: 1.5rem 1.9rem; text-align: center; min-width: 260px;
}
.boot p { margin: .9rem 0 .8rem; color: var(--text-soft); font-size: .86rem; }
.bar { height: 3px; background: rgba(148,178,214,.18); border-radius: 3px; overflow: hidden; }
.bar span { display: block; height: 100%; width: 0; background: var(--accent); transition: width .2s var(--ease); }
.spinner {
  width: 26px; height: 26px; margin: 0 auto;
  border: 2px solid rgba(94,234,212,.22); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.fail { color: #FCA5A5; font-size: .88rem; max-width: 34rem; }

@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.4s; } }
@media (max-width: 620px) {
  .tune { width: calc(100vw - 32px); left: 16px; right: 16px; }
  .steplabel { display: none; }
}

/* ── Language picker, bottom right ──────────────────────────────────────────
   Mirrors .caseref in the opposite corner: same chip treatment, same lift
   above the timeline once the centred rail reaches the corners. */
.langpick {
  position: fixed; right: 18px; bottom: 18px; z-index: 5;
  display: flex; gap: .1rem; padding: .3rem;
  border-radius: 12px;
}
.langpick button {
  -webkit-appearance: none; appearance: none;
  border: 0; background: transparent; cursor: pointer;
  padding: .34rem .5rem; border-radius: 8px;
  font: inherit; font-size: .68rem; font-weight: 600; letter-spacing: .06em;
  color: var(--text-dim); line-height: 1;
  transition: background .12s, color .12s;
}
.langpick button:hover { background: rgba(94,234,212,.12); color: var(--text); }
.langpick button[aria-current="true"] {
  background: rgba(94,234,212,.22); color: var(--accent);
}
.langpick button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── Corner chips vs the timeline ───────────────────────────────────────────
   The timeline is min(1120px, 100vw - 32px), centred. Below ~1454px its edge
   reaches the corner chips, so both must lift above it. MEASURED at 1280x720,
   where the old 1180px breakpoint left the case chip and the language picker
   sitting on top of the rail.

   118px, not 92px: the timeline occupies the bottom 107px (89 tall + 18
   offset), so the previous lift still clipped it. */
@media (max-width: 1460px) {
  .caseref,
  .langpick { bottom: 118px; }
}

/* ── Info panel ─────────────────────────────────────────────────────────────
   Shares .tune's box so the two dropdowns are visibly the same object; only
   the contents differ (prose, not sliders). */
.info h3 {
  margin: .95rem 0 .18rem; font-size: .72rem; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--accent);
}
.info h3:first-of-type { margin-top: .7rem; }
.info p {
  margin: 0; font-size: .78rem; line-height: 1.45; color: var(--text-soft);
}
.info p.lede {
  color: var(--text); padding-bottom: .55rem;
  border-bottom: 1px solid var(--line);
}

/* ── Toolbar overflow, and scaling the whole interface ──────────────────────

   MEASURED: the toolbar is 657px in English but 750px in German, and German's
   longer tagline widens the brand bar too, so the worst case needs ~1206px
   before the two meet. 1240px collapses before that happens in ANY of the four
   languages — a breakpoint fitted to English would still overlap in German.

   Front / Right / Left / Occlusal stay visible because they are what a patient
   actually uses; projection, attachments, occlusogram and reset move behind
   the button. */
/* ⚠ ALWAYS a dropdown, at every width — Daniel, 2026-08-08: "It looks nicer."
   It began as a narrow-screen measure; keeping it everywhere means one layout
   to reason about instead of two, and the toolbar stops being 750px of German
   across the top of a desktop. Front / Right / Left / Occlusal stay out. */
.more {
  display: none;
  position: absolute; top: calc(100% + 6px); right: 0;
  flex-direction: column; gap: 2px; padding: 5px; min-width: 9.5rem;
  background: rgba(37, 48, 70, .96);      /* see .panel — fallback first */
  background: color-mix(in srgb, var(--bg-card) 96%, transparent);
  border: 1px solid var(--line); border-radius: var(--radius);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: 0 10px 34px rgba(0,0,0,.42);
}
.more.open { display: flex; }
/* Left-align in the dropdown: centred labels in a vertical list read as a
   ragged column on both edges. display:flex is load-bearing — justify-content
   does nothing on the default inline-block button. */
.more button {
  display: flex; align-items: center; justify-content: flex-start;
  text-align: left; width: 100%;
}
/* ⚠ ONLY #btnProj. The pinned widths stop the horizontal bar resizing as a
   cycling label changes; in the column they merely force the panel wider — but
   #btnOcclusal STAYS in the bar and still cycles Occlusal/Upper/Lower, so
   unpinning it makes the toolbar jump on every click. */
#btnProj { min-width: 0; }
.morebtn { display: inline-flex; align-items: center; justify-content: center; }

/* ---- Pill tooltips -----------------------------------------------------
   ⚠ THE SAME PILL AS THE REST OF THE ESTATE, DRAWN BY HAND (2026-08-09).
   Daniel: "unify how title/tooltip is used throughout the app including
   viewer. I don't like title element at all." The app gets this from
   Bootstrap, restyled in app.css; the viewer ships no Bootstrap and no
   Popper and is not going to start for a tooltip — a plan link is opened by
   patients on phones, and 80 KB for one control is the wrong trade. So
   tip.js draws it instead.

   ⚠ THESE NUMBERS ARE COPIED FROM .tooltip-inner IN app.css AND MUST TRACK
   IT. Padding, radius, size, line height and shadow are deliberately
   identical. The colours are the app's DARK-theme pair, because this
   interface is always dark — a dark pill on a dark stage would disappear.
   The typeface is Saira rather than the app's Noto Sans on purpose: the pill
   should look native to the page it floats over.

   Not a <div> per control but ONE element reused, so a plan with 40 steps
   does not carry 40 hidden nodes. */
.am-tip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 30;                 /* over every control; the boot overlay is 20 */
  /* ⚠ max-content IS LOAD-BEARING, NOT TIDINESS (measured 2026-08-09). Without
     it a position:fixed box with only `left` set shrink-wraps to the space
     LEFT OF THE RIGHT EDGE - and tip.js measures the pill before it moves it,
     so a pill last shown near the right edge was measured with ~80px to work
     in. The Reset tooltip came out 83px wide and 248px TALL, a column of
     single words, which then broke the above/below decision as well.
     max-content makes the measurement independent of where it currently sits;
     max-width is what still forces a long sentence to wrap. */
  width: max-content;
  max-width: 22rem;
  padding: .35rem .8rem;
  border-radius: 999px;
  background: #e8ecf2;
  color: #1b2027;
  font-size: .875rem;
  line-height: 1.4;
  box-shadow: 0 .35rem .9rem rgba(0,0,0,.5);
  pointer-events: none;        /* must never eat the hover that summoned it */
  opacity: 0;
  transition: opacity .12s var(--ease);
}

.am-tip.on { opacity: 1; }

/* One knob for the whole interface: sizes are rem, so this scales buttons,
   type, padding and the fixed offsets together. */
@media (max-width: 1240px) { html { font-size: 15px; } }
@media (max-width: 820px)  { html { font-size: 14px; } }
@media (max-width: 480px)  { html { font-size: 13px; } }

/* The tagline is charm, not information — first thing to go when width is
   short, so the brand mark and the toolbar keep their room. */
@media (max-width: 700px) { .tagline { display: none; } }

/* ⚠ While the overflow menu is open the toolbar must out-rank the gear and
   info buttons. .viewcube is position:fixed with z-index 5, so it opens a
   stacking context its children cannot climb out of, and .tunebtn/.infobtn sit
   later in the document at the same z-index — they painted OVER the open menu.
   Class from JS, not :has(): Firefox only supports :has() from 121. */
.viewcube.menu-open { z-index: 8; }

/* Below this the brand wordmark and the collapsed toolbar cannot both fit —
   measured at 375px in German: 192px of brand plus 262px of toolbar in a 375px
   viewport. The mark alone is 36px and carries the identity perfectly well. */
@media (max-width: 560px) {
  .brandtext { display: none; }
}

/* The open menu necessarily covers the info and gear buttons — both sit
   directly below the toolbar at the right edge. The panel is 96% opaque, so
   they ghosted through it. They follow .viewcube in the document, hence the
   sibling combinator. */
.viewcube.menu-open ~ .infobtn,
.viewcube.menu-open ~ .tunebtn { opacity: 0; pointer-events: none; }
