Something went wrong. Try again.
A device emulator for screen-and-buttons firmware: your own C compiled to WebAssembly, driven by a browser page the firmware itself describes. Freeze, annotate and replay; diff the emulator against real hardware.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608/* 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: the bare device, nothing else. No chrome, no control strip (no rotation buttons, no shake pill, no diagnostics), floating on the embedding page's own background. Additive: everything below is scoped under html.embed, so plain-mode layout above is completely untouched. Interactions do not go away, only their on-screen controls do: pointer/touch on the panel keeps working exactly as before (wirePanelInput never checks EMBED), and rotate/shake are reachable through the R/S keyboard shortcuts main.ts's buildChrome now always registers (a run page states this in a plain-text hint beside the iframe, site/build.ts, since the strip that used to show it is gone). 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. *//* No scrollbars, ever, in embed mode, and no RESERVED scrollbar gutter either: family-budget.css's `html { scrollbar-gutter: stable; }` (there for the plain dev UI's own sidebar/stage scroll stability) was leaking into embed mode too. html/body have no explicit overflow rule anywhere else in this file, so the browser treats the viewport as an implicit auto-scroller and honours that stable gutter unconditionally - permanently reserving a vertical-scrollbar's width (~15px) on the right edge even though embed mode never scrolls at all (root cause of the horizontal-scrollbar-looking strip a run page's iframe showed: not a real content overflow, a reserved-but-unused gutter). That reservation shrinks #root/.stage's actual rendered width below the iframe's own width, so centerDeviceOnce/fitDeviceToStage's otherwise-correct 50% math centers the device in the SHRUNK box, landing it left of the iframe's true visual centre with dead space on the right - exactly what this overrides. `overflow: hidden` on both html and body (belt and braces: only one of them is the actual "scrolling element" depending on doctype/quirks, and paying the redundant declaration once here is cheaper than depending on that browser-internal choice) plus `.stage` below closes off every remaining overflow source (the base .stage rule above is `overflow: auto`, needed for the plain dev UI's large draggable canvas, wrong here where nothing should ever scroll). */html.embed, html.embed body { overflow: hidden; scrollbar-gutter: auto;}html.embed, html.embed body { background: transparent; }html.embed .topbar,html.embed .side.controls,html.embed .diag-strip,/* The ENTIRE bottom bar, not just the tilt slider/pause/step/replay pieces: rotation buttons and any declared event-sensor ("shake") pill used to survive here as the "minimal strip". Sylve's own read of the embedded device ("more abstract, without the tilt bar... just floating") was that this whole strip reads as chrome, not as part of the device - gone entirely now, the interactions it exposed move to R/S. */html.embed .bottom-bar { 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; overflow: hidden; }/* FIX 2: whole-stage drag-as-accelerometer (src/motion.ts's DragMotion). The draggable zone is the whole stage now, not just the bezel plastic - grab/grabbing reads correctly over the background too, not just over the device. .panel keeps its own inline cursor (main.ts sets it directly, which always wins over this rule regardless of source order) and .embed-ctrl-btn/.dev-btn keep their own pointer/default cursor for the same reason - none of those are ever a drag start point (motion.ts's EXCLUDED_SELECTOR), so they should never look like one either. */html.embed .stage { cursor: grab; }html.embed .dev-btn { cursor: pointer; }/* Forced everywhere (not just on the stage) while an actual drag is held: DragMotion toggles this on <html> itself for exactly the span between pointerdown and pointerup/cancel, so the cursor reads as "dragging the whole zone" even where the pointer happens to be hovering a higher-specificity/inline cursor rule (the panel, a button) underneath - correct here because a captured drag routes every event to the stage regardless of what is visually under the pointer, so nothing under it is actually interactive during that span anyway. */html.embed.dm-dragging, html.embed.dm-dragging * { cursor: grabbing !important; }/* #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. *//* Shifted up from dead centre by 28px, UNCONDITIONALLY (not just while the phone-tilt chip happens to be mounted, contrast with an earlier version of this rule): main.ts's buildEmbedControls always builds the rotate button in embed mode, so the row below the device is always there, chip or no chip. main.ts's fitDeviceToStage reserves the matching space in its own scale computation (its own "controlsReserve" constant, kept in step with this number by comment, not by a shared source - a CSS custom property would be the more failure-proof link, left as-is since both live a few lines from their own explanation). */html.embed .device-wrap { position: absolute; left: 50%; top: calc(50% - 28px); transform: translate(-50%, -50%) scale(var(--embed-scale, 1)); transform-origin: center center;}
/* ---------- embed mode: the minimal control cluster (rotate + shake, main.ts's buildEmbedControls) - centred below the device, one row. Ghost icon buttons: fully transparent until hovered/pressed, exactly the design brief ("disappear into the page, visible on hover/press") and this codebase's own existing .chrome-btn precedent (top of this file), just circular and sized for a touch target instead of text. This is NOT the old .bottom-bar strip above (still display:none) - a fresh, separate element built only for the embed layer. ---------- */html.embed .embed-controls { position: absolute; z-index: 5; left: 50%; bottom: 8px; transform: translateX(-50%); display: flex; align-items: center; gap: 10px;}html.embed .embed-ctrl-btn { width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; background: transparent; border: 1px solid transparent; color: var(--muted); cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background 120ms ease-out, border-color 120ms ease-out, color 120ms ease-out;}html.embed .embed-ctrl-btn:hover,html.embed .embed-ctrl-btn:focus-visible { background: color-mix(in srgb, var(--card) 88%, transparent); border-color: var(--border-soft); color: var(--ink-soft);}html.embed .embed-ctrl-btn:active { background: var(--accent-bg); border-color: var(--accent-bg); color: var(--accent-dark);}html.embed .embed-ctrl-btn[hidden] { display: none; }
/* Created only when the loaded descriptor has a vector sensor and this browser exposes a phone-motion API - see motion.ts's PhoneMotion. Mounted as a child of .embed-controls above (its own mountTo option) so it joins the SAME row as the rotate/shake buttons rather than self-centering on a second, independent spot: the position/transform this rule would otherwise give it (a lone, absolutely-centred pill, this control's original and still-default behaviour outside embed's shared row) are overridden by the more specific ".embed-controls .motion-chip" rule right below, letting it size and place itself as an ordinary flex row item instead. */html.embed .motion-chip { position: absolute; z-index: 5; left: 50%; bottom: 8px; transform: translateX(-50%); white-space: nowrap; font-family: var(--font-mono, monospace); font-size: 10px; color: var(--muted); background: color-mix(in srgb, var(--card) 88%, transparent); border: 1px solid var(--border-soft); pointer-events: auto; touch-action: manipulation;}html.embed .embed-controls .motion-chip { position: static; left: auto; bottom: auto; transform: none;}html.embed .motion-chip.active { color: var(--accent-dark); background: var(--accent-bg);}html.embed .motion-chip[data-state="blocked"] { color: var(--danger); background: var(--danger-bg);}