From 07a22d3924764fb0acd378767ad421d1ad0b0578 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:08:13 +0100 Subject: [PATCH 1/2] Consolidate main-window styles into one design system Merge the legacy compact stylesheet and the large-target overrides into a single component layer with named tokens. Move Button, Input and Select sizing from Tailwind utilities into .btn and .field-control so variants can actually differ: primary, secondary, text, icon and danger now each have their own weight, and disabled controls go neutral instead of fading the brand red to pink. Closes #882 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/App.tsx | 2 +- src/styles.css | 462 +------------------------------- src/ui/app.css | 638 ++++++++++++++++++++++++++++++++++++-------- src/ui/controls.tsx | 12 +- 4 files changed, 535 insertions(+), 579 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7ace2540..103ca7e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,7 +44,7 @@ const bluetoothDescriptions: Record = { }; function NavButton({ active, icon, children, onClick }: { active: boolean; icon: ReactNode; children: ReactNode; onClick: () => void }) { - return ; + return ; } function StatusIcon({ ok, children }: { ok: boolean; children: ReactNode }) { diff --git a/src/styles.css b/src/styles.css index e824a4dc..5726ce2d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,465 +1,5 @@ -@layer theme, legacy, components, utilities; +@layer theme, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/utilities.css" layer(utilities); @import "./help/demonstrations.css"; @import "./ui/app.css"; - -@layer legacy { -:root { - font-family: Inter, "Segoe UI", system-ui, sans-serif; - color: var(--text); - background: var(--app-background); - font-synthesis: none; - text-rendering: optimizeLegibility; - --brand: #d32f2f; - --brand-label: #b3261e; - --brand-hover: #b3261e; - --brand-container: #ffdad6; - --brand-border: #efb8b4; - --on-brand: #ffffff; - --on-brand-container: #410002; - --chrome: #d32f2f; - --segment-selected: #fff1f1; - --app-background: #f5f4f7; - --surface: #ffffff; - --surface-muted: #f3f2f6; - --surface-hover: #ebe9ef; - --text: #1c1b1f; - --muted: #49454f; - --border: #e2e0e8; - --inactive: #79747e; - --status-ok: #2e7d32; - --status-ok-soft: #e8f5e9; - --status-error: #ba1a1a; - --status-error-soft: #ffdad6; - --focus: #1f6fa2; -} - -* { box-sizing: border-box; } -html, body, #root { width: 100%; min-width: 0; min-height: 100%; margin: 0; } -button, input, select { font: inherit; } -button { letter-spacing: 0; } -h1, h2, h3, p { margin: 0; letter-spacing: 0; } - -.app-shell { display: grid; grid-template-columns: 210px minmax(0, 1fr); min-height: 100vh; border-top: 3px solid var(--chrome); background: var(--app-background); } -aside { position: sticky; top: 0; display: flex; flex-direction: column; height: 100vh; padding: 20px 12px 14px; border-right: 1px solid var(--border); background: var(--surface-muted); } -.brand { display: flex; align-items: center; gap: 11px; min-height: 44px; padding: 0 8px 18px; } -.brand-mark { display: block; width: 36px; height: 36px; border-radius: 8px; object-fit: contain; } -.brand strong, .brand small { display: block; } -.brand strong { color: var(--brand-label); } -.brand small { margin-top: 2px; color: var(--muted); } -nav { display: grid; gap: 3px; } -.nav-button { display: flex; align-items: center; width: 100%; border: 0; color: var(--muted); background: transparent; cursor: pointer; text-align: left; } -.nav-button:hover { background: var(--surface-hover); } -.nav-button[data-active="true"] { color: #ffffff; background: var(--chrome); font-weight: 650; } -.sidebar-footer { margin-top: auto; padding: 12px 9px 0; color: var(--muted); } - -main { min-width: 0; } -.view { margin: 0 auto; } -.page-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 28px; } -.page-header h1 { font-size: 24px; font-weight: 700; } -.page-header p { margin-top: 5px; color: var(--muted); } -.page-header > svg { color: var(--muted); } - -.connection-band { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 15px; min-height: 94px; border: 1px solid var(--border); border-left: 4px solid var(--inactive); padding: 18px 20px; background: var(--surface); } -.connection-band[data-connected="true"] { border-left-color: var(--status-ok); } -.connection-band h2 { font-size: 17px; } -.connection-band p { margin-top: 4px; color: var(--muted); } -.status-icon { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; color: var(--muted); background: var(--surface-muted); } -.status-icon[data-ok="true"] { color: var(--status-ok); background: var(--status-ok-soft); } - -.status-list { margin-top: 22px; border-top: 1px solid var(--border); } -.status-list article { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 76px; border-bottom: 1px solid var(--border); padding: 12px 4px; } -.status-list h3 { } -.status-list p { margin-top: 3px; color: var(--muted); } -.status-list .status-icon { width: 34px; height: 34px; } - -.primary, .secondary, .text-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 36px; border-radius: 6px; padding: 0 14px; cursor: pointer; } -.primary { border: 1px solid var(--brand); color: var(--on-brand); background: var(--brand); font-weight: 650; } -.primary:hover { border-color: var(--brand-hover); background: var(--brand-hover); } -.secondary { border: 1px solid var(--border); color: var(--text); background: var(--surface); } -.secondary:hover { background: var(--surface-muted); } -.secondary.danger { border-color: var(--brand-border); color: var(--status-error); background: var(--status-error-soft); } -.primary.danger { border-color: var(--status-error); background: var(--status-error); } -.text-button { border: 0; color: var(--brand); background: transparent; font-weight: 650; } -button:disabled { cursor: default; opacity: 0.55; } -.icon-button { display: grid; place-items: center; width: 36px; height: 36px; border: 0; border-radius: 6px; color: inherit; background: transparent; cursor: pointer; } -.danger-icon { color: var(--status-error); } -.danger-icon:hover { background: var(--status-error-soft); } - -.empty-state { display: grid; justify-items: center; border: 1px dashed var(--border); padding: 52px 24px; color: var(--muted); text-align: center; } -.empty-state h2 { margin-top: 14px; color: var(--text); font-size: 16px; } -.empty-state p { margin-top: 6px; } -.device-list, .profile-list { border-top: 1px solid var(--border); } -.device-list article, .profile-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 72px; border-bottom: 1px solid var(--border); padding: 10px 4px; } -.device-list h2, .profile-list h2 { } -.device-list p, .profile-list p { margin-top: 4px; color: var(--muted); } -.profile-row { width: 100%; grid-template-columns: auto 1fr auto auto; border-top: 0; border-right: 0; border-left: 0; color: inherit; background: transparent; cursor: pointer; text-align: left; } -.profile-row:hover { background: var(--surface-hover); } -.profile-row > span { color: var(--muted); } -.profile-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 6px; color: var(--muted); background: var(--surface-muted); } -.capability-note { margin-top: 16px; color: var(--muted); } - -.setting-group { display: grid; border-top: 1px solid var(--border); } -.setting-group h2 { } -.setting-group header p { margin-top: 5px; color: var(--muted); line-height: 1.5; } -.setting-controls { display: grid; align-content: start; gap: 17px; } -.update-controls { display: grid; gap: 10px; color: var(--muted); } -.update-controls > div { display: flex; flex-wrap: wrap; gap: 8px; } -.update-controls progress { width: 100%; accent-color: var(--brand); } -.update-controls > span { font-variant-numeric: tabular-nums; } -.setting-group:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; } -.toggle-row, .range-row { display: grid; align-items: center; gap: 12px; } -.toggle-row { grid-template-columns: 1fr auto; cursor: pointer; } -.toggle-row input { position: absolute; width: 1px; height: 1px; opacity: 0; } -.toggle { position: relative; width: 38px; height: 22px; border-radius: 999px; background: var(--inactive); transition: background 140ms; } -.toggle::after { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: white; content: ""; transition: transform 140ms; } -.toggle-row input:checked + .toggle { background: var(--brand); } -.toggle-row input:checked + .toggle::after { transform: translateX(16px); } -.toggle-row input:focus-visible + .toggle { outline: 2px solid var(--focus); outline-offset: 2px; } -.toggle-row[data-disabled="true"] { cursor: default; opacity: 0.55; } -.range-row span { display: flex; justify-content: space-between; } -.range-row input { width: 100%; accent-color: var(--brand); } -.pointer-speed, .repeat-options fieldset { min-width: 0; margin: 0; border: 0; padding: 0; } -.pointer-speed legend, .repeat-options legend { width: 100%; margin-bottom: 8px; color: var(--muted); } -.pointer-speed legend strong { float: right; color: inherit; } -.segmented.compact.five { grid-template-columns: repeat(5, 1fr); } -.segmented.compact.four { grid-template-columns: repeat(4, 1fr); } -.segmented.compact.five button, .segmented.compact.four button { min-width: 0; padding: 0 6px; } -.pointer-speed .pointer-speed-slider { width: 100%; padding: 0; accent-color: var(--brand); cursor: pointer; } -.pointer-speed-ends, .pointer-speed-steps { display: flex; justify-content: space-between; gap: 12px; } -.pointer-speed-ends { color: var(--muted); } -.pointer-speed-fine-tune { display: grid; gap: 12px; } -.pointer-speed-fine-tune p { margin: 0; } -.pointer-speed-steps button { flex: 1; } -.exact-speed { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: var(--muted); } -.exact-speed input { min-width: 92px; min-height: 32px; border: 1px solid var(--border); border-radius: 5px; padding: 4px 8px; color: inherit; background: var(--surface); } -.repeat-settings { display: grid; gap: 17px; border-top: 1px solid var(--border); padding-top: 17px; } -.repeat-options { display: grid; gap: 17px; } -/* A disabled group dims itself and the prose of the note beside it, never a - container: the note's disclosure stays operable and must read that way, and - a child cannot escape a parent opacity. Buttons inside a disabled fieldset - already match button:disabled, so reset them to stop the two rules - compounding to ~0.30. */ -.setting-controls fieldset:disabled { opacity: 0.55; } -.setting-controls fieldset:disabled button:disabled { opacity: 1; } -.option-with-note > fieldset:disabled + .setting-note-block .setting-note { opacity: 0.55; } -.overlay-options { display: grid; gap: 17px; border-top: 1px solid var(--border); padding-top: 17px; } -.overlay-options fieldset { min-width: 0; margin: 0; border: 0; padding: 0; } -.overlay-options legend { margin-bottom: 8px; color: var(--muted); } -.setting-note { margin-top: 8px; color: var(--muted); line-height: 1.5; } -.privacy-choice { display: flex; gap: 8px; margin-top: 10px; } -.segmented.compact { display: grid; width: 100%; min-width: 0; margin: 0; } -.segmented.compact.three { grid-template-columns: repeat(3, 1fr); } -.segmented.compact button[aria-pressed="true"] { color: var(--on-brand-container); background: var(--segment-selected); box-shadow: inset 0 0 0 1px var(--brand-border); font-weight: 650; } -.color-options { display: flex; gap: 10px; } -.color-options label { position: relative; display: grid; place-items: center; width: 34px; height: 34px; cursor: pointer; } -.color-options input { position: absolute; width: 1px; height: 1px; opacity: 0; } -.color-swatch { width: 22px; height: 22px; border: 1px solid rgba(20, 30, 25, 0.26); border-radius: 50%; } -.color-options input:checked + .color-swatch { outline: 2px solid var(--brand); outline-offset: 3px; } -.color-options input:focus-visible + .color-swatch { outline: 2px solid var(--focus); outline-offset: 3px; } -.color-swatch.red { background: #d32f2f; } -.color-swatch.green { background: #84ff91; } -.color-swatch.blue { background: #64a6ff; } -.color-swatch.yellow { background: #ffd166; } -.color-swatch.white { background: #ffffff; } -.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } - -.segmented { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 22px; border: 1px solid var(--border); border-radius: 6px; padding: 3px; background: var(--surface-muted); } -.segmented button { min-height: 32px; border: 0; border-radius: 4px; padding: 0 14px; color: var(--muted); background: transparent; cursor: pointer; } -.segmented button[aria-selected="true"] { color: var(--on-brand-container); background: var(--segment-selected); box-shadow: inset 0 0 0 1px var(--brand-border); font-weight: 650; } -.task-list { border-top: 1px solid var(--border); } -.task-list article { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; min-height: 78px; border-bottom: 1px solid var(--border); padding: 12px 4px; } -.task-list article > svg { margin: 0 9px; color: var(--muted); } -.task-list h2 { } -.task-list p { margin-top: 4px; color: var(--muted); line-height: 1.4; } -.task-list .permission-recovery { margin-top: 8px; color: var(--text); } -.setup-launch { margin-bottom: 18px; } - -.modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 20px; background: rgba(28, 27, 31, 0.48); } -.pairing-dialog { width: min(100%, 620px); max-height: calc(100vh - 40px); overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 24px; color: var(--text); background: var(--surface); box-shadow: 0 20px 60px rgba(28, 27, 31, 0.2); } -.setup-dialog { display: flex; flex-direction: column; width: min(100%, 760px); height: min(520px, calc(100vh - 40px)); min-height: 0; max-height: calc(100vh - 40px); overflow: hidden; border: 1px solid var(--border); border-radius: 10px; padding: 28px; color: var(--text); background: var(--surface); box-shadow: 0 20px 60px rgba(28, 27, 31, 0.2); } -.setup-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } -.setup-dialog > header, .setup-dialog > footer { display: flex; align-items: center; gap: 10px; } -.setup-dialog > header { justify-content: space-between; } -.setup-dialog > header span { color: var(--brand); font-weight: 700; text-transform: uppercase; } -.setup-dialog > header h2 { margin-top: 4px; font-size: 22px; } -.setup-dialog > header strong { color: var(--muted); } -.setup-progress { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-top: 18px; } -.setup-progress span { height: 4px; border-radius: 2px; background: var(--border); } -.setup-progress span[data-active="true"] { background: var(--brand); } -.setup-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 42px 14px 28px; } -.setup-content h3 { font-size: 18px; } -.setup-content p { margin-top: 8px; color: var(--muted); line-height: 1.55; } -.setup-statuses { border-top: 1px solid var(--border); } -.setup-statuses article { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 92px; border-bottom: 1px solid var(--border); padding: 14px 4px; } -.setup-statuses h3 { } -.mobile-download { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 36px; } -.mobile-download figure { margin: 0; max-width: 210px; } -.mobile-download figcaption { margin-top: 8px; color: var(--muted); } -.mobile-pairing { margin-top: 28px; } -.mobile-pairing ol { padding-left: 24px; color: var(--muted); line-height: 1.6; } -.mobile-download a { width: max-content; margin-top: 22px; text-decoration: none; } -.mobile-download img { width: 190px; height: 190px; border: 1px solid var(--border); border-radius: 9px; padding: 10px; background: white; image-rendering: crisp-edges; } -.setup-pairings { margin-top: 20px; border-top: 1px solid var(--border); } -.setup-pairings article { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 16px; min-height: 82px; border-bottom: 1px solid var(--border); } -.setup-pairings article > div:first-child { display: grid; gap: 4px; } -.setup-pairings article span { color: var(--muted); } -.setup-pairings output { font: 700 24px/1 ui-monospace, monospace; letter-spacing: 0.1em; } -.setup-pairings article > div:last-child, .setup-choices { display: flex; gap: 8px; } -.setup-choices { margin-top: 22px; } -.setup-choices button[aria-pressed="true"] { border-color: var(--brand); color: var(--on-brand-container); background: var(--segment-selected); box-shadow: inset 0 0 0 1px var(--brand-border); font-weight: 650; } -.setup-privacy { display: inline-block; margin-top: 18px; color: var(--brand); } -.setup-dialog > footer { border-top: 1px solid var(--border); padding-top: 18px; } -.setup-dialog > footer > span { flex: 1; } -.pairing-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } -.pairing-dialog > header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px; } -.pairing-dialog > header > svg { margin-top: 2px; color: var(--brand); } -.pairing-dialog > header > span { display: grid; place-items: center; min-width: 28px; height: 28px; border-radius: 14px; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } -.pairing-dialog h2 { font-size: 19px; } -.pairing-dialog h3 { } -.pairing-dialog p { margin-top: 4px; color: var(--muted); line-height: 1.4; } -.pairing-dialog .pairing-connection { color: var(--text); } -.pairing-list { margin-top: 20px; border-top: 1px solid var(--border); } -.pairing-list article { display: grid; grid-template-columns: minmax(110px, 1fr) auto auto; align-items: center; gap: 16px; min-height: 88px; border-bottom: 1px solid var(--border); padding: 12px 0; } -.pairing-list output { color: var(--text); font: 700 25px/1 ui-monospace, monospace; letter-spacing: 0.12em; white-space: nowrap; } -.pairing-actions { display: flex; gap: 8px; } -.profile-dialog { width: min(100%, 700px); max-height: calc(100vh - 40px); overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 24px; color: inherit; background: var(--surface); box-shadow: 0 20px 60px rgba(28, 27, 31, 0.2); } -.profile-dialog > header, .profile-dialog > footer { display: flex; align-items: center; gap: 10px; } -.profile-dialog > header { justify-content: space-between; } -.profile-dialog h2 { font-size: 18px; } -.profile-dialog header p { margin-top: 4px; color: var(--muted); } -.field { display: grid; gap: 6px; margin-top: 20px; color: var(--muted); } -.field input, .binding-row input, .binding-row select { min-width: 0; min-height: 36px; border: 1px solid var(--border); border-radius: 5px; padding: 6px 9px; color: inherit; background: var(--surface); } -.field input[aria-invalid="true"], .binding-row [aria-invalid="true"] { border-color: var(--status-error); } -.field-error { color: var(--status-error); line-height: 1.35; } -.dialog-error { margin-top: 16px; border-left: 3px solid var(--status-error); padding: 9px 11px; color: var(--status-error); background: var(--status-error-soft); } -.binding-list { margin-top: 18px; border-top: 1px solid var(--border); } -.binding-row { display: grid; grid-template-columns: 74px 140px minmax(160px, 1fr); align-items: center; gap: 10px; min-height: 54px; border-bottom: 1px solid var(--border); } -.binding-row strong { } -.binding-error { grid-column: 3; margin-top: -8px; padding-bottom: 7px; } -.profile-dialog > footer { margin-top: 20px; } -.profile-dialog > footer > span { flex: 1; } -.confirm-dialog { width: min(100%, 480px); } -.error-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 14px; color: var(--status-error); background: var(--status-error-soft); } -.error-banner button { border: 0; color: inherit; background: transparent; cursor: pointer; font-weight: 650; } -.update-banner { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 10px 14px; color: var(--on-brand-container); background: var(--brand-container); } -.update-banner > svg { color: var(--brand); } -.update-banner .text-button { min-height: 30px; padding: 0 8px; color: var(--on-brand-container); } -.loading { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 100vh; color: var(--muted); } -.spin { animation: spin 1s linear infinite; } -@keyframes spin { to { transform: rotate(360deg); } } - -@media (max-width: 680px) { - .brand, .sidebar-footer { display: none; } - .nav-button { justify-content: center; } - .pairing-list article { grid-template-columns: minmax(0, 1fr) auto; } - .pairing-actions { grid-column: 1 / -1; justify-content: flex-end; } - .view { } - .connection-band { grid-template-columns: auto 1fr; } - .connection-band > button { grid-column: 1 / -1; } - .update-banner { grid-template-columns: auto 1fr; } - .update-banner .text-button { grid-column: 2; justify-self: start; padding-left: 0; } - .setting-group { } - .binding-row { grid-template-columns: 72px 1fr; padding: 9px 0; } - .binding-row input { grid-column: 1 / -1; } - .binding-error { grid-column: 1 / -1; margin-top: 0; } - .profile-dialog > footer { flex-wrap: wrap; } - .profile-dialog > footer > span { flex-basis: 100%; height: 0; } - .task-list article { grid-template-columns: auto minmax(0, 1fr); } - .task-list article > button { grid-column: 1 / -1; } - .setup-dialog { min-height: 0; padding: 20px; } - .setup-content { padding: 28px 0 20px; } - .mobile-download { grid-template-columns: 1fr; } - .mobile-download img { width: 150px; height: 150px; } - .setup-statuses article, .setup-pairings article { grid-template-columns: auto 1fr; } - .setup-statuses article > button, .setup-pairings article > div:last-child { grid-column: 1 / -1; } -} - -@media (prefers-color-scheme: dark) { - :root { - --brand: #d65a52; - --brand-label: #ffb4ab; - --brand-hover: #e06b63; - --brand-container: #5b1b18; - --brand-border: #8f4943; - --on-brand: #141318; - --on-brand-container: #ffdad6; - --chrome: #a8231c; - --segment-selected: #3b2424; - --app-background: #141318; - --surface: #1f1f23; - --surface-muted: #2a292f; - --surface-hover: #343138; - --text: #f4eff4; - --muted: #cac4d0; - --border: #45424b; - --inactive: #938f99; - --status-ok: #81c784; - --status-ok-soft: #1c3821; - --status-error: #ffb4ab; - --status-error-soft: #3b2221; - --focus: #9ecbff; - } - .color-swatch.white { border-color: var(--inactive); } -} - -.tab-bar { display: grid; grid-template-columns: repeat(var(--tab-count), 1fr); width: 100%; min-width: 0; } -.tab-bar button { position: relative; min-width: 0; padding: 0 8px; } -/* Positioned siblings paint in order, so lift a focused tab or the selected - neighbour overpaints one edge of its focus ring. */ -.tab-bar button:focus-visible { z-index: 1; } - -.tab-panel:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; } - -.disclosure { display: inline-flex; align-items: center; align-self: start; min-height: 28px; margin-top: 8px; border: 0; padding: 0; color: var(--brand); background: transparent; font-weight: 650; cursor: pointer; } -.disclosure:hover { text-decoration: underline; } - -.setting-note-block { display: grid; justify-items: start; } -.setting-note-block .disclosure { margin-top: 4px; } - -/* Out of flow so a marker never wraps the label and grows the tab row. */ -/* The transparent outline is invisible normally and becomes a visible ring - under forced colours, where the background alone would be flattened away. */ -.tab-attention { position: absolute; top: 5px; right: 6px; width: 8px; height: 8px; border-radius: 50%; outline: 1px solid transparent; background: var(--status-error); } - -.settings-view > .tab-bar { grid-template-columns: repeat(var(--tab-count), auto); } -.settings-view > .tab-bar button { padding: 0 4px; } - -/* Switches: a summary row per switch, expanding to an editor. Switch-access - users step through every control, so the collapsed row exposes only Edit - and Remove. */ -.switch-status { margin-top: 0; } -.switch-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 0; } -.switch-empty { padding: 36px 24px; } -.switch-empty h3 { margin-top: 12px; color: var(--text); } -.switch-empty .primary { margin-top: 16px; } -.switch-list { display: grid; gap: 8px; } -.switch-row { border: 1px solid var(--border); border-radius: 8px; background: var(--surface); } -.switch-row[data-open] { border-color: var(--brand-border); } -.switch-row-summary { display: grid; align-items: center; min-height: 60px; } -.switch-row-summary > svg { color: var(--muted); } -.switch-row-summary h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.switch-row-summary p { margin-top: 3px; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } -.key-badge { display: inline-flex; align-items: center; min-height: 26px; border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 5px; padding: 0 9px; background: var(--surface-muted); font: 600 16px/1 ui-monospace, Consolas, monospace; white-space: nowrap; } -.key-badge[data-unavailable] { border-color: var(--status-error); color: var(--status-error); background: var(--status-error-soft); } -.key-badge-empty { border-style: dashed; color: var(--muted); font-family: inherit; font-weight: 500; } -.capture-dialog { display: grid; justify-items: center; width: min(100%, 420px); border: 1px solid var(--brand-border); border-radius: 10px; padding: 32px 28px 24px; color: var(--text); background: var(--surface); box-shadow: 0 20px 60px rgba(28, 27, 31, 0.2); text-align: center; animation: capture-pulse 1.4s ease-in-out infinite; } -.capture-dialog:focus { outline: none; } -.capture-dialog > svg { color: var(--brand); } -.capture-dialog h2 { margin-top: 14px; font-size: 18px; } -.capture-dialog p { margin-top: 8px; color: var(--muted); line-height: 1.5; } -.capture-dialog button { margin-top: 20px; } -@keyframes capture-pulse { 50% { border-color: var(--brand); box-shadow: 0 20px 60px rgba(28, 27, 31, 0.2), 0 0 0 4px var(--brand-container); } } -@media (prefers-reduced-motion: reduce) { .capture-dialog { animation: none; } } -.switch-editor { display: grid; gap: 16px; min-width: 0; margin: 0; border: 0; border-top: 1px solid var(--border); padding: 4px 14px 14px; } -.switch-editor .field { margin-top: 0; } -.switch-editor .field > span:first-child { font-weight: 600; } -.switch-editor select { min-width: 0; min-height: 36px; border: 1px solid var(--border); border-radius: 5px; padding: 6px 9px; color: var(--text); background: var(--surface); } -.key-row { display: flex; align-items: center; gap: 10px; } -.hold-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; } -.hold-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto auto auto; align-items: center; gap: 4px; } -.hold-step { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } -.hold-list select { margin-right: 6px; } -.hold-actions .setting-note { margin-top: 2px; } -.hold-actions .disclosure { margin-top: 0; } -.switch-editor-footer { display: flex; gap: 8px; padding-top: 4px; } -.switch-editor-footer .danger { margin-left: auto; } -.switch-list-actions { display: flex; } -.switch-empty .switch-list-actions { margin-top: 16px; } -.switch-empty .primary { margin-top: 0; } - -@media (max-width: 680px) { - .switch-row-summary { } - .switch-row-summary .key-badge { grid-column: 2; justify-self: start; } - .switch-row-summary > .secondary { grid-column: 3; grid-row: 1; } - .switch-row-summary > .icon-button { grid-column: 3; grid-row: 2; justify-self: end; } -} - -.setup-backdrop[hidden] { display: none; } -.setup-dialog .setting-group { } -.setup-dialog > footer { flex-wrap: wrap; } - -.setup-dialog[data-switch-step="true"] { height: min(760px, calc(100vh - 40px)); } -.setup-dialog[data-switch-step="true"] .setup-content { padding-top: 16px; } -.setup-dialog .setting-group { } - -.scanner-colours { display: flex; flex-wrap: wrap; gap: 12px; } -.scanner-colours label { display: flex; align-items: center; gap: 6px; cursor: pointer; } -.scanner-colours input { accent-color: var(--brand); } -.scanner-sample { --scanner-colour: #64a6ff; height: 100px; margin-top: 16px; padding: 16px; background: #1e232e; border-radius: 8px; } -.scanner-sample.red { --scanner-colour: #d32f2f; } -.scanner-sample.green { --scanner-colour: #84ff91; } -.scanner-sample.yellow { --scanner-colour: #ffd166; } -.scanner-sample.white { --scanner-colour: #ffffff; } -.scanner-sample-selection { display: flex; align-items: center; justify-content: center; height: 100%; border: 3px solid var(--scanner-colour); background: color-mix(in srgb, var(--scanner-colour) 25%, transparent); color: white; } - -.scanner-sample-selection.thin { border-width: 1px; } -.scanner-sample-selection.standard { border-width: 2px; } -.scanner-sample-selection.thick { border-width: 4px; } -.scanner-preference + .scanner-preference { margin-top: 20px; } - -.scanner-settings { max-width: 920px; } -.scanner-status { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 20px; padding: 16px 20px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-muted); } -.scanner-help { margin: 18px 0 28px; color: var(--muted); line-height: 1.6; } -.scanner-help summary { cursor: pointer; color: var(--text); padding: 8px 0; } -.scanner-help p + p { margin-top: 10px; } -.scanner-panel-heading { margin-bottom: 24px; } -.scanner-panel-heading h2, .scanner-areas h2 { font-size: 20px; margin-bottom: 8px; } -.scanner-panel-heading > p, .scanner-areas header p { color: var(--muted); line-height: 1.5; } -.scanner-panel-heading > button { padding-left: 0; margin-bottom: 16px; } -.scanner-preferences .setting-group { } -.scanner-preferences .setting-group h2 { font-size: 16px; } -.scanner-preferences .setting-group header p, .scanner-preferences .setting-note { line-height: 1.5; } -.scanner-preferences .setting-controls { } -.scanner-preferences .scanner-preference { margin: 0; min-width: 0; } -.scanner-inheritance { display: flex; align-items: center; justify-content: space-between; min-height: 36px; margin-bottom: 8px; color: var(--muted); } -.scanner-inheritance > span, .scanner-area-badge { background: var(--surface-muted); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; } -.scanner-inheritance .custom { color: var(--text); font-weight: 600; } -.scanner-preferences .exact-speed { margin: 0; color: var(--text); } -.scanner-preferences .exact-speed select, .scanner-preferences .exact-speed input { min-height: 44px; max-width: 100%; border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; color: var(--text); background: var(--surface); } -.scanner-preferences .segmented button { min-height: 44px; } -.scanner-areas { margin-top: 24px; } -.scanner-area-cards { display: grid; margin-top: 20px; } -.scanner-area-card { display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px solid var(--border); border-radius: 12px; color: var(--text); background: var(--surface); text-align: left; cursor: pointer; font: inherit; min-width: 0; } -.scanner-area-card:hover { background: var(--surface-hover); } -.scanner-area-card strong { display: flex; justify-content: space-between; gap: 10px; font-size: 16px; } -.scanner-area-card > span { color: var(--muted); line-height: 1.5; } -.scanner-area-appearance { display: flex; align-items: center; gap: 8px; text-transform: capitalize; } -.scanner-area-badge { align-self: flex-start; margin-top: auto; } -.scanner-save-note { margin-top: 24px; } -@media (max-width: 850px) { - .scanner-area-cards { } - .scanner-preferences .setting-group { } -} -@media (max-width: 520px) { - .scanner-preferences .exact-speed { align-items: stretch; flex-direction: column; } - .scanner-preferences .segmented { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} -.scanner-preferences fieldset { min-width: 0; margin: 0; padding: 0; border: 0; } -.scanner-preferences legend { padding: 0; margin-bottom: 10px; color: var(--text); } -.scanner-preferences .scanner-areas { margin: 0 0 32px; } -@media (max-width: 850px) { - .scanner-area-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; padding: 16px; } - .scanner-area-card strong { grid-column: 1 / -1; margin-bottom: 4px; } - .scanner-area-badge { grid-column: 2; grid-row: 2 / 4; align-self: center; margin: 0; } - .scanner-area-card > span:not(.scanner-area-badge) { grid-column: 1; } -} - -.scan-rate-stepper { display: flex; align-items: center; } -.scan-rate-stepper button { } -.scan-rate-stepper output { min-width: 72px; text-align: center; font-variant-numeric: tabular-nums; } - -.switch-practice-entry { padding: 12px 0; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } -.switch-practice-entry p { width: 100%; } -.switch-practice-entry label { display: flex; align-items: center; gap: 8px; } -.switch-practice-dialog { background: var(--surface); color: var(--text); border-radius: 12px; padding: 24px; width: min(720px, 95vw); max-height: 92vh; overflow-y: auto; } -.switch-practice-buttons { display: flex; gap: 12px; justify-content: flex-end; } - -.switch-practice-blocks { list-style: none; margin: 16px 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; font-size: 16px; } -.switch-practice-blocks li { min-height: 72px; padding: 16px; display: flex; align-items: center; justify-content: center; text-align: center; border: 2px solid var(--border); border-radius: 8px; background: var(--panel, var(--surface)); } -.switch-practice-blocks li[data-selected="true"] { background: var(--segment-selected); color: var(--on-brand-container); font-weight: 700; border-color: var(--brand); } -.setup-navigation-guidance { flex: 0 0 auto; margin: 0; color: var(--muted); line-height: 1.5; } -.setup-navigation-guidance:not(:empty) { padding-bottom: 16px; } - -} diff --git a/src/ui/app.css b/src/ui/app.css index 3bf23ce3..7f607137 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -1,115 +1,529 @@ +/* Main-window and portalled-dialog styles. + + One layer, one source of truth. Rules are ordered from tokens and base + elements to shared controls, then page areas, then responsive overrides, so + a later rule of equal specificity is always the intended one. Native + scanning panels, menus and overlays are styled elsewhere. */ + @layer components { - /* Main-window and portalled-dialog styles only. No global Preflight. */ - :where(.app-shell, .modal-backdrop, .capture-backdrop) { @apply text-lg leading-relaxed; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) [hidden] { display: none !important; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) :where(p, small, legend, label, .setting-note, .field-error, .key-error, .sidebar-footer, .teaching-steps, .teaching-caption) { font-size: 1rem; line-height: 1.5; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) :where(h2, h3) { @apply text-xl font-semibold; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) :where(button, a[href], input:not([type="checkbox"]):not([type="radio"]), select, summary) { min-height: 64px; min-width: 64px; font-size: 18px; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) a[href] { @apply inline-flex items-center rounded-xl px-3 py-3; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) :where(button, a, input, select, summary):focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } - :where(.app-shell, .modal-backdrop, .capture-backdrop) summary { @apply flex cursor-pointer items-center rounded-xl px-4 py-3 font-semibold; } - .app-shell { grid-template-columns: 240px minmax(0, 1fr); } - .app-shell aside { @apply gap-3; overflow-y: auto; } - .app-shell nav { @apply gap-3; } - .app-shell .nav-button { @apply text-left; } - .view { width: min(100%, 1100px); @apply px-8 py-8; } - .page-header { @apply mb-6; } - .page-header h1 { @apply text-3xl; } - .page-header p { @apply text-base; } - .setting-group { @apply grid grid-cols-1 gap-4 py-6; } - .setting-controls, .repeat-options, .repeat-settings, .overlay-options { @apply gap-4; } - .setting-group header:has(p:empty) p { display: none; } - .more-options { @apply my-4 min-w-0; } - .more-options-content { @apply pt-4; } - .toggle-row > span:first-child { font-size: 18px; } - .toggle-row { @apply min-h-16 rounded-xl border px-4 py-3 text-lg; border-color: var(--border); background: var(--surface); } - .toggle { width: 52px; height: 30px; } - .toggle::after { width: 24px; height: 24px; } - .toggle-row input:checked + .toggle::after { transform: translateX(22px); } - .segmented, .segmented.compact, .segmented.compact.three, .segmented.compact.four, .segmented.compact.five { @apply grid gap-3 border-0 bg-transparent p-0; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } - .segmented button { border: 1px solid var(--border); background: var(--surface); } - .segmented button[aria-pressed="true"], .segmented button[aria-selected="true"] { border: 2px solid var(--brand); background: var(--brand-container); color: var(--on-brand-container); } - .update-controls, .update-controls > div, .privacy-choice, .pairing-actions, .profile-dialog > header, .profile-dialog > footer, .binding-row, .update-banner { @apply gap-3; } - .privacy-choice, .pairing-actions, .setup-dialog > header { flex-wrap: wrap; } - .tab-bar { @apply mb-6; } - .exact-speed { @apply flex flex-wrap items-center gap-3 text-base; } - .exact-speed > select, .exact-speed > input { max-width: 240px; } - .scan-rate-stepper { @apply flex items-center gap-3; } - .scan-rate-stepper output { @apply min-w-20 text-center text-lg; } - .scanner-area-cards { @apply grid grid-cols-1 gap-3; } - .scanner-area-card { @apply items-start p-5 text-left; } - .scanner-colours, .color-options { @apply flex flex-wrap gap-3; } - .scanner-colours label, .color-options label { @apply min-h-16 min-w-16 items-center justify-center rounded-xl px-4 py-3 text-lg; } - .switch-row-summary { @apply grid items-center gap-3 p-4; grid-template-columns: auto minmax(0, 1fr) auto auto; } - .switch-row-summary > .key-badge { grid-column: 2; justify-self: start; font-size: 16px; } - .switch-row-summary > button { grid-row: 1; } - .switch-row-summary > button:first-of-type { grid-column: 3; } - .switch-row-summary > button:last-of-type { grid-column: 4; } - .switch-row-summary p { white-space: normal; overflow-wrap: anywhere; } - .hold-list, .hold-list li, .key-row, .switch-editor-footer { gap: 12px; } - .hold-list li { grid-template-columns: 24px minmax(0, 1fr) repeat(3, 64px); } - .key-row, .switch-editor-footer { flex-wrap: wrap; } - .scanner-area-card strong { font-size: 18px; } - .scanner-area-card > span, .scanner-inheritance { font-size: 16px; } - .profile-dialog > header, .pairing-dialog > header { flex-wrap: wrap; } - .teaching-disclosure > button > span { min-width: 0; overflow-wrap: anywhere; } - .more-options button, .app-shell button { white-space: normal; } - .switch-list { @apply grid gap-4; } - .switch-list-actions, .switch-editor-actions, .setup-choices, .teaching-controls, .dialog-actions { @apply flex flex-wrap gap-3; } - .switch-editor { @apply grid gap-4 p-4; } - .switch-editor .field, .field { @apply min-w-0; } - .switch-practice-entry { @apply grid gap-3; } - .switch-practice-entry > p:first-child { @apply text-base; } - .switch-practice-entry label { @apply flex flex-wrap items-center gap-3; } - .switch-practice-entry select { width: auto; max-width: 100%; } - .connection-band { @apply grid gap-4 rounded-xl p-5; grid-template-columns: auto minmax(0, 1fr); } - .connection-band > button { grid-column: 1 / -1; } - .status-list article, .task-list article { @apply grid gap-4 py-4; grid-template-columns: auto minmax(0, 1fr); } - .status-list article > button, .task-list article > button { grid-column: 1 / -1; justify-self: start; } - .profile-row, .device-list article { @apply gap-3 py-4; } - .profile-row { @apply text-left; } - .modal-backdrop { @apply p-3; } - .setup-dialog { width: min(100%, 920px); height: min(900px, calc(100dvh - 24px)); max-height: calc(100dvh - 24px); @apply p-5; } - .setup-dialog[data-switch-step="true"] { height: min(900px, calc(100dvh - 24px)); } - .setup-content, .setup-dialog[data-switch-step="true"] .setup-content { @apply px-1 py-4; } - .setup-dialog > footer { @apply flex-wrap gap-3 pt-3; } - .setup-dialog > header h2 { @apply text-2xl; } - .setup-dialog > header span, .setup-dialog > header strong { @apply text-base; } - .pairing-dialog, .profile-dialog, .switch-confirm-dialog, .switch-practice-dialog { width: min(100%, 900px); max-height: calc(100dvh - 24px); @apply overflow-auto p-5; } - .pairing-row, .pairing-list article { @apply flex flex-wrap gap-4; } - .mobile-download { @apply gap-5; } - .teaching-disclosure { @apply my-4; } - .teaching-disclosure > button { @apply w-full flex-wrap justify-between border text-left; border-color: var(--border); } - .teaching-demo { @apply max-w-full p-4; } - .teaching-caption { @apply text-lg; } - .home-actions { @apply mt-6 grid gap-4; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } - .home-actions > button { @apply min-h-24 text-xl; } - @media (max-width: 800px) { - .app-shell { grid-template-columns: 1fr; } - .app-shell aside { position: static; height: auto; @apply p-3; } - .app-shell aside .brand, .app-shell .sidebar-footer { display: none; } - .app-shell nav { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .app-shell .nav-button { @apply justify-center; flex-direction: column; } - .app-shell .nav-button span { display: block; } - .view { @apply p-4; } - .switch-row-summary { grid-template-columns: auto minmax(0, 1fr); } - .switch-row-summary > button:first-of-type, .switch-row-summary > button:last-of-type { width: 100%; grid-row: auto; grid-column: auto; } - .hold-list li { grid-template-columns: repeat(3, minmax(64px, 1fr)); } - .hold-list li > select { grid-column: 2 / -1; } - .mobile-download { grid-template-columns: 1fr; } - .mobile-download figure { justify-self: start; } - .profile-row { grid-template-columns: auto minmax(0, 1fr) auto; } - .profile-row > span:not(:last-child) { display: none; } - } - @media (max-width: 480px) { - .app-shell nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .view { padding: 12px; } - .connection-band { grid-template-columns: minmax(0, 1fr); } - .profile-row { display: flex; flex-wrap: wrap; } - } - @media (max-height: 600px) { - .setup-dialog { overflow-y: auto; } - .setup-dialog > * { flex-shrink: 0; } - .setup-dialog .setup-content { overflow: visible; } + +/* --- Tokens --------------------------------------------------------------- + The palette follows Switchify for Android's Material colours so the two apps + read as one product. Brand red marks identity, the active place and the one + primary action on a view. Status colours carry meaning and are always + paired with an icon or words. */ +:root { + font-family: Inter, "Segoe UI", system-ui, sans-serif; + color: var(--text); + background: var(--app-background); + font-synthesis: none; + text-rendering: optimizeLegibility; + + --brand: #d32f2f; + --brand-label: #b3261e; + --brand-hover: #b3261e; + --brand-container: #ffdad6; + --brand-border: #efb8b4; + --on-brand: #ffffff; + --on-brand-container: #410002; + --chrome: #d32f2f; + --segment-selected: #fff1f1; + + --app-background: #f5f4f7; + --surface: #ffffff; + --surface-muted: #f3f2f6; + --surface-hover: #ebe9ef; + --surface-disabled: #ebe9ef; + --text: #1c1b1f; + --muted: #49454f; + --ink-disabled: #6f6b76; + --border: #e2e0e8; + --border-strong: #c9c5d0; + --inactive: #79747e; + + --status-ok: #2e7d32; + --status-ok-soft: #e8f5e9; + --status-attention: #8a5300; + --status-attention-soft: #ffefd6; + --status-error: #ba1a1a; + --status-error-soft: #ffdad6; + --status-error-border: #e8a39d; + --focus: #1f6fa2; + + --radius-control: 12px; + --radius-card: 12px; + --radius-dialog: 16px; + --shadow-dialog: 0 24px 64px rgba(28, 27, 31, 0.22); +} + +@media (prefers-color-scheme: dark) { + :root { + --brand: #d65a52; + --brand-label: #ffb4ab; + --brand-hover: #e06b63; + --brand-container: #5b1b18; + --brand-border: #8f4943; + --on-brand: #141318; + --on-brand-container: #ffdad6; + --chrome: #a8231c; + --segment-selected: #3b2424; + + --app-background: #141318; + --surface: #1f1f23; + --surface-muted: #2a292f; + --surface-hover: #343138; + --surface-disabled: #2a292f; + --text: #f4eff4; + --muted: #cac4d0; + --ink-disabled: #9a96a1; + --border: #45424b; + --border-strong: #5d5a63; + --inactive: #938f99; + + --status-ok: #81c784; + --status-ok-soft: #1c3821; + --status-attention: #ffb95c; + --status-attention-soft: #3a2a12; + --status-error: #ffb4ab; + --status-error-soft: #3b2221; + --status-error-border: #8c4a44; + --focus: #9ecbff; + + --shadow-dialog: 0 24px 64px rgba(0, 0, 0, 0.5); } + .color-swatch.white { border-color: var(--inactive); } +} + +/* --- Base ------------------------------------------------------------------ */ +* { box-sizing: border-box; } +html, body, #root { width: 100%; min-width: 0; min-height: 100%; margin: 0; } +button, input, select { font: inherit; } +button { letter-spacing: 0; } +h1, h2, h3, p { margin: 0; letter-spacing: 0; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } + +/* Everything the main window and its dialogs render starts from one type + scale: 18px body copy, 16px supporting text, 20px section headings. */ +:where(.app-shell, .modal-backdrop) { font-size: 18px; line-height: 1.625; } +:where(.app-shell, .modal-backdrop) [hidden] { display: none !important; } +:where(.app-shell, .modal-backdrop) :where(p, small, legend, label, .setting-note, .field-error, .key-error, .sidebar-footer, .teaching-steps, .teaching-caption) { font-size: 1rem; line-height: 1.5; } +:where(.app-shell, .modal-backdrop) :where(h2, h3) { font-size: 1.25rem; line-height: 1.4; font-weight: 600; } + +/* Every interactive target is at least 64px square for switch and pointer + users alike. */ +:where(.app-shell, .modal-backdrop) :where(button, a[href], input:not([type="checkbox"]):not([type="radio"]), select, summary) { min-height: 64px; min-width: 64px; font-size: 18px; } +:where(.app-shell, .modal-backdrop) :where(button, a, input, select, summary):focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } +:where(.app-shell, .modal-backdrop) :where(a[href]) { display: inline-flex; align-items: center; border-radius: var(--radius-control); padding: 12px; } +:where(.app-shell, .modal-backdrop) :where(summary) { display: flex; align-items: center; border-radius: var(--radius-control); padding: 12px 16px; font-weight: 600; cursor: pointer; } + +/* --- Buttons ---------------------------------------------------------------- + `.btn` is the shape every Button shares. A variant sets its weight on the + page: primary for the one main action, secondary for everything else, text + for low-emphasis links, and danger for removal. A button with no variant + reads as secondary. */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 12px; + min-height: 64px; min-width: 64px; + border: 1px solid var(--border-strong); border-radius: var(--radius-control); + padding: 12px 20px; + color: var(--text); background: var(--surface); + font-size: 18px; font-weight: 600; line-height: 1.375; + white-space: normal; cursor: pointer; + transition: background-color 120ms, border-color 120ms, color 120ms; +} +.btn:hover { background: var(--surface-hover); } +.btn.primary { border-color: var(--brand); color: var(--on-brand); background: var(--brand); } +.btn.primary:hover { border-color: var(--brand-hover); background: var(--brand-hover); } +.btn.secondary { border-color: var(--border-strong); color: var(--text); background: var(--surface); } +.btn.secondary:hover { background: var(--surface-hover); } +.btn.text-button { border-color: transparent; padding: 12px; color: var(--brand-label); background: transparent; } +.btn.text-button:hover { background: var(--surface-hover); } +.btn.secondary.danger { border-color: var(--status-error-border); color: var(--status-error); background: var(--surface); } +.btn.secondary.danger:hover { background: var(--status-error-soft); } +.btn.primary.danger { border-color: var(--status-error); color: var(--on-brand); background: var(--status-error); } +.btn.icon-button { border-color: transparent; padding: 0; color: inherit; background: transparent; } +.btn.icon-button:hover { background: var(--surface-hover); } +.btn.danger-icon { color: var(--status-error); } +.btn.danger-icon:hover { background: var(--status-error-soft); } + +/* Disabled controls go quiet and neutral rather than fading the brand red to + an alarming pink. Inside a disabled fieldset the whole group dims instead + (see .setting-controls), so a selected choice still shows which it is. */ +.btn:disabled { cursor: default; } +.btn:disabled:not(fieldset:disabled *) { border-color: var(--border); color: var(--ink-disabled); background: var(--surface-disabled); } +.btn:is(.text-button, .icon-button, .disclosure, .nav-button):disabled:not(fieldset:disabled *) { border-color: transparent; background: transparent; } + +/* --- Form fields ----------------------------------------------------------- */ +.field-control { + width: 100%; min-width: 0; min-height: 64px; + border: 1px solid var(--border-strong); border-radius: var(--radius-control); + padding: 12px 16px; + color: var(--text); background: var(--surface); + font-size: 18px; +} +.field-control[type="range"] { border: 0; padding: 12px 0; background: transparent; accent-color: var(--brand); cursor: pointer; } +.field-control[aria-invalid="true"] { border-color: var(--status-error); } +.field-control:disabled { color: var(--ink-disabled); background: var(--surface-disabled); } +.field-control[type="range"]:disabled { background: transparent; } +.field { display: grid; gap: 6px; min-width: 0; margin-top: 20px; color: var(--muted); } +.field-error { color: var(--status-error); line-height: 1.35; } + +/* --- Shell ------------------------------------------------------------------ */ +.app-shell { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; border-top: 3px solid var(--chrome); background: var(--app-background); } +.app-shell > aside { position: sticky; top: 0; display: flex; flex-direction: column; gap: 12px; height: 100vh; overflow-y: auto; padding: 20px 12px 14px; border-right: 1px solid var(--border); background: var(--surface-muted); } +.brand { display: flex; align-items: center; gap: 11px; min-height: 44px; padding: 0 8px 18px; } +.brand-mark { display: block; width: 36px; height: 36px; border-radius: 8px; object-fit: contain; } +.brand strong, .brand small { display: block; } +.brand strong { color: var(--brand-label); } +.brand small { margin-top: 2px; color: var(--muted); } +.app-shell nav { display: grid; gap: 12px; } +.btn.nav-button { justify-content: flex-start; width: 100%; border-color: transparent; color: var(--muted); background: transparent; text-align: left; } +.btn.nav-button:hover { background: var(--surface-hover); } +.btn.nav-button[data-active="true"] { color: #ffffff; background: var(--chrome); } +.sidebar-footer { margin-top: auto; padding: 12px 9px 0; color: var(--muted); } + +main { min-width: 0; } +.view { width: min(100%, 1100px); margin: 0 auto; padding: 32px; } +.page-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 24px; } +.page-header h1 { font-size: 30px; line-height: 1.2; font-weight: 700; } +.page-header p { margin-top: 5px; color: var(--muted); } +.page-header > svg { color: var(--muted); } + +.loading { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 100vh; color: var(--muted); } +.spin { animation: spin 1s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +/* --- Banners ---------------------------------------------------------------- */ +.error-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 14px; color: var(--status-error); background: var(--status-error-soft); } +.error-banner .btn { border-color: transparent; color: inherit; background: transparent; } +.update-banner { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; padding: 10px 14px; color: var(--on-brand-container); background: var(--brand-container); } +.update-banner > svg { color: var(--brand); } +.update-banner .btn.text-button { color: var(--on-brand-container); } + +/* --- Status and lists ------------------------------------------------------- */ +.connection-band { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 16px; min-height: 94px; border: 1px solid var(--border); border-left: 4px solid var(--inactive); border-radius: var(--radius-card); padding: 20px; background: var(--surface); } +.connection-band[data-connected="true"] { border-left-color: var(--status-ok); } +.connection-band p { margin-top: 4px; color: var(--muted); } +.connection-band > .btn { grid-column: 1 / -1; } +.status-icon { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; color: var(--muted); background: var(--surface-muted); } +.status-icon[data-ok="true"] { color: var(--status-ok); background: var(--status-ok-soft); } + +.status-list { margin-top: 22px; border-top: 1px solid var(--border); } +.status-list .status-icon { width: 34px; height: 34px; } +.status-list p { margin-top: 3px; color: var(--muted); } +.task-list { border-top: 1px solid var(--border); } +.task-list article > svg { margin: 0 9px; color: var(--muted); } +.task-list p { margin-top: 4px; color: var(--muted); line-height: 1.4; } +.task-list .permission-recovery { margin-top: 8px; color: var(--text); } +.status-list article, .task-list article { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 16px; min-height: 76px; border-bottom: 1px solid var(--border); padding: 16px 4px; } +.status-list article > .btn, .task-list article > .btn { grid-column: 1 / -1; justify-self: start; } +.setup-launch { margin-bottom: 18px; } + +.empty-state { display: grid; justify-items: center; border: 1px dashed var(--border); padding: 52px 24px; color: var(--muted); text-align: center; } +.empty-state h2 { margin-top: 14px; color: var(--text); } +.empty-state p { margin-top: 6px; } +.device-list, .profile-list { border-top: 1px solid var(--border); } +.device-list article, .btn.profile-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; min-height: 72px; border-bottom: 1px solid var(--border); padding: 16px 4px; } +.device-list p, .profile-list p { margin-top: 4px; color: var(--muted); } +.btn.profile-row { justify-content: stretch; width: 100%; grid-template-columns: auto 1fr auto auto; border-width: 0 0 1px; border-radius: 0; color: inherit; background: transparent; text-align: left; } +.btn.profile-row:hover { background: var(--surface-hover); } +.profile-row > span { color: var(--muted); } +.profile-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 6px; color: var(--muted); background: var(--surface-muted); } +.capability-note { margin-top: 16px; color: var(--muted); } + +/* --- Settings controls ------------------------------------------------------ */ +.setting-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; border-top: 1px solid var(--border); padding: 24px 0; } +.setting-group header p { margin-top: 5px; color: var(--muted); line-height: 1.5; } +.setting-group header:has(p:empty) p { display: none; } +.setting-group:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; } +.setting-controls { display: grid; align-content: start; gap: 16px; } +.setting-note { margin-top: 8px; color: var(--muted); line-height: 1.5; } +.setting-note-block { display: grid; justify-items: start; } +.setting-note-block .disclosure { margin-top: 4px; } +.privacy-choice { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; } + +.update-controls { display: grid; gap: 12px; color: var(--muted); } +.update-controls > div { display: flex; flex-wrap: wrap; gap: 12px; } +.update-controls progress { width: 100%; accent-color: var(--brand); } +.update-controls > span { font-variant-numeric: tabular-nums; } + +.toggle-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px; min-height: 64px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: 12px 16px; background: var(--surface); font-size: 18px; cursor: pointer; } +.toggle-row > span:first-child { font-size: 18px; } +.toggle-row input { position: absolute; width: 1px; height: 1px; opacity: 0; } +.toggle { position: relative; width: 52px; height: 30px; border-radius: 999px; background: var(--inactive); transition: background 140ms; } +.toggle::after { position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: white; content: ""; transition: transform 140ms; } +.toggle-row input:checked + .toggle { background: var(--brand); } +.toggle-row input:checked + .toggle::after { transform: translateX(22px); } +.toggle-row input:focus-visible + .toggle { outline: 3px solid var(--focus); outline-offset: 3px; } +.toggle-row[data-disabled="true"] { cursor: default; opacity: 0.55; } + +.pointer-speed, .repeat-options fieldset, .overlay-options fieldset, .scanner-preferences fieldset { min-width: 0; margin: 0; border: 0; padding: 0; } +.pointer-speed legend, .repeat-options legend { width: 100%; margin-bottom: 8px; color: var(--muted); } +.pointer-speed legend strong { float: right; color: inherit; } +.pointer-speed-ends, .pointer-speed-steps { display: flex; justify-content: space-between; gap: 12px; } +.pointer-speed-ends { color: var(--muted); } +.pointer-speed-fine-tune { display: grid; gap: 12px; } +.pointer-speed-fine-tune p { margin: 0; } +.pointer-speed-steps .btn { flex: 1; } +.exact-speed { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: var(--muted); font-size: 16px; } +.exact-speed > .field-control { max-width: 240px; } +.repeat-settings, .overlay-options { display: grid; gap: 16px; border-top: 1px solid var(--border); padding-top: 17px; } +.repeat-options { display: grid; gap: 16px; } +.overlay-options legend { margin-bottom: 8px; color: var(--muted); } +/* A disabled group dims itself and the prose of the note beside it, never a + container: the note's disclosure stays operable and must read that way, and + a child cannot escape a parent opacity. */ +.setting-controls fieldset:disabled { opacity: 0.55; } +.option-with-note > fieldset:disabled + .setting-note-block .setting-note { opacity: 0.55; } + +.more-options { margin: 16px 0; min-width: 0; } +.more-options-content { padding-top: 16px; } +.btn.more-options-toggle { justify-content: space-between; width: 100%; } + +.disclosure.btn { align-self: start; justify-content: flex-start; margin-top: 8px; border-color: transparent; padding: 12px 0; color: var(--brand-label); background: transparent; } +.disclosure.btn:hover { background: transparent; text-decoration: underline; } + +/* --- Teaching demonstrations -------------------------------------------------- */ +.teaching-disclosure { margin: 16px 0; } +.teaching-disclosure > .btn.disclosure { flex-wrap: wrap; justify-content: space-between; width: 100%; margin-top: 0; border-color: var(--border); padding: 12px 20px; text-align: left; } +.teaching-disclosure > .btn > span { min-width: 0; overflow-wrap: anywhere; } +.teaching-demo { max-width: 100%; padding: 16px; } +.teaching-caption { font-size: 18px; } + +/* --- Segmented choices and tabs --------------------------------------------- */ +.segmented { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; min-width: 0; margin: 0 0 22px; border: 0; padding: 0; background: transparent; } +.segmented.compact { width: 100%; margin: 0; } +.segmented .btn { color: var(--muted); border-color: var(--border); background: var(--surface); } +.segmented .btn:is([aria-pressed="true"], [aria-selected="true"]) { border: 2px solid var(--brand); color: var(--on-brand-container); background: var(--brand-container); box-shadow: inset 0 0 0 1px var(--brand-border); } + +.tab-bar { width: 100%; margin-bottom: 24px; } +.tab-bar .btn { position: relative; padding: 12px 8px; } +/* Positioned siblings paint in order, so lift a focused tab or the selected + neighbour overpaints one edge of its focus ring. */ +.tab-bar .btn:focus-visible { z-index: 1; } +.tab-panel:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; } +/* Out of flow so a marker never wraps the label and grows the tab row. The + transparent outline is invisible normally and becomes a visible ring under + forced colours, where the background alone would be flattened away. */ +.tab-attention { position: absolute; top: 5px; right: 6px; width: 8px; height: 8px; border-radius: 50%; outline: 1px solid transparent; background: var(--status-error); } + +/* --- Colour pickers --------------------------------------------------------- */ +.color-options, .scanner-colours { display: flex; flex-wrap: wrap; gap: 12px; } +.color-options label, .scanner-colours label { position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-width: 64px; min-height: 64px; border-radius: var(--radius-control); padding: 12px 16px; font-size: 18px; cursor: pointer; } +.color-options input { position: absolute; width: 1px; height: 1px; opacity: 0; } +.scanner-colours input { accent-color: var(--brand); } +.color-swatch { width: 22px; height: 22px; border: 1px solid rgba(20, 30, 25, 0.26); border-radius: 50%; } +.color-options input:checked + .color-swatch { outline: 2px solid var(--brand); outline-offset: 3px; } +.color-options input:focus-visible + .color-swatch { outline: 3px solid var(--focus); outline-offset: 3px; } +.color-swatch.red { background: #d32f2f; } +.color-swatch.green { background: #84ff91; } +.color-swatch.blue { background: #64a6ff; } +.color-swatch.yellow { background: #ffd166; } +.color-swatch.white { background: #ffffff; } + +/* --- Switches ---------------------------------------------------------------- + A summary row per switch, expanding to an editor. Switch-access users step + through every control, so the collapsed row exposes only Edit and Remove. */ +.switch-status { margin-top: 0; } +.switch-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 0; } +.switch-empty { padding: 36px 24px; } +.switch-empty h3 { margin-top: 12px; color: var(--text); } +.switch-list { display: grid; gap: 16px; } +.switch-row { border: 1px solid var(--border); border-radius: var(--radius-card); background: var(--surface); } +.switch-row[data-open] { border-color: var(--brand-border); } +.switch-row-summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 12px; min-height: 60px; padding: 16px; } +.switch-row-summary > svg { color: var(--muted); } +.switch-row-summary h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.switch-row-summary p { margin-top: 3px; color: var(--muted); white-space: normal; overflow-wrap: anywhere; } +.switch-row-summary > .key-badge { grid-column: 2; justify-self: start; font-size: 16px; } +.switch-row-summary > .btn { grid-row: 1; } +.switch-row-summary > .btn:first-of-type { grid-column: 3; } +.switch-row-summary > .btn:last-of-type { grid-column: 4; } +.key-badge { display: inline-flex; align-items: center; min-height: 26px; border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 5px; padding: 0 9px; background: var(--surface-muted); font: 600 16px/1 ui-monospace, Consolas, monospace; white-space: nowrap; } +.key-badge[data-unavailable] { border-color: var(--status-error); color: var(--status-error); background: var(--status-error-soft); } +.key-badge-empty { border-style: dashed; color: var(--muted); font-family: inherit; font-weight: 500; } +.switch-editor { display: grid; gap: 16px; min-width: 0; margin: 0; border: 0; border-top: 1px solid var(--border); padding: 16px; } +.switch-editor .field { margin-top: 0; } +.switch-editor .field > span:first-child { font-weight: 600; } +.key-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } +.hold-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; } +.hold-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr) repeat(3, 64px); align-items: center; gap: 12px; } +.hold-step { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } +.hold-list select { margin-right: 6px; } +.hold-actions .setting-note { margin-top: 2px; } +.hold-actions .disclosure { margin-top: 0; } +.switch-editor-footer { display: flex; flex-wrap: wrap; gap: 12px; padding-top: 4px; } +.switch-editor-footer .danger { margin-left: auto; } +.switch-list-actions, .setup-choices { display: flex; flex-wrap: wrap; gap: 12px; } +.switch-empty .switch-list-actions { margin-top: 16px; } + +.switch-practice-entry { display: grid; gap: 12px; padding: 12px 0; } +.switch-practice-entry > p:first-child { font-size: 16px; } +.switch-practice-entry label { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } +.switch-practice-entry select { width: auto; max-width: 100%; } +.switch-practice-buttons { display: flex; justify-content: flex-end; gap: 12px; } +.switch-practice-blocks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 16px 0; padding: 0; list-style: none; font-size: 16px; } +.switch-practice-blocks li { display: flex; align-items: center; justify-content: center; min-height: 72px; border: 2px solid var(--border); border-radius: 8px; padding: 16px; background: var(--surface); text-align: center; } +.switch-practice-blocks li[data-selected="true"] { border-color: var(--brand); color: var(--on-brand-container); background: var(--segment-selected); font-weight: 700; } + +/* --- Scanning ---------------------------------------------------------------- */ +.scanner-settings { max-width: 920px; } +.scanner-status { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 20px; border: 1px solid var(--border); border-radius: var(--radius-card); padding: 16px 20px; background: var(--surface-muted); } +.scanner-help { margin: 18px 0 28px; color: var(--muted); line-height: 1.6; } +.scanner-help summary { color: var(--text); } +.scanner-help p + p { margin-top: 10px; } +.scanner-panel-heading { margin-bottom: 24px; } +.scanner-panel-heading h2, .scanner-areas h2 { margin-bottom: 8px; } +.scanner-panel-heading > p, .scanner-areas header p { color: var(--muted); line-height: 1.5; } +.scanner-panel-heading > .btn { margin-bottom: 16px; padding-left: 0; } +.scanner-preferences .setting-note { line-height: 1.5; } +.scanner-preferences .scanner-preference { min-width: 0; margin: 0; } +.scanner-preference + .scanner-preference { margin-top: 20px; } +.scanner-preferences legend { margin-bottom: 10px; padding: 0; color: var(--text); } +.scanner-inheritance { display: flex; align-items: center; justify-content: space-between; min-height: 36px; margin-bottom: 8px; color: var(--muted); font-size: 16px; } +.scanner-inheritance > span, .scanner-area-badge { border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; background: var(--surface-muted); } +.scanner-inheritance .custom { color: var(--text); font-weight: 600; } +.scanner-preferences .exact-speed { margin: 0; color: var(--text); } +.scanner-areas { margin-top: 24px; } +.scanner-preferences .scanner-areas { margin: 0 0 32px; } +.scanner-area-cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 20px; } +.btn.scanner-area-card { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; min-width: 0; border-color: var(--border); border-radius: var(--radius-card); padding: 20px; color: var(--text); background: var(--surface); text-align: left; } +.btn.scanner-area-card:hover { background: var(--surface-hover); } +.scanner-area-card strong { display: flex; justify-content: space-between; gap: 10px; font-size: 18px; } +.scanner-area-card > span { color: var(--muted); font-size: 16px; font-weight: 400; line-height: 1.5; } +.scanner-area-appearance { display: flex; align-items: center; gap: 8px; text-transform: capitalize; } +.scanner-area-badge { align-self: flex-start; margin-top: auto; } +.scanner-save-note { margin-top: 24px; } +.scanner-sample { --scanner-colour: #64a6ff; height: 100px; margin-top: 16px; border-radius: 8px; padding: 16px; background: #1e232e; } +.scanner-sample.red { --scanner-colour: #d32f2f; } +.scanner-sample.green { --scanner-colour: #84ff91; } +.scanner-sample.yellow { --scanner-colour: #ffd166; } +.scanner-sample.white { --scanner-colour: #ffffff; } +.scanner-sample-selection { display: flex; align-items: center; justify-content: center; height: 100%; border: 3px solid var(--scanner-colour); color: white; background: color-mix(in srgb, var(--scanner-colour) 25%, transparent); } +.scanner-sample-selection.thin { border-width: 1px; } +.scanner-sample-selection.standard { border-width: 2px; } +.scanner-sample-selection.thick { border-width: 4px; } +.scan-rate-stepper { display: flex; align-items: center; gap: 12px; } +.scan-rate-stepper output { min-width: 80px; font-size: 18px; font-variant-numeric: tabular-nums; text-align: center; } + +/* --- Mobile ------------------------------------------------------------------- */ +.mobile-download { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 20px; } +.mobile-download figure { max-width: 210px; margin: 0; } +.mobile-download figcaption { margin-top: 8px; color: var(--muted); } +.mobile-download img { width: 190px; height: 190px; border: 1px solid var(--border); border-radius: 9px; padding: 10px; background: white; image-rendering: crisp-edges; } +.mobile-download a { width: max-content; margin-top: 22px; text-decoration: none; } +.mobile-download a.secondary { border: 1px solid var(--border-strong); padding: 12px 20px; color: var(--text); background: var(--surface); font-weight: 600; } +.mobile-download a.secondary:hover { background: var(--surface-hover); } +.mobile-pairing { margin-top: 28px; } +.mobile-pairing ol { padding-left: 24px; color: var(--muted); line-height: 1.6; } + +/* --- Dialogs ------------------------------------------------------------------ */ +.modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 12px; background: rgba(28, 27, 31, 0.48); } +.pairing-dialog, .profile-dialog, .switch-practice-dialog { width: min(100%, 900px); max-height: calc(100dvh - 24px); overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 20px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } +.switch-practice-dialog { border-radius: 12px; } +.confirm-dialog { width: min(100%, 480px); } +.dialog-error { margin-top: 16px; border-left: 3px solid var(--status-error); padding: 9px 11px; color: var(--status-error); background: var(--status-error-soft); } + +.setup-dialog { display: flex; flex-direction: column; width: min(100%, 920px); height: min(900px, calc(100dvh - 24px)); min-height: 0; max-height: calc(100dvh - 24px); overflow: hidden; border: 1px solid var(--border); border-radius: 10px; padding: 20px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } +.setup-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } +.setup-dialog > header, .setup-dialog > footer { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } +.setup-dialog > header { justify-content: space-between; } +.setup-dialog > header span { color: var(--brand); font-size: 16px; font-weight: 700; text-transform: uppercase; } +.setup-dialog > header h2 { margin-top: 4px; font-size: 24px; line-height: 1.33; } +.setup-dialog > header strong { color: var(--muted); font-size: 16px; } +.setup-progress { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-top: 18px; } +.setup-progress span { height: 4px; border-radius: 2px; background: var(--border); } +.setup-progress span[data-active="true"] { background: var(--brand); } +.setup-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 4px; } +.setup-content p { margin-top: 8px; color: var(--muted); } +.setup-statuses { border-top: 1px solid var(--border); } +.setup-statuses article { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 92px; border-bottom: 1px solid var(--border); padding: 14px 4px; } +.setup-choices { margin-top: 22px; } +.setup-choices .btn[aria-pressed="true"] { border-color: var(--brand); color: var(--on-brand-container); background: var(--segment-selected); box-shadow: inset 0 0 0 1px var(--brand-border); } +.setup-privacy { margin-top: 18px; color: var(--brand-label); } +.setup-navigation-guidance { flex: 0 0 auto; margin: 0; color: var(--muted); line-height: 1.5; } +.setup-navigation-guidance:not(:empty) { padding-bottom: 16px; } +.setup-dialog > footer { gap: 12px; border-top: 1px solid var(--border); padding-top: 12px; } +.setup-dialog > footer > span { flex: 1; } +.setup-backdrop[hidden] { display: none; } + +.pairing-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } +.pairing-dialog > header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px; } +.pairing-dialog > header > svg { margin-top: 2px; color: var(--brand); } +.pairing-dialog > header > span { display: grid; place-items: center; min-width: 28px; height: 28px; border-radius: 14px; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } +.pairing-dialog p { margin-top: 4px; color: var(--muted); line-height: 1.4; } +.pairing-dialog .pairing-connection { color: var(--text); } +.pairing-list { margin-top: 20px; border-top: 1px solid var(--border); } +.pairing-list article { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; min-height: 88px; border-bottom: 1px solid var(--border); padding: 12px 0; } +.pairing-list output { color: var(--text); font: 700 25px/1 ui-monospace, monospace; letter-spacing: 0.12em; white-space: nowrap; } +.pairing-actions { display: flex; flex-wrap: wrap; gap: 12px; } + +.profile-dialog > header, .profile-dialog > footer { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } +.profile-dialog > header { justify-content: space-between; } +.profile-dialog header p { margin-top: 4px; color: var(--muted); } +.profile-dialog > footer { margin-top: 20px; } +.profile-dialog > footer > span { flex: 1; } +.binding-list { margin-top: 18px; border-top: 1px solid var(--border); } +.binding-row { display: grid; grid-template-columns: 74px 140px minmax(160px, 1fr); align-items: center; gap: 12px; min-height: 54px; border-bottom: 1px solid var(--border); } +.binding-error { grid-column: 3; margin-top: -8px; padding-bottom: 7px; } + +.capture-dialog { display: grid; justify-items: center; width: min(100%, 420px); border: 1px solid var(--brand-border); border-radius: 10px; padding: 32px 28px 24px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); text-align: center; animation: capture-pulse 1.4s ease-in-out infinite; } +.capture-dialog:focus { outline: none; } +.capture-dialog > svg { color: var(--brand); } +.capture-dialog h2 { margin-top: 14px; } +.capture-dialog p { margin-top: 8px; color: var(--muted); } +.capture-dialog .btn { margin-top: 20px; } +@keyframes capture-pulse { 50% { border-color: var(--brand); box-shadow: var(--shadow-dialog), 0 0 0 4px var(--brand-container); } } +@media (prefers-reduced-motion: reduce) { .capture-dialog, .btn { animation: none; transition: none; } } + +/* --- Responsive --------------------------------------------------------------- */ +@media (max-width: 800px) { + .app-shell { grid-template-columns: 1fr; } + .app-shell > aside { position: static; height: auto; padding: 12px; } + .app-shell > aside .brand, .app-shell .sidebar-footer { display: none; } + .app-shell nav { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .btn.nav-button { flex-direction: column; justify-content: center; text-align: center; } + .view { padding: 16px; } + .switch-row-summary { grid-template-columns: auto minmax(0, 1fr); } + .switch-row-summary > .btn:first-of-type, .switch-row-summary > .btn:last-of-type { width: 100%; grid-row: auto; grid-column: auto; } + .hold-list li { grid-template-columns: repeat(3, minmax(64px, 1fr)); } + .hold-list li > select { grid-column: 2 / -1; } + .mobile-download { grid-template-columns: 1fr; } + .mobile-download figure { justify-self: start; } + .btn.profile-row { grid-template-columns: auto minmax(0, 1fr) auto; } + .profile-row > span:not(:last-child) { display: none; } +} +@media (max-width: 680px) { + .binding-row { grid-template-columns: 72px 1fr; padding: 9px 0; } + .binding-row input { grid-column: 1 / -1; } + .binding-error { grid-column: 1 / -1; margin-top: 0; } + .profile-dialog > footer > span { flex-basis: 100%; height: 0; } + .update-banner { grid-template-columns: auto 1fr; } + .update-banner .btn.text-button { grid-column: 2; justify-self: start; padding-left: 0; } + .setup-statuses article { grid-template-columns: auto 1fr; } + .setup-statuses article > .btn { grid-column: 1 / -1; } + .mobile-download img { width: 150px; height: 150px; } +} +@media (max-width: 480px) { + .app-shell nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .view { padding: 12px; } + .connection-band { grid-template-columns: minmax(0, 1fr); } + .btn.profile-row { display: flex; flex-wrap: wrap; } +} +@media (max-width: 520px) { + .scanner-preferences .exact-speed { flex-direction: column; align-items: stretch; } + .scanner-preferences .segmented { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 850px) { + .btn.scanner-area-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; padding: 16px; } + .scanner-area-card strong { grid-column: 1 / -1; margin-bottom: 4px; } + .scanner-area-badge { grid-column: 2; grid-row: 2 / 4; align-self: center; margin: 0; } + .scanner-area-card > span:not(.scanner-area-badge) { grid-column: 1; } +} +@media (max-height: 600px) { + .setup-dialog { overflow-y: auto; } + .setup-dialog > * { flex-shrink: 0; } + .setup-dialog .setup-content { overflow: visible; } +} + } diff --git a/src/ui/controls.tsx b/src/ui/controls.tsx index 95f9c679..b19b6848 100644 --- a/src/ui/controls.tsx +++ b/src/ui/controls.tsx @@ -1,15 +1,17 @@ import { useEffect, useId, useRef, useState, type ComponentProps, type ReactNode } from "react"; -const target = "min-h-16 min-w-16 rounded-xl px-5 py-3 text-lg font-semibold leading-snug inline-flex items-center justify-center gap-3"; +// Shape and size live in the stylesheet (.btn, .field-control); className +// picks the variant, so a variant can change anything the base sets. +const classes = (...names: (string | false)[]) => names.filter(Boolean).join(" ") || undefined; export function Button({ className = "", type = "button", ...props }: ComponentProps<"button">) { - return + ; } From 873aebbebfe6ba06b59c354bba65dd36dff8779c Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:47:17 +0100 Subject: [PATCH 2/2] Dim disabled fields once inside disabled groups A disabled fieldset already dims its whole group, so fields inside it keep their normal colours rather than also turning grey, matching how buttons are treated. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/ui/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/app.css b/src/ui/app.css index 7f607137..1858e7cc 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -163,7 +163,7 @@ h1, h2, h3, p { margin: 0; letter-spacing: 0; } } .field-control[type="range"] { border: 0; padding: 12px 0; background: transparent; accent-color: var(--brand); cursor: pointer; } .field-control[aria-invalid="true"] { border-color: var(--status-error); } -.field-control:disabled { color: var(--ink-disabled); background: var(--surface-disabled); } +.field-control:disabled:not(fieldset:disabled *) { color: var(--ink-disabled); background: var(--surface-disabled); } .field-control[type="range"]:disabled { background: transparent; } .field { display: grid; gap: 6px; min-width: 0; margin-top: 20px; color: var(--muted); } .field-error { color: var(--status-error); line-height: 1.35; }