Files
Discord-Authenticator/crates/webui/src/app.css
T
Miles WardandClaude Opus 4.8 7751ebe9b3 v0.1.2: working Edit sheet, centred search, brand assets
Fixes a bug that shipped in v0.1: the per-account Edit sheet never opened at
all. Its Cancel button used `onclick: dlg.close`, evaluated while building the
arguments to openModal() and therefore before `const dlg` was initialised — a
temporal-dead-zone ReferenceError that aborted the whole handler. Every other
button in that sheet was already a closure, which is why it went unnoticed:
browser testing had covered add, import and settings, but never opened this
sheet.

Renames now reach Discord almost immediately. The push loop skips any account
whose current code it has already sent, so a rename previously stayed invisible
until the code next rolled — up to a full period. update() now clears the
last-sent code when the naming actually changes, so the next tick treats the
tile as stale and rewrites it in about a second. Toggling push alone is not a
rename and still costs nothing.

- webui: pencil icon and "Edit account" title so the row action reads as edit
  rather than settings; both fields labelled, with the second one explained as
  the username/email and left untouched unless changed
- webui: topbar is a three-column grid so the search sits on the same centre
  line as the account tiles. Below 820px there isn't room for brand + 420px
  search + actions, so it falls back to a row rather than clipping the
  wordmark; the brand now reads "Discord Authenticator" in full
- webui: the add-account FAB pins to the viewport corner instead of tracking
  the content column edge
- brand: docs/brand/icon.svg plus PNGs rendered from it by da-render, so the
  README image, Gitea avatar and Unraid template icon all derive from one
  source rather than drifting. Unraid's <Icon> now points at a PNG, which it
  renders more reliably than SVG

126 tests (was 124).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-31 01:42:18 -04:00

784 lines
28 KiB
CSS

/* Discord Authenticator — web UI.
*
* Visual language follows Beautiful UI (https://www.beautifului.dev/):
* OKLCH ramps, hairline-ring shadows, inset fields / raised buttons,
* Inter + JetBrains Mono. Tokens below were taken from that system and
* extended with the few this app needs (code display, countdown ring).
*
* Fully offline: no CDN, no webfonts fetched at runtime. The font stack
* falls back to the platform UI face, which on every OS Miles runs is
* already a well-tuned grotesque.
*/
/* ── Tokens ───────────────────────────────────────────────────────────── */
:root {
color-scheme: light;
--page: oklch(98.5% .001 286.376);
--canvas: oklch(96.1% .002 247.84);
--surface: oklch(100% 0 0);
--inset: oklch(97.9% .002 247.839);
--hover: oklch(97% .002 247.839);
--hover-2: oklch(93.3% .003 247.86);
--ink: oklch(24.7% .006 258.361);
--ink-2: oklch(50.6% .01 264.477);
--ink-3: oklch(69.5% .009 264.505);
--line: oklch(94.6% .003 264.542);
--line-strong: oklch(91.2% .005 258.326);
--line-soft: oklch(96.6% .002 264.542);
--field: oklch(96.1% .001 286.375);
--stripe: oklch(40.5% 0 0/.075);
--stripe-bg: oklch(97% 0 0);
--accent: oklch(62.6% .205 254.947);
--accent-ink: oklch(55.6% .187 255.617);
--accent-tint: oklch(96% .019 252.878);
--on-accent: oklch(100% 0 0);
--green: oklch(60.3% .155 150.883);
--green-tint: oklch(60.3% .155 150.883/.12);
--orange: oklch(68.9% .179 49.902);
--orange-tint: oklch(68.9% .179 49.902/.12);
--red: oklch(62.1% .192 23.042);
--red-tint: oklch(62.1% .192 23.042/.12);
/* Beautiful UI's signature: hairline ring + very soft graduated drop. */
--shadow-card: 0 0 0 1px oklch(94.6% .003 264.542),
0 18px 47px 0 #00000008, 0 7.5px 19px 0 #00000005,
0 4px 10.5px 0 #00000005, 0 2.3px 5.8px 0 #00000003,
0 1.2px 3.1px 0 #00000003, 0 .5px 1.3px 0 #00000003;
--shadow-btn: 0 0 0 1px oklch(91.2% .005 258.326), 0 0 4px 0 #0000000a;
--shadow-raised: 0 0 0 1px oklch(91.2% .005 258.326), 0 2px 10px #0000000f;
--shadow-overlay: 0 0 0 1px oklch(91.2% .005 258.326), 0 16px 48px #00000021;
--shadow-inset-field: inset 0 1px 2px oklch(0% 0 0/.12);
--ring: 0 0 0 3px var(--accent-tint);
--radius: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--topbar-h: 60px;
--sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
system-ui, sans-serif;
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
monospace;
--ease: cubic-bezier(.22, 1, .36, 1);
}
:root[data-theme="dark"] {
color-scheme: dark;
--page: oklch(20.9% .004 264.477);
--canvas: oklch(23.1% .004 264.487);
--surface: oklch(26% .006 271.191);
--inset: oklch(24.3% .004 264.492);
--hover: oklch(28.9% .006 271.22);
--hover-2: oklch(31.8% .007 274.747);
--ink: oklch(96.4% .002 247.839);
--ink-2: oklch(73.1% .008 260.731);
--ink-3: oklch(54.1% .01 264.484);
--line: oklch(30.8% .006 258.354);
--line-strong: oklch(35.6% .007 264.474);
--line-soft: oklch(27.8% .006 258.354);
--field: oklch(29.3% .006 271.223);
--stripe: oklch(100% 0 0/.055);
--stripe-bg: oklch(22.6% .004 264.485);
--accent: oklch(68% .173 253.301);
--accent-ink: oklch(78.8% .113 248.33);
--accent-tint: oklch(68% .173 253.301/.16);
--on-accent: oklch(20% .01 253);
--green: oklch(70.5% .154 153.814);
--green-tint: oklch(70.5% .154 153.814/.14);
--orange: oklch(74.6% .156 55.642);
--orange-tint: oklch(74.6% .156 55.642/.14);
--red: oklch(66.6% .18 21.433);
--red-tint: oklch(66.6% .18 21.433/.14);
--shadow-card: 0 0 0 1px oklch(100% 0 0/.11), 0 1px 2px oklch(0% 0 0/.2),
0 2px 6px oklch(0% 0 0/.2);
--shadow-btn: 0 0 0 1px oklch(100% 0 0/.1), 0 1px 2px oklch(0% 0 0/.3);
--shadow-raised: 0 0 0 1px oklch(100% 0 0/.13), 0 2px 10px oklch(0% 0 0/.22);
--shadow-overlay: 0 0 0 1px oklch(100% 0 0/.15), 0 8px 28px oklch(0% 0 0/.34);
--shadow-inset-field: inset 0 1px 2px oklch(0% 0 0/.4);
}
/* ── Base ─────────────────────────────────────────────────────────────── */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
background: var(--page);
color: var(--ink);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.02em; }
h1 { font-size: 21px; }
h2 { font-size: 17px; }
h3 { font-size: 14px; }
p { margin: 0; }
a { color: var(--accent-ink); text-decoration: none; }
a:hover { text-decoration: underline; }
::selection { background: var(--accent-tint); }
:focus-visible {
outline: none;
box-shadow: var(--ring), var(--shadow-btn);
}
/* Diagonal-stripe backdrop — the Beautiful UI panel treatment. Used
behind the pre-auth screens so setup/login feel like a product, not
a bare form. */
.striped {
background-color: var(--stripe-bg);
background-image: repeating-linear-gradient(
-45deg,
var(--stripe) 0 1px,
transparent 1px 9px
);
}
/* ── Shell ────────────────────────────────────────────────────────────── */
.shell { display: flex; flex-direction: column; min-height: 100vh; }
/* Three columns: the search occupies the middle one, so it lands on the same
centre line as the account tiles below no matter how wide the brand or the
action cluster get. The outer columns are equal-width and allowed to
shrink, which keeps the centring exact rather than approximate. */
.topbar {
position: sticky; top: 0; z-index: 30;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 420px) minmax(0, 1fr);
align-items: center; gap: 12px;
height: var(--topbar-h);
padding: 0 16px;
background: color-mix(in oklab, var(--page) 82%, transparent);
backdrop-filter: saturate(1.6) blur(12px);
border-bottom: 1px solid var(--line);
}
.topbar .brand { justify-self: start; min-width: 0; }
.topbar .brand strong { overflow: hidden; text-overflow: ellipsis; }
.topbar .actions {
justify-self: end;
display: flex; align-items: center; gap: 2px;
}
/* True centring needs room for all three: the brand wordmark (~210px), the
420px search, the action cluster (~110px), plus gaps and padding — about
800px in total. Below that the grid starts clipping "Discord Authenticator",
so fall back to a plain row where the brand keeps its space and the search
takes whatever is left. */
@media (max-width: 820px) {
.topbar { display: flex; }
.topbar .brand { flex: 0 0 auto; }
.topbar .search { flex: 1 1 auto; justify-self: auto; }
.topbar .actions { margin-left: auto; flex: 0 0 auto; }
}
.brand { display: flex; align-items: center; gap: 9px; flex: none; }
.brand svg { width: 26px; height: 26px; display: block; }
.brand strong {
font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em;
white-space: nowrap;
}
@media (max-width: 560px) { .brand strong { display: none; } }
.topbar .spacer { flex: 1; }
.main {
flex: 1;
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 22px 16px 128px;
}
/* ── Search ───────────────────────────────────────────────────────────── */
.search {
position: relative;
width: 100%;
max-width: 420px;
justify-self: center;
}
.search svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
width: 15px; height: 15px; color: var(--ink-3); pointer-events: none;
}
.search input {
width: 100%; height: 34px;
padding: 0 30px 0 31px;
font: inherit; font-size: 13.5px;
color: var(--ink);
background: var(--field);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
box-shadow: var(--shadow-inset-field);
transition: box-shadow .15s var(--ease), border-color .15s var(--ease);
}
.search input::placeholder { color: var(--ink-3); }
.search input:focus {
outline: none;
border-color: var(--accent);
box-shadow: var(--shadow-inset-field), var(--ring);
}
.search .clear {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
display: none; place-items: center;
width: 20px; height: 20px; padding: 0;
border: 0; border-radius: 5px;
background: transparent; color: var(--ink-3); cursor: pointer;
}
.search .clear:hover { background: var(--hover-2); color: var(--ink); }
.search.has-value .clear { display: grid; }
/* ── Buttons ──────────────────────────────────────────────────────────── */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
height: 34px; padding: 0 13px;
font: inherit; font-size: 13.5px; font-weight: 500;
color: var(--ink);
background: var(--surface);
border: 0; border-radius: var(--radius);
box-shadow: var(--shadow-btn);
cursor: pointer;
white-space: nowrap;
transition: background .14s var(--ease), transform .14s var(--ease),
box-shadow .14s var(--ease);
}
.btn:hover { background: var(--hover); }
.btn:active { transform: translateY(0.5px); }
.btn[disabled] { opacity: .5; pointer-events: none; }
.btn svg { width: 15px; height: 15px; flex: none; }
.btn-primary {
background: var(--accent); color: var(--on-accent);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 70%, black),
0 1px 2px #00000026;
}
.btn-primary:hover { background: var(--accent-ink); }
.btn-danger { color: var(--red); }
.btn-danger:hover { background: var(--red-tint); }
.btn-ghost { background: transparent; box-shadow: none; color: var(--ink-2); }
.btn-ghost:hover { background: var(--hover-2); color: var(--ink); }
.btn-lg { height: 40px; padding: 0 18px; font-size: 14px; }
.btn-block { width: 100%; }
.icon-btn {
display: grid; place-items: center;
width: 32px; height: 32px; padding: 0;
border: 0; border-radius: var(--radius);
background: transparent; color: var(--ink-2);
cursor: pointer;
transition: background .14s var(--ease), color .14s var(--ease);
}
.icon-btn:hover { background: var(--hover-2); color: var(--ink); }
.icon-btn svg { width: 17px; height: 17px; }
/* Theme toggle shows one glyph per theme. */
.theme-toggle .t-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .t-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .t-moon { display: block; }
/* ── Cards & panels ───────────────────────────────────────────────────── */
.card {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.card + .card { margin-top: 14px; }
.card-head {
display: flex; align-items: center; gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.card-head h2 { flex: 1; }
.card-body { padding: 16px; }
.card-sub { font-size: 12.5px; color: var(--ink-2); margin-top: 3px; }
/* ── Forms ────────────────────────────────────────────────────────────── */
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field label {
display: block; margin-bottom: 6px;
font-size: 12.5px; font-weight: 500; color: var(--ink-2);
}
.field .hint { margin-top: 6px; font-size: 12px; color: var(--ink-3); }
.input, .select, textarea.input {
width: 100%; height: 38px;
padding: 0 11px;
font: inherit; font-size: 13.5px;
color: var(--ink);
background: var(--field);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
box-shadow: var(--shadow-inset-field);
transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
textarea.input { height: auto; min-height: 76px; padding: 9px 11px;
resize: vertical; font-family: var(--mono); font-size: 12.5px;
line-height: 1.6; }
.input::placeholder { color: var(--ink-3); }
.input:focus, .select:focus, textarea.input:focus {
outline: none; border-color: var(--accent);
box-shadow: var(--shadow-inset-field), var(--ring);
}
.input[aria-invalid="true"] { border-color: var(--red); }
.select { appearance: none; cursor: pointer;
background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
background-position: calc(100% - 15px) 17px, calc(100% - 10px) 17px;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 30px;
}
.row { display: flex; gap: 10px; }
.row > * { flex: 1; min-width: 0; }
/* Switch */
.switch { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch .track {
position: relative; flex: none;
width: 34px; height: 20px;
background: var(--hover-2);
border-radius: 999px;
box-shadow: inset 0 0 0 1px var(--line-strong);
transition: background .18s var(--ease);
}
.switch .track::after {
content: ""; position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px;
background: var(--surface); border-radius: 50%;
box-shadow: 0 1px 2px #00000033;
transition: transform .18s var(--ease);
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track::after { transform: translateX(14px); }
.switch input:focus-visible + .track { box-shadow: inset 0 0 0 1px var(--line-strong), var(--ring); }
.switch .switch-label { font-size: 13.5px; }
/* ── Account list ─────────────────────────────────────────────────────── */
.accounts { display: flex; flex-direction: column; gap: 10px; }
.acct {
display: grid;
grid-template-columns: 38px 1fr auto;
align-items: center;
gap: 13px;
padding: 13px 15px;
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
transition: background .14s var(--ease);
}
.acct:hover { background: var(--hover); }
.acct .avatar {
display: grid; place-items: center;
width: 38px; height: 38px;
border-radius: 10px;
font-size: 15px; font-weight: 600;
color: var(--on-accent);
background: var(--accent);
box-shadow: inset 0 0 0 1px #0000001a;
user-select: none;
}
.acct .meta { min-width: 0; }
.acct .issuer {
font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.acct .label {
font-size: 12px; color: var(--ink-2);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.acct .right { display: flex; align-items: center; gap: 11px; }
/* The code itself — the thing people came for. Tabular figures so the
digits don't shimmy as they change, and a word-gap in the middle the
way every authenticator app renders it. */
.code {
font-family: var(--mono);
font-size: 25px;
font-weight: 500;
letter-spacing: 0.055em;
font-variant-numeric: tabular-nums;
color: var(--ink);
cursor: pointer;
border: 0; background: none; padding: 2px 4px;
border-radius: 6px;
transition: color .2s var(--ease), background .14s var(--ease);
line-height: 1.1;
}
.code:hover { background: var(--hover-2); }
.code .gap { display: inline-block; width: 0.42em; }
/* Last 5 seconds: the code is about to roll. */
.acct.expiring .code { color: var(--orange); }
/* Countdown ring */
.ring { width: 26px; height: 26px; flex: none; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 2.5; }
.ring .bg { stroke: var(--line-strong); }
.ring .fg {
stroke: var(--accent); stroke-linecap: round;
transition: stroke-dashoffset .95s linear, stroke .3s var(--ease);
}
.acct.expiring .ring .fg { stroke: var(--orange); }
/* Discord push status */
.push-dot {
width: 7px; height: 7px; border-radius: 50%; flex: none;
background: var(--ink-3);
}
.push-dot.live { background: var(--green); box-shadow: 0 0 0 3px var(--green-tint); }
.push-dot.error { background: var(--red); box-shadow: 0 0 0 3px var(--red-tint); }
.push-dot.paused { background: var(--ink-3); }
/* ── Chips ────────────────────────────────────────────────────────────── */
.chip {
display: inline-flex; align-items: center; gap: 5px;
height: 22px; padding: 0 8px;
font-size: 11.5px; font-weight: 500;
color: var(--ink-2);
background: var(--inset);
border-radius: 999px;
box-shadow: inset 0 0 0 1px var(--line);
white-space: nowrap;
}
.chip.ok { color: var(--green); background: var(--green-tint); box-shadow: none; }
.chip.warn { color: var(--orange); background: var(--orange-tint); box-shadow: none; }
.chip.err { color: var(--red); background: var(--red-tint); box-shadow: none; }
/* ── Empty state ──────────────────────────────────────────────────────── */
.empty {
display: flex; flex-direction: column; align-items: center;
gap: 5px;
padding: 56px 24px 60px;
text-align: center;
border-radius: var(--radius-xl);
}
.empty .glyph {
display: grid; place-items: center;
width: 60px; height: 60px; margin-bottom: 14px;
border-radius: 18px;
color: var(--accent);
background: var(--accent-tint);
}
.empty .glyph svg { width: 28px; height: 28px; }
.empty h2 { font-size: 16px; }
.empty p { font-size: 13px; color: var(--ink-2); max-width: 330px; }
.empty .btn { margin-top: 16px; }
/* ── Floating action button ───────────────────────────────────────────── */
.fab {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 25;
display: inline-flex; align-items: center; gap: 8px;
height: 46px; padding: 0 19px;
font: inherit; font-size: 14px; font-weight: 500;
color: var(--on-accent);
background: var(--accent);
border: 0; border-radius: 999px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 70%, black),
0 6px 20px color-mix(in oklab, var(--accent) 40%, transparent),
0 2px 6px #0000001f;
cursor: pointer;
transition: transform .16s var(--ease), background .16s var(--ease);
}
.fab:hover { background: var(--accent-ink); transform: translateY(-1px); }
.fab:active { transform: translateY(0); }
.fab svg { width: 17px; height: 17px; }
/* ── Modal ────────────────────────────────────────────────────────────── */
.scrim {
position: fixed; inset: 0; z-index: 40;
display: grid; place-items: center;
padding: 20px;
background: oklch(0% 0 0 / .45);
backdrop-filter: blur(3px);
animation: fade .16s var(--ease);
}
.modal {
width: 100%; max-width: 460px;
max-height: calc(100vh - 40px);
display: flex; flex-direction: column;
background: var(--surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-overlay);
animation: pop .2s var(--ease);
overflow: hidden;
}
.modal-head {
display: flex; align-items: center; gap: 10px;
padding: 15px 16px;
border-bottom: 1px solid var(--line);
}
.modal-head h2 { flex: 1; }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-foot {
display: flex; justify-content: flex-end; gap: 8px;
padding: 13px 16px;
border-top: 1px solid var(--line);
background: var(--inset);
}
@keyframes fade { from { opacity: 0 } }
@keyframes pop { from { opacity: 0; transform: translateY(8px) scale(.98) } }
/* ── Tabs ─────────────────────────────────────────────────────────────── */
.tabs {
display: flex; gap: 3px;
padding: 3px;
background: var(--inset);
border-radius: 10px;
box-shadow: inset 0 0 0 1px var(--line);
margin-bottom: 16px;
}
.tabs button {
flex: 1;
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: 32px; padding: 0 8px;
font: inherit; font-size: 12.5px; font-weight: 500;
color: var(--ink-2);
background: transparent; border: 0; border-radius: 7px;
cursor: pointer;
transition: background .14s var(--ease), color .14s var(--ease);
}
.tabs button svg { width: 14px; height: 14px; }
.tabs button:hover { color: var(--ink); }
.tabs button.active {
color: var(--ink);
background: var(--surface);
box-shadow: var(--shadow-btn);
}
/* ── Dropzone (QR upload) ─────────────────────────────────────────────── */
.dropzone {
display: flex; flex-direction: column; align-items: center; gap: 4px;
padding: 34px 20px;
text-align: center;
color: var(--ink-2);
background: var(--inset);
border: 1.5px dashed var(--line-strong);
border-radius: var(--radius-lg);
cursor: pointer;
transition: border-color .16s var(--ease), background .16s var(--ease);
}
.dropzone:hover, .dropzone.over {
border-color: var(--accent);
background: var(--accent-tint);
}
.dropzone svg { width: 26px; height: 26px; color: var(--ink-3); margin-bottom: 8px; }
.dropzone.over svg { color: var(--accent); }
.dropzone strong { font-size: 13.5px; color: var(--ink); font-weight: 500; }
.dropzone span { font-size: 12px; }
/* Camera scanner */
.scanner {
position: relative;
aspect-ratio: 1;
width: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
background: oklch(15% 0 0);
box-shadow: inset 0 0 0 1px var(--line);
}
.scanner video { width: 100%; height: 100%; object-fit: cover; display: block; }
.scanner .reticle {
position: absolute; inset: 16%;
border-radius: 14px;
box-shadow: 0 0 0 3px oklch(100% 0 0 / .85),
0 0 0 9999px oklch(0% 0 0 / .45);
}
/* ── Import list (Google export / multi-account) ───────────────────── */
.import-head {
display: flex; align-items: center; justify-content: space-between;
gap: 10px; margin: 14px 0 8px;
}
.btn-sm { height: 26px; padding: 0 9px; font-size: 12px; }
/* Capped so a 20-account export scrolls inside the modal rather than
pushing the Import button off the bottom of the screen. */
.import-list {
max-height: 320px;
overflow-y: auto;
border-radius: var(--radius-lg);
box-shadow: inset 0 0 0 1px var(--line);
background: var(--inset);
}
.import-row {
display: grid;
grid-template-columns: auto 30px 1fr auto;
align-items: center;
gap: 11px;
padding: 9px 12px;
cursor: pointer;
transition: background .12s var(--ease);
}
.import-row + .import-row { border-top: 1px solid var(--line-soft); }
.import-row:hover { background: var(--hover); }
.import-row input[type="checkbox"] {
width: 16px; height: 16px; margin: 0;
accent-color: var(--accent);
cursor: pointer;
flex: none;
}
.import-row .avatar {
width: 30px; height: 30px; border-radius: 8px; font-size: 13px;
}
.import-row .meta { min-width: 0; }
.import-row .issuer { font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.import-row .label { font-size: 11.5px; color: var(--ink-2);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.import-row .tags { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.import-row .tags .chip { height: 19px; padding: 0 7px; font-size: 10.5px; }
/* An account already in the vault is de-emphasised but still selectable. */
.import-row:has(input:not(:checked)) .meta { opacity: .55; }
/* ── Toast ────────────────────────────────────────────────────────────── */
.toasts {
position: fixed; left: 50%; bottom: 22px; z-index: 60;
transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex; align-items: center; gap: 8px;
padding: 9px 14px;
font-size: 13px; font-weight: 500;
color: var(--ink);
background: var(--surface);
border-radius: 999px;
box-shadow: var(--shadow-overlay);
animation: toast-in .22s var(--ease);
}
.toast svg { width: 15px; height: 15px; flex: none; }
.toast.ok svg { color: var(--green); }
.toast.err svg { color: var(--red); }
@keyframes toast-in { from { opacity: 0; transform: translateY(10px) } }
/* ── Auth screens (setup / login) ─────────────────────────────────────── */
.auth-wrap {
min-height: 100vh;
display: grid; place-items: center;
padding: 24px;
}
.auth-card {
width: 100%; max-width: 400px;
padding: 28px;
background: var(--surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-overlay);
}
.auth-card .brand {
justify-content: center;
margin-bottom: 6px;
}
.auth-card .brand svg { width: 34px; height: 34px; }
.auth-card h1 { text-align: center; font-size: 19px; margin-top: 12px; }
.auth-card .sub {
text-align: center; font-size: 13px; color: var(--ink-2);
margin: 5px 0 22px;
}
.auth-card .btn { margin-top: 6px; }
.alert {
display: flex; align-items: flex-start; gap: 8px;
padding: 10px 12px; margin-bottom: 14px;
font-size: 12.5px; line-height: 1.45;
border-radius: var(--radius);
}
.alert svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }
.alert.err { color: var(--red); background: var(--red-tint); }
.alert.warn { color: var(--orange); background: var(--orange-tint); }
.alert.info { color: var(--ink-2); background: var(--inset);
box-shadow: inset 0 0 0 1px var(--line); }
/* ── Settings ─────────────────────────────────────────────────────────── */
.settings-nav { display: flex; gap: 4px; margin-bottom: 18px; flex-wrap: wrap; }
.kv { display: flex; justify-content: space-between; gap: 16px;
padding: 9px 0; font-size: 13px; }
.kv + .kv { border-top: 1px solid var(--line-soft); }
.kv dt { color: var(--ink-2); }
.kv dd { margin: 0; font-family: var(--mono); font-size: 12.5px;
text-align: right; word-break: break-all; }
/* ── Utility ──────────────────────────────────────────────────────────── */
.mono { font-family: var(--mono); }
.muted { color: var(--ink-2); }
.small { font-size: 12.5px; }
.stack { display: flex; flex-direction: column; gap: 14px; }
.stack-s { display: flex; flex-direction: column; gap: 8px; }
.hidden { display: none !important; }
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg) } }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}