/* Contacts — same design variables as the rest of the platform (.assets/auth.css). */

* { box-sizing: border-box; }

/* Author `display` rules (.editor label > input, for one) otherwise beat the
   UA stylesheet's [hidden] { display: none }, which would leave "hidden"
   file inputs visible. */
[hidden] { display: none !important; }

/* An app-shell layout, not a scrolling page: header and toolbar take their
   natural height, and everything below (.workspace) fills what's left. That
   fixed-height remainder is what lets the two panes each scroll on their
   own instead of one long page scroll dragging both along together. */
html, body { height: 100%; }

body {
    margin: 0;
    background: var(--auth-bg);
    color: var(--auth-text);
    font: 16px/1.55 system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
    -webkit-font-smoothing: antialiased;
    display: flex;
    flex-direction: column;
}

/* Header chrome (.i-top, .i-nav, .i-who) lives in /.assets/chrome.css,
   shared with the launcher and My phone. */

main {
    flex: 1;
    min-height: 0; /* the flexbox trick that lets a flex child actually be scrollable instead of growing to fit its content */
    display: flex;
    flex-direction: column;
    max-width: 720px;
    width: 100%;
    margin: 0 auto;
    padding: 18px 18px 0;
}

@media (min-width: 880px) {
    main { max-width: 1120px; }
}
/* Wide enough for the tree as a third column — see the .pane-tree query at
   the bottom of this file. */
@media (min-width: 1040px) {
    main { max-width: 1320px; }
}

.muted { color: var(--auth-muted); }
.empty { color: var(--auth-muted); text-align: center; padding: 30px 10px; }

/* ---------- Toolbar & list ---------- */

.toolbar {
    flex: none;
    display: flex;
    gap: 10px;
    padding-bottom: 14px;
}
/* The two labelled buttons travel together so the mobile query below can give
   them a row of their own without hand-placing each one. */
.toolbar-actions { display: flex; gap: 10px; }
.toolbar input[type=search] {
    flex: 1;
    font: inherit;
    padding: 10px 12px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-input-bg);
    color: var(--auth-text);
    transition: border-color .15s, box-shadow .15s;
}
.toolbar input[type=search]:focus {
    outline: none;
    border-color: var(--auth-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--auth-accent) 20%, transparent);
}

/* Opens the tag tree as a drawer at widths where it isn't a column. */
#btn-folders { flex: none; padding: 8px 12px; font-size: 1.05rem; line-height: 1; }

/* On a phone the search box and the two labelled buttons can't share a line
   without the labels wrapping mid-word, so the labelled buttons drop to their
   own row. Search keeps the ☰ for company on the first row — giving search a
   `flex-basis: 100%` instead left the ☰ alone on a row of its own, which is a
   lot of vertical space for one icon. */
@media (max-width: 559px) {
    .toolbar { flex-wrap: wrap; }
    .toolbar input[type=search] { flex: 1 1 0; min-width: 0; }
    .toolbar-actions { flex: 1 1 100%; }
    .toolbar-actions button { flex: 1; }
}

.avatar {
    flex: none;
    width: 38px; height: 38px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--auth-surface);
}
.avatar.placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: .8rem;
    font-weight: 700;
    color: var(--auth-muted);
    border: 1px solid var(--auth-border-strong);
}

/* ---------- Workspace panes ----------
   Mobile default: one pane visible at a time, toggled by `.detail-open` on
   `.workspace`, with the tree reached through the toolbar's ☰ as a drawer.
   The `min-width` queries below are the only thing that turns this into a
   side-by-side layout — same markup, same JS, just a different arrangement:
   list+detail at 880px, tree+list+detail at 1040px.

   Every pane gets its own `overflow-y: auto` within `.workspace`'s fixed
   height (inherited from `main`'s flex:1), so a long contact list scrolls
   independently of whatever's open in the detail pane or how far the tree is
   scrolled — none of them pushes another around. */

.workspace { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* .pane-tree is the tag tree. Hidden by default and reached through the
   toolbar's ☰ as a drawer — a tree needs width the phone doesn't have. It
   becomes a real column at ≥1040px, where there's room for all three. */
.pane-tree { display: none; }
.pane-tree-scroll { height: 100%; overflow-y: auto; }

/* .pane-list is chrome (the list head) + its own scroll viewport, so the head
   stays put while the rows beneath it scroll — same split as .pane-detail /
   .pane-detail-inner below, for the same reason.

   `position: relative` anchors .bulk-bar, which floats over the bottom of the
   list instead of sitting in the flow. */
.pane-list   { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; }
.pane-list-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.pane-detail { display: none; flex: 1; min-height: 0; overflow-y: auto; }
.workspace.detail-open .pane-list { display: none; }
.workspace.detail-open .pane-detail { display: block; }

.pane-detail-inner {
    background: var(--auth-card);
    border: 2px solid var(--auth-border-strong);
    border-radius: 0;
    padding: 24px;
    box-shadow: 3px 3px 0 var(--auth-border-strong);
    margin-bottom: 18px; /* breathing room at the bottom of its own scroll area */
}

.detail-empty { color: var(--auth-muted); text-align: center; padding: 60px 10px; font-size: .95rem; }

.back-button {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: none;
    border: none;
    box-shadow: none;
    font-family: inherit;
    text-transform: none;
    letter-spacing: normal;
    color: var(--auth-accent-hover, var(--auth-accent));
    font-weight: 600;
    font-size: .9rem;
    padding: 0 0 18px;
    cursor: pointer;
}
.back-button:hover { text-decoration: underline; }
.back-button:active { transform: none; }

@media (min-width: 880px) {
    /* Row, not column, and no align-items override — the default `stretch`
       is what makes both panes take the workspace's full height so each has
       something bounded to scroll within, rather than growing to fit content. */
    .workspace { flex-direction: row; gap: 20px; }
    .workspace.detail-open .pane-list { display: flex; } /* undo the mobile hide-on-select */

    .pane-list { width: 320px; flex: none; }
    .pane-detail { display: block; } /* always visible, selection or not */

    .back-button { display: none; } /* both panes are always on screen; nothing to go "back" from */
}

/* Three columns: tree | list | detail. The list gets 360 rather than the
   880px layout's 320 — with a tree taking a column of its own there's room,
   and it's the difference between "Katherine Johnson" fitting and not. */
@media (min-width: 1040px) {
    .pane-tree { display: block; width: 220px; flex: none; }
    .pane-list { width: 360px; }
    #btn-folders { display: none; } /* the tree is on screen; the drawer button is redundant */
}

.contact-list { list-style: none; margin: 0; padding: 0; }
.contact-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    background: var(--auth-card);
    border: 1px solid var(--auth-border);
    border-radius: 0;
    margin-bottom: 6px;
    cursor: pointer;
    transition: border-color .12s, background .12s, transform .12s;
}
.contact-row:hover { border-color: var(--auth-border-strong); transform: translateX(1px); }
.contact-row.active { border-color: var(--auth-accent); background: var(--auth-surface); }
.contact-main {
    display: flex;
    flex-direction: column;
    gap: 0;
    min-width: 0;
    flex: 1;
}
.contact-main .fn {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Org and email share one line under the name rather than the email sitting
   in a right-hand column. In the 320-360px list pane a side column took 40%
   of the row and left the name about 90px — every name in the list came out
   truncated to "Ada Lovel…" while the detail pane sat mostly empty. Stacked,
   the name gets the full width and the secondary line truncates instead,
   which is the right thing to sacrifice. */
.contact-main .meta {
    color: var(--auth-muted);
    font-size: .82rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------- Checkboxes ---------- */

/* A column of checkboxes down every row is visual noise for the 99% of the
   time you're reading the list, not selecting from it. So they're hidden
   until they're plausibly wanted: the row you're hovering, the one the
   keyboard is on, the ones already ticked — or any row at all, once a
   selection exists and stays visible is the more useful behaviour.
   `opacity`/`pointer-events` rather than `display` so the column keeps its
   width and nothing shifts when one appears. */
.row-checkbox {
    flex: none;
    width: 17px; height: 17px;
    margin: 0;
    accent-color: var(--auth-accent);
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s;
}
.contact-row:hover .row-checkbox,
.contact-row:focus-within .row-checkbox,
.row-checkbox:checked,
.row-checkbox:focus-visible,
.pane-list.selecting .row-checkbox { opacity: 1; pointer-events: auto; }

/* ---------- List head: count + select all/none ---------- */

.list-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 2px 8px;
    font-size: .82rem;
    color: var(--auth-muted);
    flex: none;
}
.list-head .list-count { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-head strong { color: var(--auth-text); font-weight: 600; }
.link-btn {
    background: none;
    border: none;
    box-shadow: none;
    font-family: inherit;
    text-transform: none;
    letter-spacing: normal;
    color: var(--auth-accent-hover, var(--auth-accent));
    font-weight: 600;
    font-size: .82rem;
    padding: 2px 4px;
    cursor: pointer;
    flex: none;
}
.link-btn:hover { text-decoration: underline; }
.link-btn:active { transform: none; }
.link-btn:disabled { color: var(--auth-muted); cursor: default; text-decoration: none; }

/* ---------- Tag tree ---------- */

.tree { list-style: none; margin: 0; padding: 0; }
.tree-children { list-style: none; margin: 0; padding: 0; }
.tree-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    border-radius: 0;
    cursor: pointer;
    font-size: .88rem;
    color: var(--auth-text);
}
.tree-row:hover { background: var(--auth-surface); }
.tree-row.active { background: var(--auth-surface); color: var(--auth-accent-hover, var(--auth-accent)); font-weight: 600; }
.tree-row:focus-visible { outline: 2px solid var(--auth-accent); outline-offset: -2px; }
.tree-caret {
    flex: none;
    width: 14px;
    text-align: center;
    color: var(--auth-muted);
    font-size: .7rem;
    line-height: 1;
}
.tree-caret.leaf { visibility: hidden; }
.tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-name.muted { color: var(--auth-muted); }
.tree-count { flex: none; font-size: .74rem; color: var(--auth-muted); font-variant-numeric: tabular-nums; }

/* In the drawer these are finger targets, not mouse ones — the desktop tree's
   26px rows are comfortable to click and too small to tap. */
@media (max-width: 1039px) {
    #tag-drawer .tree-row { padding-top: 10px; padding-bottom: 10px; font-size: .95rem; }
    #tag-drawer .tree-caret { width: 18px; font-size: .8rem; }
}

/* ---------- Bulk selection bar ---------- */

/* A floating overlay, not a row in the list. As an in-flow sibling it grew
   the pane the instant you ticked a box, shoving every contact below it down
   by its own height — the list jumped exactly when you were aiming at it.
   Absolute positioning takes it out of flow entirely, so selecting never
   moves a row; the list just gains bottom padding so the last contact can
   still be scrolled out from under it.

   Two deliberate rows inside rather than one wrapping row: in the 320px list
   pane the count and four buttons can't share a line, and wrapping on its
   own stranded one button on a ragged second line. */
.bulk-bar {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: none;
    flex-direction: column;
    gap: 8px;
    padding: 10px 12px;
    background: var(--auth-surface);
    border: 2px solid var(--auth-border-strong);
    border-radius: 0;
    box-shadow: 3px -3px 0 var(--auth-border-strong);
    font-size: .88rem;
    z-index: 2;
}
.bulk-bar.active { display: flex; }
/* The list's bottom padding is measured from the bar's rendered height in
   renderBulk() rather than hardcoded, because the bar's height depends on
   whether the buttons fit on one line at this width. */
.bulk-head { display: flex; align-items: center; gap: 10px; }
.bulk-head .count { font-weight: 600; flex: 1; }
/* A 2x2 grid, not a wrapping row. With four buttons and only ~296px of pane,
   a wrapping row fits three and strands "Delete" alone on a full-width second
   line — which is how it looked the first time round. Four equal cells read
   as deliberate at every width and give a bigger tap target on a phone. */
.bulk-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.bulk-actions button { padding: 7px 8px; font-size: .82rem; white-space: nowrap; }

/* ---------- Overlay / editor ---------- */

.overlay {
    position: fixed; inset: 0;
    background: rgba(0,0,0,.45);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 40px 16px;
    overflow-y: auto;
    z-index: 80;
    animation: contacts-fade-in .12s ease-out;
}

@keyframes contacts-fade-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

/* Bare field styling — used both inline in the detail pane (which supplies
   its own card background via .pane-detail) and inside the Sync & Import
   modal (which needs the card look here, since .overlay has none). */
.editor { width: 100%; animation: contacts-fade-in .15s ease-out; }

.overlay .editor {
    background: var(--auth-card);
    border: 2px solid var(--auth-border-strong);
    border-radius: 0;
    box-shadow: 3px 3px 0 var(--auth-border-strong);
    padding: 22px;
    max-width: 520px;
}

.editor h2, .editor h3 {
    font-family: var(--i-mono, ui-monospace, monospace);
    text-transform: uppercase;
    letter-spacing: .03em;
}
.editor h2 { margin: 0 0 16px; font-size: 1rem; }
.editor h3 { margin: 0 0 6px; font-size: .88rem; }
.editor section { margin-bottom: 22px; }
.editor section:last-of-type { margin-bottom: 12px; }

.editor label { display: block; font-size: .86rem; font-weight: 600; margin-bottom: 12px; }
.editor label > input, .editor label > textarea {
    display: block;
    width: 100%;
    font: inherit;
    font-weight: 400;
    margin-top: 5px;
    padding: 9px 11px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-input-bg);
    color: var(--auth-text);
    transition: border-color .12s, box-shadow .12s;
}
.editor input:focus, .editor textarea:focus {
    outline: none;
    border-color: var(--auth-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--auth-accent) 20%, transparent);
}

/* Given/Family and Display name/Organization pair up on a wide detail pane
   instead of stretching into single very-wide inputs. */
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
.field-grid label { min-width: 0; }

.field-hint { display: block; font-weight: 400; font-size: .78rem; color: var(--auth-muted); margin: -4px 0 10px; }

.field-group { margin-bottom: 12px; }
.field-group-head {
    display: flex; justify-content: space-between; align-items: center;
    font-family: var(--i-mono, ui-monospace, monospace);
    text-transform: uppercase; letter-spacing: .03em;
    font-size: .78rem; font-weight: 700; margin-bottom: 6px;
}
.field-group-head button { padding: 3px 10px; font-size: .82rem; }

.field-row { display: flex; gap: 6px; margin-bottom: 6px; }
.field-row .type { width: 90px; flex: none; }
.field-row .value { flex: 1; }
.field-row input {
    font: inherit;
    padding: 8px 10px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-input-bg);
    color: var(--auth-text);
}
.field-row .remove {
    flex: none;
    width: 32px;
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
}

.editor-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 2px solid var(--auth-border-strong);
}
.editor-actions > div { display: flex; gap: 8px; }

/* ---------- Import panel ---------- */

/* The remote-pull form is for people migrating in from another provider, not
   the common path, so it stays folded away: the panel's default state is one
   dropzone and one button. */
details.more { margin-top: 10px; }
details.more > summary {
    cursor: pointer;
    font-family: var(--i-mono, ui-monospace, monospace);
    text-transform: uppercase;
    letter-spacing: .03em;
    font-size: .78rem;
    font-weight: 700;
    padding: 6px 0;
}
details.more[open] > summary { margin-bottom: 2px; }

.import-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px 0;
    border-top: 2px solid var(--auth-border-strong);
}
.import-form:first-of-type { border-top: none; }
.import-form label { font-size: .86rem; font-weight: 600; }
.import-form input {
    font: inherit;
    padding: 9px 11px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-input-bg);
    color: var(--auth-text);
}
.import-form button { align-self: flex-start; }

/* Created on demand inside whichever import form is running, so the bar sits
   under the button you pressed rather than at the foot of the panel. */
.import-result { margin-top: 8px; }

.dropzone {
    border: 2px dashed var(--auth-border-strong);
    border-radius: 0;
    padding: 16px;
    text-align: center;
    cursor: pointer;
    transition: border-color .15s, background .15s;
}
.dropzone:hover, .dropzone:focus-visible { border-color: var(--auth-accent); outline: none; }
.dropzone.drag { border-color: var(--auth-accent); background: var(--auth-surface); }
.dropzone.busy { opacity: .6; pointer-events: none; }
.dropzone-text { font-size: .9rem; font-weight: 600; }
.dropzone-file { font-size: .82rem; margin-top: 4px; }

/* ---------- Photo picker (editor) ---------- */

.photo-picker { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }

/* A real <button>, not a div — the circle itself is what opens the crop
   popup, with a hover overlay hinting that it's clickable rather than just
   decorative. */
.photo-preview {
    position: relative;
    flex: none;
    width: 64px; height: 64px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--auth-surface);
    border: 2px solid var(--auth-border-strong);
    box-shadow: none; /* a square offset shadow reads as a mistake on a circular photo */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    cursor: pointer;
    transition: border-color .12s;
}
.photo-preview:hover { border-color: var(--auth-accent); }
.photo-preview img { width: 100%; height: 100%; object-fit: cover; }
.photo-preview .placeholder { color: var(--auth-muted); font-size: .72rem; text-align: center; }

.photo-preview .edit-hint {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,.45);
    color: white;
    font-size: .72rem;
    font-weight: 600;
    opacity: 0;
    transition: opacity .12s;
}
.photo-preview:hover .edit-hint, .photo-preview:focus-visible .edit-hint { opacity: 1; }

.photo-picker-actions { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.photo-picker-actions .muted { font-size: .82rem; }

.button-like {
    display: inline-block;
    font-family: var(--i-mono, ui-monospace, monospace);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    font-size: .78rem;
    padding: 8px 14px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-surface);
    color: var(--auth-text);
    cursor: pointer;
    box-shadow: 2px 2px 0 var(--auth-border-strong);
}
.button-like:hover { border-color: var(--auth-accent); }
.button-like:active { transform: translate(2px, 2px); box-shadow: none; }

/* ---------- Photo crop popup ---------- */

.crop-viewport {
    width: 260px; height: 260px;
    margin: 0 auto 18px;
    border-radius: 50%;
    overflow: hidden;
    background: repeating-conic-gradient(var(--auth-surface) 0% 25%, var(--auth-card) 0% 50%) 50% / 20px 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    cursor: grab;
    touch-action: none; /* stops touch drag from also scrolling the page behind the popup */
    border: 2px solid var(--auth-border-strong);
}
.crop-viewport.dragging { cursor: grabbing; }
/* Pinned to the viewport's top-left corner rather than centred by the layout.
   Every automatic centring method (flex, inset:0 + margin:auto) gives up and
   anchors the box to one edge once it's bigger than its container, which this
   image always is — and paint()'s transform assumes a known origin, so the
   layout must not quietly move it. Starting at 0,0 makes the image's own
   centre exactly (width/2, height/2) there, and paint() translates from that. */
.crop-viewport img {
    position: absolute;
    left: 0;
    top: 0;
    max-width: none;
    max-height: none;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none; /* the viewport, not the img, owns the pointer/drag handling */
}
.crop-empty {
    color: var(--auth-muted);
    font-size: .86rem;
    text-align: center;
    padding: 0 20px;
}

.crop-controls { display: flex; flex-direction: column; gap: 12px; margin-bottom: 18px; }
.crop-controls .row { display: flex; align-items: center; gap: 10px; }
.crop-controls .row label { flex: none; width: 56px; font-size: .82rem; font-weight: 600; margin: 0; }
.crop-controls input[type=range] { flex: 1; accent-color: var(--auth-accent); }
.crop-rotate { display: flex; gap: 8px; }
.crop-rotate button { padding: 6px 10px; font-size: 1rem; line-height: 1; }

/* ---------- Buttons ---------- */

button {
    font: inherit;
    font-family: var(--i-mono, ui-monospace, monospace);
    font-weight: 700;
    font-size: .85rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    padding: 10px 18px;
    border-radius: 0;
    border: 2px solid var(--auth-border-strong);
    background: var(--auth-accent);
    border-color: var(--auth-accent);
    color: white;
    cursor: pointer;
    box-shadow: 2px 2px 0 var(--auth-border-strong);
    transition: filter .12s, transform .05s, box-shadow .05s;
}
button:hover:not(:disabled)   { filter: brightness(1.08); }
button:active:not(:disabled)  { transform: translate(2px, 2px); box-shadow: none; }
button:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--auth-accent) 30%, transparent);
}
button.secondary { background: var(--auth-surface); border-color: var(--auth-border-strong); color: var(--auth-text); }
button.secondary:hover:not(:disabled) { filter: none; border-color: var(--auth-accent); }
button.danger { background: var(--auth-error); border-color: var(--auth-error); color: white; }
button:disabled { opacity: .5; cursor: default; }

/* ---------- Progress ---------- */

.progress { display: flex; flex-direction: column; gap: 6px; }
.progress-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    font-size: .86rem;
}
.progress-label { flex: 1; min-width: 0; font-weight: 600; }
.progress-count {
    flex: none;
    color: var(--auth-muted);
    font-size: .8rem;
    font-variant-numeric: tabular-nums; /* the number mustn't jiggle as it counts */
}
/* --auth-border is near-invisible against the card in light mode (#e2e8f0 on
   white), which left the bar reading as an empty gap rather than a track. The
   strong token holds up on both themes. */
.progress-track {
    height: 10px;
    border-radius: 0;
    background: var(--auth-surface);
    border: 2px solid var(--auth-border-strong);
    overflow: hidden;
}

/* The track sits on two different surfaces — the white editor card, and the
   bulk bar whose background IS --auth-surface — so no opaque token can
   contrast with both (an --auth-surface track is invisible in the bar, an
   --auth-card one is invisible in the card). A translucent tint of the border
   token is defined relative to whatever is behind it, so it reads on both. */
@supports (background: color-mix(in srgb, red 50%, transparent)) {
    .progress-track {
        background: color-mix(in srgb, var(--auth-border-strong) 40%, transparent);
    }
}
.progress-fill {
    height: 100%;
    background: var(--auth-accent);
    border-radius: 0;
    /* Width is set from JS on every event. The transition makes ~100 discrete
       steps read as continuous motion rather than a stutter. */
    transition: width .18s ease-out;
}
.progress-detail { margin: 0; font-size: .8rem; color: var(--auth-muted); }
.progress-cancel { align-self: flex-start; padding: 0; }

/* No total to divide by — a track that fills to 100% and stops would read as
   "nearly done" forever, so the indeterminate state sweeps instead. */
.progress.indeterminate .progress-fill {
    width: 35% !important;
    animation: contacts-sweep 1.1s ease-in-out infinite;
}
@keyframes contacts-sweep {
    0%   { margin-left: -35%; }
    100% { margin-left: 100%; }
}
@media (prefers-reduced-motion: reduce) {
    .progress.indeterminate .progress-fill { animation-duration: 2.4s; }
    .progress-fill { transition: none; }
}

/* ---------- Status ---------- */

.banner { border-radius: 0; padding: 11px 14px; margin-bottom: 16px; font-size: .9rem; border: 2px solid; }
.banner.err  { background: rgba(239,68,68,.10);  border-color: rgba(239,68,68,.4); color: var(--auth-error); }
.banner.ok   { background: rgba(34,197,94,.10);  border-color: rgba(34,197,94,.4); }
.banner.wait { background: rgba(245,158,11,.10); border-color: rgba(245,158,11,.4); }

/* Top-centred, not bottom-centred: the bottom of the window is where this
   app's most consequential controls live — the selection bar's Delete among
   them — and a toast parked over them covers a destructive button's label
   while taps pass straight through to it. The top of the screen is the one
   region with nothing interactive in it at any width. */
.toast {
    position: fixed;
    top: 68px; left: 50%; /* clears the app header rather than sitting on it */
    transform: translateX(-50%);
    max-width: calc(100vw - 32px);
    background: var(--auth-card);
    border: 2px solid var(--auth-border-strong);
    border-radius: 0;
    padding: 11px 20px;
    box-shadow: 3px 3px 0 var(--auth-border-strong);
    font-size: .92rem;
    z-index: 90;
    pointer-events: none; /* informational only; never swallow a click */
    animation: contacts-toast-in .16s ease-out;
}
@keyframes contacts-toast-in {
    from { opacity: 0; transform: translate(-50%, -8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
.toast.err { border-color: var(--auth-error); color: var(--auth-error); }
