/* ==========================================================================
   Peptix - shared components
   Modal, popover, toast, combo list, syringe diagram, 3D vial stage, inspector,
   dose timer and the form extras the dialogs are built from.

   Everything here is drawn from the tokens defined in app.css. There is no
   colour in this file beyond those names (the one exception is the pure white
   text on a red error toast). Dark theme comes for free through the tokens.

   Palette rule for this layer: ink and hairlines carry the structure; green
   (--accent) is reserved for live state - the liquid in the barrel, the
   reading edge and its DRAW TO label, the remaining level on the vial rail and
   the time left on the dose track. Amber is a low vial, red is empty or late.
   ========================================================================== */


/* ==========================================================================
   1. Modal
   A bottom sheet on a phone (14px top corners, flush with the bottom edge)
   and a centred 10px dialog from 640px up. The scrim is a flat ink wash;
   the dialog itself is a hairline-bordered surface with no coloured glow.
   ========================================================================== */
.scrim {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: flex-end; justify-content: center;
  background: var(--scrim);
  animation: fade var(--t-4) var(--ease);
}
/* Set by the close handler one frame before the node is removed, so a dialog
   leaves the same way it arrived instead of vanishing. */
.scrim.is-closing { animation: fade-out var(--t-2) var(--ease) forwards; }
.scrim.is-closing .modal { animation: modal-out var(--t-2) var(--ease) forwards; }

.modal {
  width: 100%; max-width: 520px; min-width: 0;
  max-height: min(92vh, 880px);
  max-height: min(92dvh, 880px);
  display: flex; flex-direction: column;
  margin: 0;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-bottom: 0;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--shadow-3);
  outline: none;
  animation: modal-in var(--t-4) var(--enter);
}
.modal.wide { max-width: 600px; }

.modal-head {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  flex: none;
}
/* The close control is the lead's 32px .act. Pulled 4px into the gutter so
   its glyph, not its hit box, lines up with the body's right edge. */
.modal-head .act { flex: none; margin-right: -4px; }
.modal-title {
  margin: 0; min-width: 0;
  font-size: 15px; font-weight: 600; line-height: 1.3; letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal-body {
  min-width: 0; padding: 16px;
  overflow-y: auto; overflow-x: clip;
  overscroll-behavior: contain; scroll-padding-block: 16px;
  display: flex; flex-direction: column; gap: 14px;
}
/* A scrollable column must let its contents keep their natural height. Without
   this, flexbox shrinks complex sections (notably the schedule editor) to fit
   the viewport and their overflow gets clipped instead of making the body
   scroll. */
.modal-body > * { flex: 0 0 auto; }
.modal-foot {
  display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--surface);
  flex: none;
}
.modal-foot .btn { flex: 1 1 auto; min-width: min(120px, 100%); }

.dialog-text { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--text); }
.dialog-text + .hint { margin-top: -6px; }

/* While a submit is in flight the whole form is frozen and the body dims; the
   spinner in the submit button is the only thing that moves. */
.is-busy { pointer-events: none; }
.is-busy .modal-body { opacity: .6; transition: opacity var(--t-2) var(--ease); }
.spin { animation: spin .8s linear infinite; }

@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fade { from { opacity: 0; } }
@keyframes fade-out { to { opacity: 0; } }
@keyframes rise { from { transform: translateY(14px); opacity: 0; } }
/* A dialog arrives from slightly small and slightly low. The travel has to be
   a noticeable fraction of the thing that is moving: 16px and 4% were too
   small to register against a full-width sheet. */
@keyframes modal-in {
  from { transform: translateY(26px) scale(.96); opacity: 0; }
}
@keyframes modal-out {
  to { transform: translateY(8px) scale(.985); opacity: 0; }
}

@media (max-width: 639.98px) {
  /* The sheet owns the bottom edge; nothing behind it may scroll into view. */
  .scrim { overflow: hidden; }
}
@media (min-width: 640px) {
  .scrim { align-items: center; padding: 24px; }
  .modal { border-bottom: 1px solid var(--border); border-radius: var(--r); }
  .modal-head { padding: 16px 20px; }
  .modal-body { padding: 20px; gap: 16px; }
  .modal-foot { padding: 14px 20px; }
  .modal-foot .btn { flex: none; min-width: 96px; }
}


/* ==========================================================================
   2. Popover and toasts
   ========================================================================== */
.pop-wrap { position: relative; }
.popover {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 240px; max-width: min(320px, calc(100vw - 24px));
  max-height: 70vh; overflow-y: auto;
  padding: 6px;
  background: var(--glass);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow-3);
  transform-origin: top right;
  animation: pop-in var(--t-2) var(--enter);
}
.popover[hidden] { display: none; }
/* Anchors that sit at the bottom of the sidebar open upward, and ones on the
   left edge open to the right. Both grow from the corner nearest the control. */
.popover.is-up { top: auto; bottom: calc(100% + 8px); transform-origin: bottom right; animation-name: pop-in-up; }
.popover.is-left { right: auto; left: 0; transform-origin: top left; }
.popover.is-up.is-left { transform-origin: bottom left; }

.pop-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%; min-height: 36px; padding: 7px 10px;
  background: none; border: 0; border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 13.5px; font-weight: 500;
  text-align: left; cursor: pointer;
  transition: background var(--t-1) var(--ease);
}
.pop-item > svg { flex: none; color: var(--text-2); }
.pop-item > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pop-item:hover { background: var(--sunken); }
.pop-item:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.pop-sep { height: 1px; margin: 6px 4px; background: var(--border); }
.pop-user { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px; }
.pop-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; }
.pop-name.small { font-size: 12px; }

@keyframes pop-in {
  from { transform: translateY(-6px) scale(.97); opacity: 0; }
}
@keyframes pop-in-up {
  from { transform: translateY(6px) scale(.97); opacity: 0; }
}

/* Toasts: a solid ink pill with page-coloured text. They clear the phone tab
   bar and the floating log button; on a desk they sit just above the fold. */
.toasts {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(100px + env(safe-area-inset-bottom)); z-index: 90;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  width: max-content; max-width: calc(100vw - 32px);
  pointer-events: none;
}
.toast {
  max-width: 100%;
  padding: 9px 14px;
  background: var(--solid); color: var(--solid-ink);
  border-radius: 999px;
  font-size: 13px; font-weight: 500; line-height: 1.4;
  box-shadow: var(--shadow-3);
  overflow-wrap: anywhere;
  animation: toast-in var(--t-3) var(--spring);
  transition: opacity var(--t-2) var(--ease), transform var(--t-2) var(--ease);
}
.toast.bad { background: var(--crit); color: #fff; }
/* Set just before removal so a toast sinks out the way it rose in. */
.toast.is-leaving { opacity: 0; transform: translateY(8px) scale(.97); }
@keyframes toast-in {
  from { transform: translateY(14px) scale(.96); opacity: 0; }
}
@media (min-width: 900px) {
  .toasts { bottom: 28px; }
}


/* ==========================================================================
   3. Combo list (searchable dropdown)
   Lives on <body> with fixed positioning, so a dialog can never clip it;
   combo.js sets its box and flips it above the field when space runs out.
   ========================================================================== */
.combo-pop {
  position: fixed; z-index: 95;
  display: flex; flex-direction: column;
  background: var(--glass);
  border: 1px solid var(--border-firm);
  border-radius: var(--r);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  transform-origin: top center;
  animation: pop-in var(--t-2) var(--enter);
}
.combo-pop.is-flipped { transform-origin: bottom center; animation-name: pop-in-up; }
.combo-list { overflow-y: auto; padding: 4px; overscroll-behavior: contain; }
.combo-item {
  display: block; width: 100%; min-height: 36px;
  padding: 8px 10px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--text);
  font: inherit; font-size: 13.5px; line-height: 1.3; text-align: left;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  cursor: pointer;
  transition: background var(--t-1) var(--ease);
}
.combo-item:hover, .combo-item.is-active { background: var(--sunken); }
.combo-item.is-active { box-shadow: inset 0 0 0 1px var(--border-firm); }
/* The matched run is bold ink, not a coloured highlight. */
.combo-item mark { background: none; color: var(--text); font-weight: 700; }
.combo-foot {
  padding: 7px 10px;
  border-top: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-3); font-size: 11.5px; line-height: 1.4;
}


/* ==========================================================================
   4. Syringe panel and diagram
   Numbers in the readout are data and set in the mono face. In the drawing,
   glass and hardware are neutral, ticks are ink, and the liquid, meniscus,
   reading edge and DRAW TO guide are the only green. Over capacity turns the
   whole measure red.
   ========================================================================== */
.syr-panel {
  padding: 14px 16px 12px;
  background: var(--sunken);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.syr-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.syr-read { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.syr-big {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 28px; font-weight: 600; line-height: 1.1; letter-spacing: -.02em;
  color: var(--text);
}
.syr-big.muted { color: var(--text-3); }
.syr-big em {
  font-family: var(--sans); font-style: normal;
  font-size: 12.5px; font-weight: 500; letter-spacing: 0;
  color: var(--text-2); margin-left: 5px;
}
.syr-alt {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 12px; color: var(--text-2);
}
.syr-model { flex: none; text-align: right; line-height: 1.25; }
.syr-model strong, .syr-model small { display: block; }
.syr-model strong { font-size: 12px; font-weight: 600; color: var(--text); }
.syr-model small { margin-top: 2px; color: var(--text-3); font-size: 11px; }
.syr-note { margin: 8px 0 0; font-size: 12px; line-height: 1.5; color: var(--text-2); }
.syr-note.bad { color: var(--crit-ink); font-weight: 500; }

.syringe { display: block; width: 100%; height: auto; margin-top: 8px; overflow: visible; }

/* glass and liquid gradients (stop colours are set here so they follow the theme) */
.syr-glass-hi  { stop-color: var(--surface); stop-opacity: .96; }
.syr-glass-lo  { stop-color: var(--sunken);  stop-opacity: .72; }
.syr-liquid-hi { stop-color: var(--accent);  stop-opacity: .42; }
.syr-liquid-lo { stop-color: var(--accent);  stop-opacity: .28; }

/* barrel */
.syr-barrel  { stroke: none; }
.syr-chamber { fill: none; stroke: var(--border); stroke-width: 1; }
.syr-outline { fill: none; stroke: var(--border-firm); stroke-width: 1.6; }
.syr-glass-line { fill: none; stroke: var(--surface); stroke-width: 2; opacity: .8; }
.syr-glass-line.low { opacity: .42; }

/* needle and hub */
.syr-needle    { fill: none; stroke: var(--text-3); stroke-width: 3.4; stroke-linecap: round; }
.syr-needle-hi { fill: none; stroke: var(--surface); stroke-width: 1; opacity: .75; }
.syr-bevel     { fill: none; stroke: var(--text-3); stroke-width: 2; stroke-linecap: round; }
.syr-hub-ring  { fill: var(--text-3); }
.syr-hub       { fill: var(--border-firm); stroke: var(--text-3); stroke-width: 1; }
.syr-hub-hi    { fill: var(--surface); opacity: .42; }
.syr-collar    { fill: var(--border-firm); stroke: var(--text-3); stroke-width: 1; }

/* plunger hardware */
.syr-rod       { fill: var(--border); stroke: var(--border-firm); stroke-width: 1; }
.syr-rod-hi    { fill: none; stroke: var(--surface); stroke-width: 2; opacity: .65; }
.syr-flange, .syr-thumb { fill: var(--border-firm); stroke: var(--text-3); stroke-width: 1; }
.syr-flange-edge, .syr-thumb-hi { fill: var(--surface); opacity: .45; }

/* liquid: the one green in the drawing */
.syr-liquid     { opacity: 1; }
.syr-tip-liquid { fill: var(--accent); opacity: .3; }
.syr-meniscus   { fill: none; stroke: var(--accent); stroke-width: 1.4; opacity: .75; }

/* stopper - its left face is where the barrel is read */
.syr-gasket       { fill: var(--text-2); stroke: var(--text); stroke-width: 1; }
.syr-gasket-ring  { stroke: var(--page); stroke-width: 1.2; opacity: .55; }
.syr-reading-edge { stroke: var(--accent); stroke-width: 2.8; }

/* printed scale: ink ticks, mono numbers haloed in the glass colour so one
   stays readable under the stopper */
.syr-tick    { stroke: var(--text-2); stroke-width: 1; opacity: .72; }
.syr-tick.md { stroke-width: 1.25; opacity: .86; }
.syr-tick.mj { stroke: var(--text); stroke-width: 1.55; opacity: 1; }
.syr-num {
  fill: var(--text-2);
  font: 600 11px var(--mono); font-variant-numeric: tabular-nums;
  stroke: var(--surface); stroke-width: 3px; paint-order: stroke fill;
}
.syr-scale-label { fill: var(--text-3); font: 600 9.5px var(--sans); letter-spacing: .1em; }

/* draw-to guide */
.syr-target { pointer-events: none; }
.syr-target-label { fill: var(--accent); font: 700 11px var(--sans); letter-spacing: .1em; }
.syr-target-line  { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 3 3; opacity: .85; }
.syr-target-arrow { fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

.syr-legend {
  display: flex; justify-content: space-between; gap: 6px 14px; flex-wrap: wrap;
  margin-top: 2px; color: var(--text-3); font-size: 11.5px; line-height: 1.4;
}
.syr-legend span { display: inline-flex; align-items: center; gap: 6px; }
.syr-edge-key { width: 3px; height: 13px; flex: none; border-radius: 2px; background: var(--accent); }

/* over capacity: the measure itself turns red */
.syr-over .syr-liquid, .syr-over .syr-tip-liquid { fill: var(--crit); opacity: .34; }
.syr-over .syr-liquid-hi, .syr-over .syr-liquid-lo { stop-color: var(--crit); }
.syr-over .syr-meniscus, .syr-over .syr-outline,
.syr-over .syr-reading-edge, .syr-over .syr-target-line,
.syr-over .syr-target-arrow { stroke: var(--crit); }
.syr-over .syr-target-label { fill: var(--crit-ink); }
.syr-over + .syr-legend .syr-edge-key { background: var(--crit); }

/* The whole diagram scales down on a phone - thicken it so it stays readable. */
@media (max-width: 600px) {
  .syr-panel { padding: 12px 12px 10px; }
  .syr-panel-head { gap: 10px; }
  .syr-big { font-size: 24px; }
  .syr-model { display: none; }
  .syr-num { font-size: 19px; stroke-width: 5px; }
  .syr-scale-label, .syr-target-label { font-size: 18px; }
  .syr-tick { stroke-width: 1.8; }
  .syr-tick.md { stroke-width: 2.1; }
  .syr-tick.mj { stroke-width: 2.6; }
  .syr-outline { stroke-width: 2.2; }
  .syr-reading-edge { stroke-width: 3.6; }
  .syr-legend { font-size: 11px; }
}


/* ==========================================================================
   5. Vial stage
   The stage is size-agnostic: it has a 96x128 default and the model shell
   fills whatever is left beside the usage rail, so page CSS sizes `.vial-stage`
   (and, if it wants, `.vial-model-shell`) and nothing here fights it.
   The only green is the remaining level on the rail; low is amber, empty is
   red, finished is grey.
   ========================================================================== */
.vial-stage {
  position: relative;
  width: 96px; height: 128px; max-width: 100%;
  flex: none; min-width: 0;
  display: flex; align-items: stretch; justify-content: center; gap: 4px;
}

/* usage rail */
.vial-usage {
  width: 18px; height: 100%; flex: none; align-self: center;
  display: flex; flex-direction: column; align-items: center;
  color: var(--text-3);
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.vial-usage-value {
  height: 16px; display: flex; align-items: baseline; justify-content: center;
  font-size: 10px; line-height: 1; font-weight: 600; color: var(--text-2);
  transition: color var(--t-2) var(--ease);
}
.vial-usage-value small { font-size: 7px; font-weight: 600; }
.vial-usage-track {
  position: relative; width: 5px; flex: 1 1 auto; min-height: 40px;
  margin: 3px 0 4px; overflow: hidden;
  background: var(--track); border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--border);
}
.vial-usage-fill {
  position: absolute; inset: auto 0 0; height: var(--vial-level, 0%);
  background: var(--accent); border-radius: 999px;
  transform-origin: center bottom;
  transition: height var(--t-3) var(--ease), background var(--t-2) var(--ease);
}
.vial-usage-label {
  font-family: var(--sans);
  font-size: 7px; line-height: 1; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
}
.vial-stage.is-low .vial-usage-track { background: var(--track-warn); }
.vial-stage.is-low .vial-usage-fill  { background: var(--warn); }
.vial-stage.is-low .vial-usage-value { color: var(--warn-ink); }
.vial-stage.is-crit .vial-usage-track { background: var(--track-crit); }
.vial-stage.is-crit .vial-usage-fill  { min-height: 3px; background: var(--crit); }
.vial-stage.is-crit .vial-usage-value { color: var(--crit-ink); }
.vial-stage.is-finished .vial-usage-track { background: var(--sunken); }
.vial-stage.is-finished .vial-usage-fill  { background: var(--text-3); }
.vial-stage.is-finished .vial-usage-value { color: var(--text-3); }

/* model shell: fills the stage beside the rail */
.vial-model-shell {
  position: relative; flex: 1 1 auto; min-width: 0; height: 100%;
  isolation: isolate; contain: layout;
}
.vial-model-halo {
  position: absolute; z-index: 0; left: 10%; right: 10%; bottom: 4px; height: 14px;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(ellipse, var(--art-shadow) 0%, transparent 72%);
  filter: blur(2px); opacity: .6;
  transform-origin: center;
  transition: opacity var(--t-2) var(--ease), transform var(--t-2) var(--ease);
}
.vial-model {
  position: absolute; z-index: 2; inset: 0;
  display: block; width: 100%; height: 100%;
  background: transparent; --poster-color: transparent;
  opacity: 0; transform: scale(.97) translateY(1px);
  pointer-events: none; touch-action: pan-y;
  transition: opacity var(--t-2) var(--ease), transform var(--t-3) var(--ease),
              filter var(--t-2) var(--ease);
}
.vial-model::part(default-progress-bar) { display: none; }
.vial-stage.is-model-ready .vial-model { opacity: 1; transform: none; }
.vial-stage.is-model-error .vial-model { display: none; }
.vial-stage.is-finished .vial-model { filter: grayscale(.6) saturate(.4); }
/* With no renderer the stage would be a blank box; a dashed silhouette says
   "a vial goes here" without pretending to be one. */
.vial-stage.is-model-error .vial-model-shell::before {
  content: ''; position: absolute; z-index: 1; inset: 10% 26% 8%;
  border: 1px dashed var(--border-firm);
  border-radius: 6px 6px 10px 10px;
  pointer-events: none;
}

/* the whole shell is the button that opens the inspector */
.vial-inspect-hit {
  position: absolute; z-index: 4; inset: 0;
  padding: 0; border: 0; border-radius: var(--r);
  background: transparent; color: var(--text-2);
  cursor: zoom-in; -webkit-tap-highlight-color: transparent;
}
.vial-inspect-hit:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.vial-inspect-badge {
  position: absolute; right: 2px; bottom: 4px;
  min-width: 30px; height: 20px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center; gap: 3px;
  border: 1px solid var(--border-firm); border-radius: 999px;
  background: var(--surface); color: var(--text-2);
  box-shadow: var(--shadow-1);
  font-family: var(--mono); font-size: 8.5px; line-height: 1; font-weight: 600; letter-spacing: .04em;
  opacity: .8; transform: translateY(1px);
  transition: opacity var(--t-1) var(--ease), color var(--t-1) var(--ease),
              border-color var(--t-1) var(--ease), transform var(--t-1) var(--ease);
}
.vial-inspect-badge svg {
  width: 10px; height: 10px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.vial-inspect-hit:hover .vial-inspect-badge,
.vial-inspect-hit:focus-visible .vial-inspect-badge {
  opacity: 1; color: var(--text); border-color: var(--text-3); transform: none;
}
@media (hover: hover) and (pointer: fine) {
  .vial-stage:hover .vial-model-halo { opacity: .85; transform: scaleX(1.08); }
}


/* ==========================================================================
   6. Inspector
   A large, user-controlled view of the model. It has no auto-rotation: once
   opened, the vial follows the drag and nothing fights the camera. The stage
   is a neutral vignette, surface at the centre falling to sunken at the edge.
   ========================================================================== */
.modal.vial-inspector-modal { max-width: 620px; }
.vial-inspector-modal .modal-body { padding: 12px 12px 14px; }
.vial-inspector { display: flex; flex-direction: column; gap: 12px; }
.vial-inspector-stage {
  position: relative; isolation: isolate; overflow: hidden;
  width: 100%;
  height: clamp(240px, 48vh, 450px);
  height: clamp(240px, 48dvh, 450px);
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: radial-gradient(ellipse at 50% 45%, var(--surface) 0%, var(--sunken) 78%);
}
.vial-inspector-halo {
  position: absolute; z-index: 1; left: 25%; right: 25%; bottom: 7%; height: 8%;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(ellipse, var(--art-shadow) 0%, transparent 72%);
  filter: blur(5px); opacity: .8;
}
.vial-inspector-model {
  position: absolute; z-index: 3; inset: 0;
  display: block; width: 100%; height: 100%;
  background: transparent; --poster-color: transparent;
  opacity: 0; transform: scale(.985);
  cursor: grab; touch-action: none; outline: none;
  transition: opacity var(--t-2) var(--ease), transform var(--t-3) var(--ease);
}
.vial-inspector-model:active { cursor: grabbing; }
.vial-inspector-model:focus-visible {
  outline: none; border-radius: var(--r);
  box-shadow: inset 0 0 0 2px var(--ring);
}
.vial-inspector-model::part(default-progress-bar) { display: none; }
.vial-inspector-stage.is-model-ready .vial-inspector-model { opacity: 1; transform: none; }
.vial-inspector-stage.is-model-error .vial-inspector-model { display: none; }
.vial-inspector-stage.is-model-error::after {
  content: ''; position: absolute; z-index: 1; left: 38%; right: 38%; top: 14%; bottom: 12%;
  border: 1px dashed var(--border-firm);
  border-radius: 10px 10px 18px 18px;
  pointer-events: none;
}

.vial-inspector-summary {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;
}
.vial-inspector-summary > span {
  min-width: 0; padding: 10px 12px;
  display: flex; flex-direction: column; gap: 3px;
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--surface);
}
.vial-inspector-summary strong {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 14px; font-weight: 600; line-height: 1.25; color: var(--text);
}
.vial-inspector-summary small { font-size: 11px; color: var(--text-3); }
.vial-inspector-hint { margin: -2px 0 0; text-align: center; font-size: 12px; color: var(--text-3); }
.vial-inspector-hint span { margin-inline: 4px; }

@media (min-width: 640px) {
  .vial-inspector-modal .modal-body { padding: 16px; }
  .vial-inspector-stage {
    height: clamp(330px, 50vh, 460px);
    height: clamp(330px, 50dvh, 460px);
  }
}


/* ==========================================================================
   7. Dose timer
   One track drains over the dose interval; the last dose and the due date
   sit beneath it. vial.js updates --dose-progress once a second, so the
   width transition is a slow linear slide, never a snap.
   ========================================================================== */
.dose-timer {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 12px; row-gap: 6px;
  grid-template-areas: "head head" "next next" "track track" "since date";
  align-items: end;
  --dose-progress: 0%;
}
.dose-timer-head {
  grid-area: head; min-width: 0;
  display: flex; align-items: center; gap: 7px;
  margin-bottom: 6px; color: var(--text-2);
}
.dose-timer-head > svg { flex: none; color: var(--text-3); }
.dose-timer-head strong {
  font-size: 12px; font-weight: 500; line-height: 1.3; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dose-timer-cell { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.dose-timer-cell small { font-size: 11px; line-height: 1.2; font-weight: 500; color: var(--text-3); }
.dose-timer-cell strong {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 14px; line-height: 1.25; font-weight: 600; color: var(--text);
  white-space: nowrap;
  transition: color var(--t-2) var(--ease);
}
.dose-timer-next {
  grid-area: next;
  flex-direction: row; justify-content: space-between; align-items: baseline; gap: 8px;
}
.dose-timer-next small { font-size: 12px; font-weight: 400; color: var(--text-2); }
.dose-timer-next strong { font-size: 16px; }
.dose-timer-since {
  grid-area: since;
  flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 3px 5px;
}
.dose-timer-since small, .dose-timer-since strong { font-size: 11px; font-weight: 400; color: var(--text-3); }
.dose-timer-date {
  grid-area: date; text-align: right;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 11px; line-height: 1.4; color: var(--text-3);
  white-space: nowrap;
}
.dose-timer-track {
  grid-area: track; position: relative; display: block;
  height: 5px; margin: 2px 0; overflow: hidden;
  background: var(--track); border-radius: 999px;
}
.dose-timer-fill {
  position: absolute; inset: 0 auto 0 0;
  width: calc(100% - var(--dose-progress));
  background: var(--accent); border-radius: 999px;
  transition: width 900ms linear, background var(--t-2) var(--ease);
}
/* Without a head the grid drops that row entirely, so the empty row-gap never
   pushes the first line of figures down. */
.dose-timer:not(.has-head) { grid-template-areas: "next next" "track track" "since date"; }
.dose-timer.no-next { grid-template-areas: "head head" "since since"; }
.dose-timer.no-next:not(.has-head) { grid-template-areas: "since since"; }
.dose-timer.no-next .dose-timer-track { display: none; }
.dose-timer.is-due .dose-timer-track { background: var(--track-warn); }
.dose-timer.is-due .dose-timer-fill  { background: var(--warn); }
.dose-timer.is-due .dose-timer-next strong { color: var(--warn-ink); }
.dose-timer.is-late .dose-timer-track { background: var(--track-crit); }
.dose-timer.is-late .dose-timer-fill  { background: var(--crit); }
.dose-timer.is-late .dose-timer-next strong { color: var(--crit-ink); }


/* ==========================================================================
   8. Form extras
   The pieces the dialogs are assembled from that app.css does not cover.
   ========================================================================== */

/* schedule editor: a checkbox row that reveals its fields */
.schedule-box {
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--sunken); overflow: hidden;
}
.check-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px; cursor: pointer;
}
.check-row input {
  width: 18px; height: 18px; margin: 1px 0 0; flex: none;
  accent-color: var(--solid); cursor: pointer;
}
.check-row input:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 3px; }
.check-row > span { min-width: 0; }
.check-row strong { display: block; font-size: 13.5px; font-weight: 600; line-height: 1.35; color: var(--text); }
.check-row small { display: block; margin-top: 2px; color: var(--text-2); font-size: 12px; line-height: 1.45; }
.schedule-fields {
  padding: 14px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
/* The fields also carry .grid, whose display would otherwise beat [hidden]. */
.schedule-fields[hidden] { display: none; }

/* a unit printed inside the right end of a field */
.input-suffix { position: relative; }
.input-suffix .input { padding-right: 56px; }
.input-suffix span {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  color: var(--text-2); font-size: 12.5px; pointer-events: none;
}

/* a field with a small control inside its right end (Show / Hide) */
.input-wrap { position: relative; display: flex; }
.input-wrap .input { padding-right: 64px; }
.input-btn {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  min-height: 28px; padding: 4px 9px;
  border: 0; border-radius: var(--r-sm);
  background: var(--sunken); color: var(--text-2);
  font: inherit; font-size: 12px; font-weight: 600; line-height: 1;
  cursor: pointer;
  transition: color var(--t-1) var(--ease), background var(--t-1) var(--ease);
}
.input-btn:hover { color: var(--text); background: var(--border); }
.input-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }

/* a field with a button beside it (Opened ... Now) */
.input-row { display: flex; gap: 8px; align-items: stretch; }
.input-row .input { flex: 1 1 auto; min-width: 0; }
.input-row .btn { flex: none; }

/* collapsed facts on a vial card */
.vial-details { margin: 0; font-size: 12px; color: var(--text-2); }
.vial-details summary {
  display: inline-flex; align-items: center; gap: 4px;
  width: fit-content; padding: 6px 0;
  list-style: none; cursor: pointer;
  font-weight: 500; color: var(--text-2);
  transition: color var(--t-1) var(--ease);
}
.vial-details summary::-webkit-details-marker { display: none; }
.vial-details summary svg { flex: none; color: var(--text-3); transition: transform var(--t-2) var(--ease); }
.vial-details[open] summary svg { transform: rotate(180deg); }
.vial-details summary:hover { color: var(--text); }
.vial-details summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; border-radius: 3px; }
.vial-details dl { display: grid; gap: 6px; margin: 8px 0 0; }
.vial-details dl > div { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.vial-details dt { color: var(--text-3); }
.vial-details dd {
  margin: 0; text-align: right; color: var(--text);
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.vial-detail-notes { margin: 10px 0 0; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }

/* "Set your schedule" - an ink link with an icon, not a button */
.vial-set-schedule {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 0; border: 0; background: none;
  color: var(--text); font: inherit; font-size: 12.5px; font-weight: 600; line-height: 1.5;
  text-align: left; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--border-firm);
  transition: text-decoration-color var(--t-1) var(--ease);
}
.vial-set-schedule svg { flex: none; color: var(--text-3); }
.vial-set-schedule:hover { text-decoration-color: var(--text); }
.vial-set-schedule:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; border-radius: 3px; }

/* stock warning under the timer: an amber rule, not a coloured box */
.vial-stock-notice {
  margin: 10px 0 0; padding-left: 9px;
  border-left: 2px solid var(--warn);
  font-size: 11.5px; line-height: 1.5; color: var(--text-2);
}


/* ==========================================================================
   9. Reduced motion
   Everything in this file that enters, leaves or slides. Colour changes stay:
   they carry the feedback without any movement. The spinner is kept - it is
   the only sign that a save is in progress.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .scrim, .scrim.is-closing,
  .modal, .scrim.is-closing .modal,
  .popover, .combo-pop, .toast { animation: none; }
  .scrim.is-closing { opacity: 0; }
  .toast, .is-busy .modal-body,
  .pop-item, .combo-item, .input-btn,
  .vial-usage-value, .vial-usage-fill,
  .vial-model, .vial-model-halo, .vial-inspect-badge,
  .vial-inspector-model,
  .dose-timer-fill, .dose-timer-cell strong,
  .vial-details summary, .vial-details summary svg,
  .vial-set-schedule { transition: none; }
  .toast.is-leaving { transform: none; }
  .vial-model { transform: none; }
  .vial-inspect-badge { transform: none; }
  .vial-stage:hover .vial-model-halo { transform: none; }
}
