/* ========================================================================== MacroPad PWA — "Tactile Deck" A macro deck that reads like a slab of physical keys: warm near-black surfaces, an electric-violet signature accent, softly-raised cards with a hairline top highlight, and a satisfying pressed state. Self-contained (no external fonts/CDNs — CSP forbids them). ========================================================================== */ /* ---- Design tokens -------------------------------------------------------- Everything downstream references these. Dark is the default; the prefers-color-scheme: light block below overrides only the surface/text/ border tokens so both themes stay deliberate and pass WCAG AA. */ :root { color-scheme: dark; /* Surfaces — warm near-black, not a flat grey */ --surface: #15161a; /* app background */ --surface-raised: #1f2027; /* cards, header, sheets */ --surface-sunken: #101116; /* inset wells: inputs, command list */ --border: #2c2e38; /* hairline dividers */ --border-strong: #3a3d4a; /* Text — muted meets 4.5:1 on --surface */ --text: #f3f4f8; --text-muted: #a9adbd; /* Signature accent — electric violet */ --accent: #7c5cff; --accent-hover: #8f73ff; --accent-2: #ffb057; /* warm amber companion (wake glow, highlights) */ --accent-contrast: #ffffff; --accent-soft: rgba(124, 92, 255, 0.16); /* Status */ --danger: #ff5d6c; --success: #3ddc84; --warning: #ffc861; /* Spacing scale — 4px base */ --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 24px; --s-6: 32px; /* Radii */ --r-sm: 8px; --r-md: 12px; --r-lg: 18px; --r-card: 14px; --r-pill: 999px; /* Elevation — soft, warm-tinted shadows */ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.35); --shadow-2: 0 6px 16px rgba(0, 0, 0, 0.40); --shadow-3: 0 18px 48px rgba(0, 0, 0, 0.55); /* Hairline top highlight that makes surfaces read as physical keys */ --key-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06); /* Type scale */ --fs-xs: 0.75rem; --fs-sm: 0.85rem; --fs-base: 1rem; --fs-lg: 1.2rem; --fs-xl: 1.6rem; --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace; --tap: 44px; /* minimum comfortable touch target */ --dur: 0.18s; --ease: cubic-bezier(0.2, 0.7, 0.2, 1); } /* ---- Light theme — clean, deliberate, AA-compliant --------------------- */ @media (prefers-color-scheme: light) { :root { color-scheme: light; --surface: #eceef4; --surface-raised: #ffffff; --surface-sunken: #e3e6ef; --border: #d7dbe6; --border-strong: #c3c8d6; --text: #191b22; --text-muted: #5a5f70; /* 4.5:1+ on white and on --surface */ --accent: #5b3ee6; /* darkened so white text clears AA */ --accent-hover: #4d31d6; --accent-2: #c9720a; --accent-soft: rgba(91, 62, 230, 0.12); --danger: #d13342; --success: #17924f; --warning: #a5730a; --shadow-1: 0 1px 2px rgba(20, 22, 40, 0.10); --shadow-2: 0 6px 16px rgba(20, 22, 40, 0.12); --shadow-3: 0 18px 48px rgba(20, 22, 40, 0.20); --key-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.9); } } /* ---- Reset ------------------------------------------------------------- */ * { box-sizing: border-box; margin: 0; padding: 0; } /* [hidden] must always win over any display rule (keeps the modal closed). */ [hidden] { display: none !important; } html { -webkit-text-size-adjust: 100%; } body { font-family: var(--font-ui); background-color: var(--surface); color: var(--text); font-size: var(--fs-base); line-height: 1.45; min-height: 100vh; min-height: 100dvh; overflow-x: hidden; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; padding-top: env(safe-area-inset-top); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); } /* App-wide keyboard focus ring (the old CSS had none). */ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); } :focus:not(:focus-visible) { outline: none; } /* ---- Header ------------------------------------------------------------ */ .header { background: linear-gradient(180deg, rgba(124, 92, 255, 0.10), transparent 62%), var(--surface-raised); padding: var(--s-4); display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); position: sticky; top: 0; z-index: 100; border-bottom: 1px solid var(--border); box-shadow: var(--shadow-1); } .header h1 { font-size: var(--fs-xl); font-weight: 800; letter-spacing: -0.03em; line-height: 1; background: linear-gradient(105deg, var(--text) 30%, var(--accent)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: var(--accent); /* fallback if clip unsupported */ } .header-actions { display: flex; align-items: center; gap: var(--s-2); } /* Header buttons */ .header-btn { background: var(--accent); color: var(--accent-contrast); border: none; padding: 0 var(--s-4); min-height: var(--tap); border-radius: var(--r-sm); cursor: pointer; font-size: var(--fs-sm); font-weight: 600; box-shadow: var(--shadow-1), var(--key-highlight); transition: background var(--dur) var(--ease), transform var(--dur) var(--ease); } .header-btn:hover { background: var(--accent-hover); } .header-btn:active { transform: translateY(1px); } .header-btn.secondary { background: var(--surface-sunken); color: var(--text); border: 1px solid var(--border-strong); } .header-btn.secondary:hover { border-color: var(--accent); color: var(--text); } /* Square icon controls (fullscreen) — meet 44px tap target */ .header-btn.icon-btn { padding: 0; width: var(--tap); height: var(--tap); display: inline-flex; align-items: center; justify-content: center; font-size: var(--fs-lg); line-height: 1; } /* ---- Connection status ------------------------------------------------- */ .connection-status { display: flex; align-items: center; gap: var(--s-2); font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); padding: 0 var(--s-2); white-space: nowrap; } .status-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--danger); box-shadow: 0 0 0 0 rgba(255, 93, 108, 0.5); transition: background var(--dur) var(--ease); } .status-dot.connected { background: var(--success); animation: pulse-ring 2.4s ease-out infinite; } @keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 rgba(61, 220, 132, 0.5); } 70% { box-shadow: 0 0 0 7px rgba(61, 220, 132, 0); } 100% { box-shadow: 0 0 0 0 rgba(61, 220, 132, 0); } } /* ---- Wake-lock toggle -------------------------------------------------- */ .wake-lock-status { display: inline-flex; align-items: center; justify-content: center; width: var(--tap); height: var(--tap); background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r-sm); font-size: var(--fs-base); cursor: pointer; opacity: 0.55; transition: opacity var(--dur) var(--ease), border-color var(--dur) var(--ease); } .wake-lock-status:hover { opacity: 0.85; } .wake-lock-status.active { opacity: 1; border-color: var(--accent-2); } .wake-lock-status .wake-icon { color: var(--accent-2); line-height: 1; } .wake-lock-status.active .wake-icon { animation: wake-glow 2s ease-in-out infinite; } .wake-lock-status.unsupported { opacity: 0.35; cursor: not-allowed; } .wake-lock-status.unsupported .wake-icon { color: var(--text-muted); text-decoration: line-through; } @keyframes wake-glow { 0%, 100% { filter: drop-shadow(0 0 0 rgba(255, 176, 87, 0)); opacity: 1; } 50% { filter: drop-shadow(0 0 6px rgba(255, 176, 87, 0.7)); opacity: 0.8; } } /* ---- Tabs -------------------------------------------------------------- */ .tabs { display: flex; gap: var(--s-2); padding: var(--s-3) var(--s-4); background: var(--surface); overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .tabs::-webkit-scrollbar { display: none; } .tab { background: var(--surface-raised); color: var(--text-muted); border: 1px solid var(--border); padding: 0 var(--s-4); min-height: var(--tap); border-radius: var(--r-pill); cursor: pointer; white-space: nowrap; font-size: var(--fs-sm); font-weight: 600; transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background var(--dur) var(--ease); } .tab:hover { color: var(--text); border-color: var(--border-strong); } .tab.active { background: var(--accent-soft); color: var(--text); border-color: var(--accent); } /* ---- Macro grid -------------------------------------------------------- */ .macro-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: var(--s-4); padding: var(--s-4); padding-bottom: calc(var(--s-6) + env(safe-area-inset-bottom)); } /* Cards read as physical keys: raised slab, hairline top highlight, satisfying press. */ .macro-card { position: relative; background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r-card); padding: var(--s-4) var(--s-3) var(--s-3); display: flex; flex-direction: column; align-items: center; gap: var(--s-3); cursor: pointer; min-height: 148px; color: var(--text); box-shadow: var(--shadow-2), var(--key-highlight); transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease); } /* Faint wash of the macro's own color across the top of the key */ .macro-card::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(180deg, color-mix(in srgb, var(--macro-color, var(--accent)) 22%, transparent), transparent 45%); opacity: 0.55; pointer-events: none; } .macro-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-3), var(--key-highlight); border-color: var(--border-strong); } .macro-card:active { transform: translateY(1px) scale(0.985); box-shadow: var(--shadow-1), var(--key-highlight); } .macro-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } /* Pressed / executing — accent ring + brief success flash */ .macro-card.executing { animation: card-press 0.32s var(--ease); border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-2); } @keyframes card-press { 0% { transform: scale(1); } 35% { transform: scale(0.94); } 55% { box-shadow: 0 0 0 4px rgba(61, 220, 132, 0.35), var(--shadow-2); } 100% { transform: scale(1); } } .macro-image { width: 68px; height: 68px; object-fit: contain; border-radius: var(--r-md); position: relative; } /* Placeholder background = the per-card --macro-color set by app.js */ .macro-image-placeholder { position: relative; width: 68px; height: 68px; background: var(--macro-color, var(--accent)); border-radius: var(--r-md); display: flex; align-items: center; justify-content: center; font-size: 1.9rem; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255, 255, 255, 0.25); } .macro-name { position: relative; text-align: center; font-size: var(--fs-sm); font-weight: 600; line-height: 1.3; color: var(--text); /* 2-line clamp with ellipsis — no mid-word breaks */ display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: break-word; } /* Edit affordance — always faintly visible; solid on hover/focus */ .macro-edit-btn { position: absolute; top: var(--s-2); right: var(--s-2); z-index: 1; width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--surface-sunken) 82%, transparent); color: var(--text-muted); border: 1px solid var(--border); border-radius: 50%; cursor: pointer; font-size: var(--fs-sm); opacity: 0; transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease); } .macro-card:hover .macro-edit-btn, .macro-card:focus-within .macro-edit-btn { opacity: 1; } .macro-edit-btn:hover { color: var(--text); border-color: var(--accent); } .macro-edit-btn:focus-visible { opacity: 1; } /* ---- Floating action button -------------------------------------------- */ .fab { position: fixed; right: calc(var(--s-5) + env(safe-area-inset-right)); bottom: calc(var(--s-5) + env(safe-area-inset-bottom)); z-index: 150; width: 60px; height: 60px; border: none; border-radius: 50%; background: var(--accent); color: var(--accent-contrast); font-size: 2rem; font-weight: 300; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 8px 24px rgba(124, 92, 255, 0.45), var(--key-highlight); transition: transform var(--dur) var(--ease), background var(--dur) var(--ease), box-shadow var(--dur) var(--ease); } .fab:hover { background: var(--accent-hover); transform: translateY(-2px) scale(1.04); box-shadow: 0 12px 30px rgba(124, 92, 255, 0.55), var(--key-highlight); } .fab:active { transform: translateY(0) scale(0.96); } /* ---- Modal / editor sheet ---------------------------------------------- */ /* .modal is the fixed scrim; .modal-content is the sheet. */ .modal { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; padding: var(--s-4); background: rgba(8, 9, 14, 0.6); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); animation: fade-in var(--dur) var(--ease); } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } .modal-content { background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r-lg); width: 100%; max-width: 520px; max-height: min(90vh, 780px); display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-3), var(--key-highlight); animation: sheet-rise var(--dur) var(--ease); } @keyframes sheet-rise { from { transform: translateY(12px) scale(0.98); opacity: 0; } to { transform: none; opacity: 1; } } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--border); background: var(--surface-raised); position: sticky; top: 0; } .modal-header h2 { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.01em; } .modal-close { width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; background: none; border: none; color: var(--text-muted); font-size: 1.6rem; line-height: 1; cursor: pointer; border-radius: 50%; transition: background var(--dur) var(--ease), color var(--dur) var(--ease); } .modal-close:hover { background: var(--surface-sunken); color: var(--text); } .modal-body { padding: var(--s-5); overflow-y: auto; flex: 1; } .modal-footer { display: flex; justify-content: flex-end; gap: var(--s-3); padding: var(--s-4) var(--s-5); border-top: 1px solid var(--border); background: var(--surface-raised); position: sticky; bottom: 0; } /* Push Delete to the far left when present */ .modal-footer .btn-danger { margin-right: auto; } /* ---- Form elements ----------------------------------------------------- */ .form-group { margin-bottom: var(--s-5); } .form-group:last-child { margin-bottom: 0; } .form-group label { display: block; margin-bottom: var(--s-2); font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted); } .form-group input, .form-group select { width: 100%; padding: var(--s-3); min-height: var(--tap); background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--r-sm); color: var(--text); font-family: inherit; font-size: var(--fs-base); transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); } .form-group input::placeholder { color: var(--text-muted); opacity: 0.8; } .form-group input:focus, .form-group select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } /* ---- Command builder --------------------------------------------------- */ .command-list { background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-3); min-height: 96px; display: flex; flex-direction: column; gap: var(--s-2); } .command-item { background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--s-2); display: flex; align-items: center; gap: var(--s-2); } .command-type-select { flex: 0 0 auto; min-height: 40px; padding: 0 var(--s-2); background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--r-sm); color: var(--text); font-family: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; } .command-type-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .command-value { flex: 1 1 auto; min-width: 0; min-height: 40px; padding: 0 var(--s-3); background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--r-sm); color: var(--text); font-family: var(--font-mono); font-size: var(--fs-sm); } .command-value:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .command-value::placeholder { color: var(--text-muted); opacity: 0.7; } .command-actions { display: flex; gap: var(--s-1); flex: 0 0 auto; } .command-actions button { width: 34px; height: 40px; display: inline-flex; align-items: center; justify-content: center; background: var(--surface-sunken); border: 1px solid var(--border); color: var(--text-muted); border-radius: var(--r-sm); cursor: pointer; font-size: var(--fs-sm); transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease); } .command-actions button:hover { color: var(--text); border-color: var(--border-strong); } .command-actions button.delete:hover { color: var(--danger); border-color: var(--danger); } .add-command-btns { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-3); } .add-command-btn { background: transparent; border: 1px dashed var(--border-strong); color: var(--text-muted); padding: var(--s-2) var(--s-3); min-height: 40px; border-radius: var(--r-sm); cursor: pointer; font-size: var(--fs-sm); font-weight: 600; transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease), background var(--dur) var(--ease); } .add-command-btn:hover { color: var(--accent); border-color: var(--accent); border-style: solid; background: var(--accent-soft); } /* ---- Buttons ----------------------------------------------------------- */ .btn { padding: 0 var(--s-5); min-height: var(--tap); border: 1px solid transparent; border-radius: var(--r-sm); cursor: pointer; font-family: inherit; font-size: var(--fs-sm); font-weight: 600; box-shadow: var(--key-highlight); transition: background var(--dur) var(--ease), transform var(--dur) var(--ease), border-color var(--dur) var(--ease); } .btn:active { transform: translateY(1px); } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: var(--accent); color: var(--accent-contrast); } .btn-primary:hover { background: var(--accent-hover); } .btn-secondary { background: var(--surface-sunken); color: var(--text); border-color: var(--border-strong); } .btn-secondary:hover { border-color: var(--accent); } .btn-danger { background: var(--danger); color: #fff; } .btn-danger:hover { filter: brightness(1.08); } /* ---- Toasts ------------------------------------------------------------ */ .toast-container { position: fixed; bottom: calc(var(--s-4) + env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%); z-index: 300; display: flex; flex-direction: column; align-items: center; gap: var(--s-2); width: max-content; max-width: calc(100vw - 2 * var(--s-4)); pointer-events: none; } .toast { pointer-events: auto; background: var(--surface-raised); color: var(--text); padding: var(--s-3) var(--s-4); border: 1px solid var(--border); border-left-width: 4px; border-radius: var(--r-sm); font-size: var(--fs-sm); font-weight: 500; box-shadow: var(--shadow-2); animation: toast-in var(--dur) var(--ease); } .toast.success { border-left-color: var(--success); } .toast.error { border-left-color: var(--danger); } @keyframes toast-in { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } } /* ---- Empty state ------------------------------------------------------- */ .empty-state { grid-column: 1 / -1; text-align: center; padding: var(--s-6) var(--s-4); display: flex; flex-direction: column; align-items: center; gap: var(--s-3); } /* Large signature glyph */ .empty-state::before { content: "⌘"; font-size: 4rem; line-height: 1; color: var(--accent); opacity: 0.85; margin-bottom: var(--s-2); } .empty-state p { font-size: var(--fs-lg); font-weight: 700; color: var(--text); } .empty-state p.hint { font-size: var(--fs-sm); font-weight: 400; color: var(--text-muted); max-width: 32ch; } .empty-state .btn { margin-top: var(--s-3); } /* ---- Loading ----------------------------------------------------------- */ .loading { grid-column: 1 / -1; display: flex; justify-content: center; padding: var(--s-6); } .spinner { width: 42px; height: 42px; border: 3px solid var(--border-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* ---- Install banner ---------------------------------------------------- */ .install-banner { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-4); background: var(--accent-soft); color: var(--text); border-bottom: 1px solid var(--accent); font-size: var(--fs-sm); font-weight: 500; } .install-banner > div { display: flex; gap: var(--s-2); align-items: center; } .install-banner button { background: var(--accent); color: var(--accent-contrast); border: none; padding: var(--s-2) var(--s-4); min-height: 40px; border-radius: var(--r-sm); cursor: pointer; font-weight: 600; } .install-banner button:hover { background: var(--accent-hover); } .install-banner .dismiss { background: transparent; color: var(--text-muted); padding: var(--s-2); min-width: 40px; } .install-banner .dismiss:hover { color: var(--text); } /* ---- Fullscreen tweaks ------------------------------------------------- */ :fullscreen .header { padding-top: var(--s-2); padding-bottom: var(--s-2); } :fullscreen .macro-grid { padding-bottom: var(--s-4); } /* ---- Mobile ------------------------------------------------------------ */ @media (max-width: 480px) { .header { padding: var(--s-3); } .header h1 { font-size: var(--fs-lg); } /* Keep the status text from crowding; dot + label still fit */ .connection-status { font-size: 0.7rem; padding: 0; } .macro-grid { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: var(--s-3); padding: var(--s-3); } .macro-card { min-height: 128px; padding: var(--s-3) var(--s-2) var(--s-2); } .macro-image, .macro-image-placeholder { width: 56px; height: 56px; font-size: 1.6rem; } .modal { padding: var(--s-2); align-items: flex-end; } .modal-content { max-width: 100%; max-height: 92vh; } .modal-body { padding: var(--s-4); } .modal-header, .modal-footer { padding: var(--s-4); } .fab { right: calc(var(--s-4) + env(safe-area-inset-right)); bottom: calc(var(--s-4) + env(safe-area-inset-bottom)); } } /* ---- Reduced motion ---------------------------------------------------- */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } .macro-card:hover { transform: none; } }