/* Page chrome: warm paper, terracotta, monospace (see family-budget.css,
   vendored from this owner's design system). The device panel itself stays
   pure device output, styled below outside that palette on purpose: it is
   a framebuffer, not UI.

   The panel is the subject of this page, everything else is furniture.
   Furniture stays quiet: smaller, lower contrast, terracotta reserved for
   genuine "this is active" state, never decorative. No control carries a
   sentence; anything longer than two words lives behind a title tooltip
   or a <details> disclosure.

   --panel-w / --panel-h are set from the firmware's own emu_device()
   descriptor at runtime (see main.ts, buildChrome): nothing here hardcodes
   a panel size, because this emulator is generic across devices. The
   values below are just a plausible fallback for the instant before the
   first module loads. */

:root {
  --panel-w: 240px;
  --panel-h: 240px;
  --bezel-pad: 18px;
  --sidebar-w: 280px;
}

/* height, not min-height: the sidebar's content is easily taller than the
   viewport, and without a definite height here .layout's flex children have
   nothing definite to stretch/shrink into, so the WHOLE PAGE grows to fit
   the sidebar instead of the sidebar scrolling on its own (.side.controls
   already has overflow-y:auto, but that is a no-op without this). */
#root { height: 100vh; display: flex; flex-direction: column; }

/* ---------- quiet chrome controls: text only, no border/fill until
   hovered, so the top and bottom bars do not compete with the device ---- */
.chrome-btn {
  font: inherit;
  font-size: 11px;
  background: none;
  border: none;
  color: var(--muted);
  padding: 4px 7px;
  border-radius: var(--radius-control);
  cursor: pointer;
}
.chrome-btn:hover { background: var(--row-hover); color: var(--ink-soft); }
.chrome-btn[disabled] { opacity: .4; cursor: default; }
.deck-btn { font-size: 13px; padding: 4px 10px; }
.icon-btn { display: inline-flex; align-items: center; justify-content: center; }
.chrome-btn.active { color: var(--accent-dark); background: var(--accent-bg); }

.divider { width: 1px; height: 16px; background: var(--border); margin: 0 4px; align-self: center; }

/* ---------- disclosure: the one mechanism for anything longer than two
   words (touch tuning, a gesture's prose) ---------- */
.disclosure { margin: 4px 0 8px; }
.disclosure summary {
  cursor: pointer;
  font-size: 11px;
  color: var(--muted);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 5px;
  user-select: none;
}
.disclosure summary::-webkit-details-marker { display: none; }
.disclosure summary::before { content: "\25B8"; font-size: 9px; display: inline-block; transition: transform .1s; }
.disclosure[open] summary::before { transform: rotate(90deg); }
.disclosure[open] summary { color: var(--ink-soft); }
.disclosure > *:not(summary) { margin-top: 6px; }

.topbar {
  display: flex; align-items: center; gap: 2px;
  padding: 8px 14px;
  background: var(--card);
  border-bottom: 1px solid var(--border-soft);
}
.topbar .spacer { flex: 1; }
.topbar b { font-size: 13px; }

.reload-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--border-dashed);
  display: inline-block;
  margin-left: 6px;
}
.reload-dot.connected { background: var(--positive); }

.filebtn { position: relative; overflow: hidden; }
.filebtn input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

.wasm-error {
  margin: 12px 16px;
  padding: 12px 14px;
  background: var(--danger-bg);
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-card);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
.wasm-error-text { white-space: pre-wrap; word-break: break-word; }

/* ---------- engine-dead banner: deliberately NOT a copy of .wasm-error.
   That one means a module never came up (blank panel, nothing to look
   at). This means a module DID come up and something threw mid-tick --
   the panel below may still show a perfectly normal-looking last frame,
   which is exactly the state that reads as "my firmware is stuck" if this
   doesn't say otherwise, loudly. Hazard-striped border instead of a plain
   one, on purpose: same danger palette as .wasm-error but a different
   silhouette, so the two are never mistakable for each other even at a
   glance. See main.ts's paintDeadOverlay for the matching treatment
   painted directly on the panel itself. ---------- */
.engine-dead {
  margin: 12px 16px;
  padding: 12px 14px;
  background: var(--danger-bg);
  color: var(--danger);
  border: 3px solid transparent;
  border-image: repeating-linear-gradient(135deg, var(--danger) 0 8px, var(--danger-bg) 8px 16px) 3;
  border-radius: var(--radius-card);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.engine-dead-title { font-weight: 700; }
.engine-dead-text { white-space: pre-wrap; word-break: break-word; font-size: 11px; }
.engine-dead-meta { font-variant-numeric: tabular-nums; opacity: .85; }
.engine-dead-hint { opacity: .85; max-width: 720px; }

.hidden { display: none !important; }

.layout { display: flex; flex: 1; min-height: 0; }

/* ---------- stage: the draggable device lives here ---------- */
.stage {
  position: relative;
  flex: 1;
  overflow: auto;
  background:
    radial-gradient(circle at 1px 1px, var(--border-fine) 1px, transparent 0) 0 0 / 22px 22px,
    var(--paper);
  min-height: 640px;
}

.device-wrap {
  /* Centred over the stage on first load by main.ts (centerDeviceOnce),
     once actual layout sizes are known; these are just a reasonable
     fallback for the instant before that runs. Dragging (device.ts,
     makeDraggable) overwrites left/top directly from then on. */
  position: absolute;
  left: 120px;
  top: 60px;
  touch-action: none;
}

.bezel {
  position: relative;
  width: calc(var(--panel-w) + 2 * var(--bezel-pad));
  height: calc(var(--panel-h) + 2 * var(--bezel-pad));
  padding: var(--bezel-pad);
  border-radius: 34px;
  background: linear-gradient(160deg, #3a3a3d, #1c1c1e 60%);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .08);
  cursor: grab;
  user-select: none;
}
.bezel:active { cursor: grabbing; }

.panel {
  display: block;
  width: var(--panel-w);
  height: var(--panel-h);
  background: #fff;
  border-radius: 6px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .5);
  touch-action: none;
}

.overlay-canvas {
  position: absolute;
  top: var(--bezel-pad);
  left: var(--bezel-pad);
  width: var(--panel-w);
  height: var(--panel-h);
  pointer-events: none;
}

/* Buttons: shape and colour are fixed, position (edge + offset along it)
   is set inline per-button from the device descriptor (main.ts / device.ts,
   createButtonElement), because that position IS real device geometry.

   Pressing a button should never be too subtle to trust, and someone
   holding a button for a real gesture (a long-press threshold, or any
   power-off style hold) should never have to wonder whether the emulator
   noticed. Three layers of feedback here, deliberately redundant with each
   other rather than relying on any single cue:
     1. a physical depress: the button visibly shrinks toward the bezel
        along the axis it protrudes on, plus a brighter, inverted colour
        and an inset shadow, the instant it goes down;
     2. a fill (the ::after pseudo-element) that grows along the button's
        own length, its CSS transition-duration set to the button's real
        longPressMs (via --hold-ms, device.ts) - the fill's own animation
        IS the timer, not an approximation that could drift from it;
     3. on reaching the threshold, a distinct accent-coloured "reached"
        state (.long) with its own glow, so a completed hold looks nothing
        like an in-progress one. */
.dev-btn {
  position: absolute;
  overflow: hidden;
  background: linear-gradient(90deg, #55555a, #333336);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .4);
  border-radius: 5px;
  transition: transform 110ms ease-out;
}
.dev-btn.edge-left  { transform-origin: right center; }
.dev-btn.edge-right { transform-origin: left center; }
.dev-btn.edge-top    { transform-origin: center bottom; }
.dev-btn.edge-bottom { transform-origin: center top; }

.dev-btn.pressed {
  background: linear-gradient(90deg, #b4b4ba, #82828a);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .55), 0 0 0 2px rgba(255, 255, 255, .14);
}
.dev-btn.edge-left.pressed,  .dev-btn.edge-right.pressed  { transform: scaleX(.6); }
.dev-btn.edge-top.pressed,   .dev-btn.edge-bottom.pressed { transform: scaleY(.6); }

.dev-btn.long {
  background: linear-gradient(90deg, var(--accent), var(--accent-dark));
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .4), 0 0 0 3px rgba(196, 98, 31, .35);
}

/* The hold-progress fill. Growth direction runs along the button's own
   length (its long axis), not its thickness, so it reads as "how far
   through the hold" rather than "how hard pressed". No transition at all
   until .holding is added (so a plain tap never shows a flicker of it),
   and released before the threshold, it retracts on the fast default
   transition below rather than the slow one, an unmistakable "let go,
   did not count" snap. */
.dev-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, .5);
  transition: transform 140ms ease-out;
}
.dev-btn.edge-left::after,  .dev-btn.edge-right::after  { transform-origin: top;  transform: scaleY(0); }
.dev-btn.edge-top::after,   .dev-btn.edge-bottom::after { transform-origin: left; transform: scaleX(0); }
.dev-btn.holding.edge-left::after,  .dev-btn.holding.edge-right::after {
  transform: scaleY(1);
  transition: transform var(--hold-ms) linear;
}
.dev-btn.holding.edge-top::after,   .dev-btn.holding.edge-bottom::after {
  transform: scaleX(1);
  transition: transform var(--hold-ms) linear;
}

/* ---------- bottom bar: view + time deck, and one diagnostics strip.
   Fixed to the viewport (not the stage's scroll/drag content), centred in
   the space left of the sidebar, so it stays put regardless of where the
   puck gets dragged. Quiet: a thin card, small text, no accent unless a
   control is genuinely active (segtog's own .active). ---------- */
.bottom-bar {
  position: fixed;
  left: 0;
  bottom: 12px;
  width: calc(100% - var(--sidebar-w));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  z-index: 5;
}

.stage-controls {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-tab);
  padding: 4px 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .08);
}
.tilt-slider { width: 64px; }

.diag-strip {
  font-size: 10.5px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: pre;
}

/* ---------- sidebar ---------- */
.side.controls { width: var(--sidebar-w); overflow-y: auto; padding: 14px 12px; }
.side.controls h3:first-child { margin-top: 0; }
.side.controls h3 { margin: 16px 0 6px; }

.slider-row { margin-bottom: 8px; }
.slider-row .row-head { display: flex; justify-content: space-between; font-size: 11px; }
.slider-row .row-head b { font-weight: 600; }
.slider-row .row-head span { color: var(--accent-dark); font-variant-numeric: tabular-nums; }
.slider-row input[type=range] { width: 100%; }

.toggle-row { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; font-size: 12px; }
.toggle-row input[type=checkbox] { margin: 0; }

.device-info { font-size: 11px; color: var(--muted); margin-bottom: 8px; }

.kbd {
  display: inline-block;
  min-width: 15px;
  text-align: center;
  font-size: 10px;
  padding: 1px 4px;
  border-radius: 4px;
  background: var(--chip-bg);
  color: var(--chip-text);
  border: 1px solid var(--border);
}

.shortcut-list { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
.shortcut-row { font-size: 11px; display: flex; align-items: center; gap: 6px; }

.sensor-controls { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 4px; }
.sensor-btn { display: inline-flex; align-items: center; gap: 4px; }

/* Segmented toggle, a size down from the design system's default: these
   are secondary controls (rotation, contact size), not primary navigation. */
.segtog.seg-sm button { padding: 3px 8px; font-size: 11px; }

.contact-row { display: flex; align-items: center; gap: 5px; margin-bottom: 6px; }
.mm-input { width: 44px; padding: 3px 5px; font-size: 11px; }
.contact-row .unit { font-size: 11px; color: var(--muted); }
.contact-row .px-readout { margin-left: auto; font-size: 11px; color: var(--accent-dark); font-variant-numeric: tabular-nums; }

.gesture-detail summary { font-weight: 600; color: var(--ink-soft); }
.gesture-body { padding-left: 13px; font-size: 11px; }
.gesture-body .how { color: var(--ink-soft); margin: 0 0 6px; max-width: 240px; }
.gesture-actions { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.gesture-keys { display: flex; align-items: center; gap: 2px; color: var(--muted); }

.app-strip { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.app-strip-btn.active { background: var(--accent); color: #fff; }

.console-pane {
  height: 100px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 5px 7px;
  font-size: 10.5px;
  line-height: 1.5;
}
.console-line { white-space: pre-wrap; word-break: break-word; color: var(--hint); }
.console-line + .console-line { border-top: 1px dashed var(--border-fine); }

/* ---------- freeze annotation modal ---------- */
.freeze-modal { max-width: none; width: fit-content; }
.freeze-modal-title { margin-bottom: 10px; }
.freeze-canvas-wrap { position: relative; margin: 0 auto 10px; }
.freeze-img { position: absolute; top: 0; left: 0; image-rendering: pixelated; }
.freeze-ink { position: relative; touch-action: none; cursor: crosshair; }
.freeze-typetog { margin-bottom: 10px; }
.freeze-notes { max-height: 120px; overflow-y: auto; margin-bottom: 8px; }
.freeze-note-row { font-size: 12px; padding: 3px 0; border-bottom: 1px dashed var(--border-fine); }
.freeze-note-add { display: flex; gap: 6px; margin-bottom: 12px; }
.freeze-note-add input { flex: 1; }
.freeze-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }

/* ---------- hardware-free regression check: two controls (baseline,
   check), one status pill, and - only when something diverged - a modal
   with the baseline/current/diff frames themselves. No sentence under the
   images: a diverging pixel count is already in the pill, the pictures are
   the "show me where". See src/regression.ts and docs/harness.md. ---------- */
#regressionPill.status.fail { background: var(--danger-bg); color: var(--danger); }

.regression-modal { max-width: none; width: fit-content; max-height: 82vh; overflow-y: auto; }
.regression-modal-title { margin-bottom: 10px; }
.regression-row { margin-bottom: 12px; }
.regression-row .pill { margin-bottom: 6px; }
.regression-imgs { display: flex; gap: 10px; flex-wrap: wrap; }
.regression-thumb canvas { display: block; image-rendering: pixelated; border: 1px solid var(--border); border-radius: 4px; max-width: 200px; height: auto; }
.regression-thumb-label { font-size: 10px; color: var(--muted); text-align: center; margin-top: 3px; }
.regression-modal-actions { display: flex; justify-content: flex-end; margin-top: 4px; }

/* ---------- embed mode: device (+ minimal control strip) only, no chrome.
   Additive: everything below is scoped under html.embed, so plain-mode
   layout above is completely untouched. See main.ts's EMBED/applyEmbedMode
   for what gets moved (nothing here reaches into main.ts's DOM logic, this
   is presentation only) and docs/decisions/ for why this exists (a public
   run page embedding just the device, not the dev tool around it). ---- */
/* Both html and body: family-budget.css's own `body { background:
   var(--paper) }` is what showed up as a hard white/paper stripe behind the
   device in the embedding page's own dark background (site/styles.css's
   .emu-frame iframe also had a hardcoded white behind THIS, fixed
   separately there - see that rule's own comment). html itself has no
   background rule elsewhere today, but is included here too so this stays
   correct even if one is added later without anyone remembering embed mode
   depends on staying transparent. */
html.embed, html.embed body { background: transparent; }
html.embed .topbar,
html.embed .side.controls,
html.embed .diag-strip,
/* the cosmetic tilt slider and the pause/step/replay deck: not part of the
   minimal strip (rotation + any declared event-sensor button only) */
html.embed .tilt-slider,
html.embed .stage-controls > .divider,
html.embed #btnPause,
html.embed #btnStep,
html.embed #replayBar,
html.embed #btnStopReplay {
  display: none !important;
}
/* Sidebar column gone entirely: the stage and the bottom bar both size off
   --sidebar-w (app.css above), so zeroing it here is the one change that
   lets both reflow to the full width with no other rule needing to know
   embed mode exists. */
html.embed { --sidebar-w: 0px; }
html.embed .stage { background: transparent; min-height: 0; }
/* #sensorControls (main.ts moves this node into .stage-controls itself, so
   the wrap/gap it already has is enough) sits right after the rotation
   segtog in source order once moved; no extra spacing rule needed beyond
   the strip's own existing `gap`. */

/* Scales the whole device (bezel + its buttons) to fit the viewport/iframe,
   contained in BOTH dimensions - main.ts's fitDeviceToStage computes
   --embed-scale on load, resize and rotation change.

   TRUE (50%, 50%) CSS centering, not the plain-mode left/top main.ts's
   centerDeviceOnce() computes in PIXELS. That function measures the
   stage vs the device's UNSCALED size and clamps a too-small stage to a
   20px minimum offset rather than going negative - correct for the plain
   dev UI, where the stage is large and scrollable and the device is never
   scaled down. In embed mode the device usually IS bigger than the
   card/iframe before scaling, so that clamped, unscaled position's own
   geometric centre sits well below the iframe's actual visual centre;
   scaling around that off-centre point (a plain `transform: scale()`,
   which was tried first here) shrinks the device toward the WRONG point
   and leaves its bottom edge still hanging off the bottom of the iframe -
   this is the exact crop the fluidbox landing-page card showed (confirmed
   by measuring both rects: device bottom edge past the iframe's own
   bottom edge, by design of the plain-mode centering math, not a
   plain arithmetic mistake in the scale factor itself). Percentage
   left/top plus translate(-50%,-50%) is centring math that is correct
   at ANY scale, because translate()'s percentages resolve against the
   element's own (unscaled) box regardless of what scale() does in the
   same transform list - this is why main.ts's embed path skips setting
   left/top at all (see centerDeviceOnce's own EMBED guard) rather than
   this rule trying to fight an inline style with !important. */
html.embed .device-wrap {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(var(--embed-scale, 1));
  transform-origin: center center;
}
