  /* Brand type: Plus Jakarta Sans, bundled as variable-weight woff2 subsets
     (shared with the desktop app; copied into public/fonts/). */
  @font-face {
    font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 200 800; font-display: swap;
    src: url("/fonts/PlusJakartaSans-cyrillic-ext.woff2") format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
  }
  @font-face {
    font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 200 800; font-display: swap;
    src: url("/fonts/PlusJakartaSans-vietnamese.woff2") format('woff2');
    unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
  }
  @font-face {
    font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 200 800; font-display: swap;
    src: url("/fonts/PlusJakartaSans-latin-ext.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
  @font-face {
    font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 200 800; font-display: swap;
    src: url("/fonts/PlusJakartaSans-latin.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  /* Brand palette (shared with the desktop app renderer): amber #C6851A as the
     one accent, light = reference (white surfaces, #F6F6F8 rail, #ECECEE
     hairlines), dark = the same scheme on near-black. Dark by default (:root),
     Light forced via :root[data-theme="light"], System (no attr) follows OS. */
  :root {
    --bg: #17171a; --card: #1e1e23; --card2: #26262c; --text: #f0f0f3;
    --muted: #a5a5ad; --faint: #8a8a92; --strong: #d2d2d8;
    --accent: #c6851a; --accent-tint: rgba(198, 133, 26, .16);
    --link: #e0a957; --link-hover: #eec27f;
    --ok: #3ecf8e; --err: #ff5c5c; --line: #2a2a31; --line2: #3a3a42;
    --team: #6d7ff2; --team-tint: rgba(109, 127, 242, .18);
  }
  :root[data-theme="light"] {
    --bg: #ffffff; --card: #ffffff; --card2: #f6f6f8; --text: #17171a;
    --muted: #6e6e76; --faint: #9a9aa2; --strong: #3a3a40;
    --accent: #c6851a; --accent-tint: #fbf1de;
    --link: #a66e14; --link-hover: #7a4f0e;
    --ok: #12a150; --err: #d92d20; --line: #ececee; --line2: #d9d9de;
    --team: #4d5fd6; --team-tint: #e9ebfd;
  }
  @media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
      --bg: #ffffff; --card: #ffffff; --card2: #f6f6f8; --text: #17171a;
      --muted: #6e6e76; --faint: #9a9aa2; --strong: #3a3a40;
      --accent: #c6851a; --accent-tint: #fbf1de;
      --link: #a66e14; --link-hover: #7a4f0e;
      --ok: #12a150; --err: #d92d20; --line: #ececee; --line2: #d9d9de;
      --team: #4d5fd6; --team-tint: #e9ebfd;
    }
  }
  * { box-sizing: border-box; margin: 0; }
  body {
    background: var(--bg); color: var(--text);
    font: 14px/1.5 'Plus Jakarta Sans', -apple-system, "Segoe UI", Roboto, sans-serif;
    min-height: 100vh;
  }
  .wrap { max-width: 860px; margin: 0 auto; padding: 28px 16px 60px; }
  a { color: var(--link); text-decoration: none; }
  a:hover { color: var(--link-hover); }
  .hidden { display: none !important; }
  .muted { color: var(--muted); }
  .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12px; }

  /* The brand's one primary: amber pill, white text, no all-caps. */
  button {
    background: var(--accent); color: #fff; border: 0; border-radius: 9px;
    padding: 9px 16px; font-family: inherit; font-weight: 700; font-size: 13px; letter-spacing: 0;
    cursor: pointer;
  }
  button:hover { background: color-mix(in srgb, var(--accent) 88%, #000); }
  /* Quiet companion actions: text-only until hovered. */
  button.ghost { background: transparent; color: var(--strong); border: 1px solid transparent; }
  button.ghost:hover { color: var(--text); background: var(--card2); }
  button.sm { padding: 5px 11px; font-size: 12px; }
  button:disabled { opacity: 0.4; cursor: default; }
  input {
    background: var(--card2); border: 1px solid var(--line); border-radius: 9px;
    color: var(--text); padding: 10px 12px; width: 100%; font-family: inherit; font-size: 14px;
  }
  input:focus { outline: none; border-color: var(--accent); }

  /* top bar — sticky so the account menu stays reachable while scrolling.
     44px tall (avatar-driven); the rail offsets below hang off that number. */
  .topbar { display: flex; align-items: center; gap: 12px; margin-bottom: 24px;
    position: sticky; top: 0; z-index: 30; background: var(--bg); }
  .brand, .brand:hover { display: flex; align-items: center; font-size: 22px; font-weight: 800; letter-spacing: -0.01em; color: var(--text); }
  .brand .dot { color: var(--accent); }
  .spacer { flex: 1; }
  .avatar {
    width: 34px; height: 34px; border-radius: 50%; background: var(--accent); color: #fff;
    display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 14px;
    overflow: hidden; flex: none;
  }
  .avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

  /* Account menu: avatar (+ name/email) is the trigger; toggles + sign out live
     inside the dropdown so the top bar stays clean. */
  .usermenu { position: relative; }
  .acct {
    display: flex; align-items: center; gap: 10px; cursor: pointer;
    background: transparent; border: 1px solid transparent; border-radius: 999px;
    padding: 4px 6px 4px 12px; color: var(--text);
  }
  .acct:hover { background: var(--card2); border-color: var(--line); }
  /* Cap the identity block: the centered get-app pill is out of the flow, so a
     long name/email would slide under it instead of pushing it aside. The full
     address is always available in the dropdown, which ellipsizes the same way. */
  .acct .who { text-align: right; line-height: 1.25; max-width: 180px; }
  .acct .who .n { font-weight: 700; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .acct .who .e { color: var(--muted); font-size: 12px; }
  /* Below this even the capped block collides with the centered pill — the
     avatar identifies the account on its own (the phone rule always worked this
     way), so the text goes before the pill does. */
  @media (max-width: 760px) { .acct .who { display: none; } }
  /* Identity lines carry the provider mark: an account is keyed by (provider,
     email), so the same address via Google and via GitHub are two accounts —
     the address alone doesn't say which one you're on. The mark never shrinks,
     so the address is what ellipsizes. */
  .acct .who .e, .menu-head .e { display: flex; align-items: center; gap: 5px; }
  .acct .who .e { justify-content: flex-end; }
  .acct .who .e .m, .menu-head .e .m { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .prov { display: inline-flex; flex: none; width: 13px; height: 13px; }
  .prov svg { width: 13px; height: 13px; display: block; }
  .prov:empty { display: none; }
  .acct .caret { width: 15px; height: 15px; color: var(--muted); flex: none; transition: transform .18s; }
  .usermenu.open .acct { background: var(--card2); border-color: var(--line); }
  .usermenu.open .caret { transform: rotate(180deg); }

  .menu {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 40;
    min-width: 224px; background: var(--card); border: 1px solid var(--line);
    border-radius: 12px; padding: 6px; box-shadow: 0 16px 40px rgba(0,0,0,.4);
  }
  .menu-head { padding: 8px 10px 10px; border-bottom: 1px solid var(--line); margin-bottom: 6px; }
  .menu-head .n { font-weight: 700; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .menu-head .e { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .menu-item {
    display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
    background: transparent; border: none; border-radius: 8px; padding: 9px 10px;
    color: var(--text); font-size: 13px; font-weight: 600;
  }
  .menu-item:hover { background: var(--card2); }
  .menu-item .mi-ic { display: flex; width: 17px; height: 17px; color: var(--muted); flex: none; }
  .menu-item .mi-ic svg { width: 17px; height: 17px; }
  .menu-item .mi-label { flex: 1; }
  .menu-item .mi-val { color: var(--muted); font-size: 12px; font-weight: 700; }
  .menu-item.on { color: var(--accent); background: var(--accent-tint); }
  .menu-item.on .mi-ic { color: var(--accent); }
  .menu-item.danger { color: var(--err); }
  .menu-item.danger .mi-ic { color: var(--err); }
  .menu-item.danger:hover { background: color-mix(in srgb, var(--err) 12%, transparent); }
  .menu-sep { height: 1px; background: var(--line); margin: 6px 4px; }
  .icon-btn {
    width: 36px; height: 36px; padding: 0; display: flex; align-items: center; justify-content: center;
    background: transparent; color: var(--muted); border: 1px solid var(--line); border-radius: 9px;
  }
  .icon-btn:hover { color: var(--text); background: var(--card2); border-color: var(--line2); }
  .icon-btn svg { width: 17px; height: 17px; }

  /* Subtle, always-present "get the app" affordance in the top bar — for people
     who sign in on the web before they've installed peeza anywhere. Centered on
     the bar itself (the sticky topbar is its containing block): flex order
     can't true-center it because the brand and account clusters differ in
     width, so it's lifted out of the flow instead. */
  .get-app { display: inline-flex; align-items: center; gap: 7px; padding: 7px 12px;
    border-radius: 999px; border: 1px solid var(--line); background: transparent;
    color: var(--strong); font-weight: 700; font-size: 13px;
    position: absolute; left: 50%; transform: translateX(-50%); }
  .get-app:hover { color: var(--text); background: var(--card2); border-color: var(--line2); }
  .get-app svg { width: 15px; height: 15px; flex: none; }

  /* login */
  .login { max-width: 380px; margin: 8vh auto 0; text-align: center; }
  .login .logo { font-size: 36px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 12px; }
  .login .logo .dot { color: var(--accent); }
  .login h1 { font-size: 22px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 6px; }
  .login .tag { color: var(--muted); font-size: 14px; margin-bottom: 26px; }
  .login .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 22px; }
  .provider { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; margin-bottom: 10px; background: var(--card2); color: var(--text); border: 1px solid var(--line); font-weight: 700; font-size: 14px; padding: 12px; }
  .provider:hover { background: var(--card2); border-color: var(--line2); }
  /* Same brand marks as the identity line, at button size. Icon and label ride
     one centred flex group, so the row stays balanced whether the name is
     "Apple" or "Microsoft". */
  .provider .pmark { display: inline-flex; flex: none; width: 18px; height: 18px; }
  .provider .pmark svg { width: 18px; height: 18px; display: block; }
  .sep { display: flex; align-items: center; gap: 10px; color: var(--faint); font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; margin: 16px 0; }
  .sep::before, .sep::after { content: ""; flex: 1; height: 1px; background: var(--line); }
  .devrow { display: flex; gap: 8px; }
  .devrow input { flex: 1; }
  .devrow button { white-space: nowrap; }
  .err { color: var(--err); font-size: 13px; margin-top: 12px; }
  /* Get peeza CTA on the signed-out screen (mirrors the / landing) */
  .dl-link { text-decoration: none; display: block; }
  .dl-btn { width: 100%; display: flex; align-items: center; justify-content: center; gap: 9px; padding: 13px; font-size: 14px; }
  .dl-btn svg { width: 18px; height: 18px; }
  .site-link { margin-top: 14px; font-size: 13px; color: var(--muted); }

  /* Waitlist: signed in, but the account isn't approved for the closed beta
     yet. Borrows the login column so a person who just signed in stays in the
     same visual place instead of landing somewhere new. */
  .pd-icon { width: 52px; height: 52px; border-radius: 15px; margin: 0 auto 16px;
    background: var(--accent-tint); color: var(--accent); display: flex; align-items: center; justify-content: center; }
  .pd-icon svg { width: 26px; height: 26px; }
  .pd-who { display: flex; align-items: center; gap: 10px; text-align: left; }
  .pd-who .avatar { width: 32px; height: 32px; font-size: 12px; }
  .pd-who .pd-lbl { color: var(--faint); font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; }
  .pd-who .pd-mail { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .pd-note { border-top: 1px solid var(--line); margin-top: 16px; padding-top: 16px;
    color: var(--muted); font-size: 13px; line-height: 1.5; text-align: left; }
  .pd-when { margin-top: 8px; color: var(--faint); font-size: 12.5px; }
  .pd-out { margin-top: 16px; }

  /* First-run onboarding: shown in place of the whole dashboard when the account
     has no machines yet — until peeza is installed and signed in, transfers /
     stats / devices are all empty and there's nothing to control. */
  .onboard { text-align: center; max-width: 460px; margin: 4vh auto 0;
    background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 32px 26px; }
  .onboard .ob-icon { width: 56px; height: 56px; border-radius: 15px; margin: 0 auto 16px;
    background: var(--accent-tint); color: var(--accent); display: flex; align-items: center; justify-content: center; }
  .onboard .ob-icon svg { width: 28px; height: 28px; }
  .onboard h2 { font-size: 20px; font-weight: 800; letter-spacing: -.01em; margin-bottom: 8px; }
  .onboard .ob-sub { color: var(--muted); font-size: 14px; line-height: 1.5; margin-bottom: 22px; }
  .onboard .ob-steps { text-align: left; display: flex; flex-direction: column; gap: 13px; margin-bottom: 24px; }
  .onboard .ob-step { display: flex; align-items: flex-start; gap: 12px; }
  .onboard .ob-num { width: 24px; height: 24px; border-radius: 50%; flex: none; font-size: 12px; font-weight: 800;
    background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; }
  .onboard .ob-text { font-size: 14px; line-height: 1.4; padding-top: 2px; }
  .onboard .ob-text b { font-weight: 700; }
  .onboard .ob-dl { width: 100%; display: flex; align-items: center; justify-content: center; gap: 9px; padding: 13px; font-size: 14px; }
  .onboard .ob-dl svg { width: 18px; height: 18px; }
  .onboard .ob-foot { margin-top: 14px; font-size: 12.5px; color: var(--faint); }
  /* Same hero rendered inside the transfers list instead of over the page: it's
     one option among others there, not the only thing on screen, so it stops
     competing with the drop zone above it. It keeps a clear gap rather than
     hugging the zone — with the filter toolbar hidden in this exact state
     (nothing to filter), only this margin separates "send one now" from
     "install the app", and they're two different offers. */
  .onboard.ob-inline { margin-top: 34px; }

  /* dashboard: left rail tabs (desktop) / top tabs (mobile) + a view pane */
  /* Live indicator, in the top nav beside the brand: a breathing dot instead
     of a ticking timestamp (the 3s poll made the time pure churn); the exact
     refresh time lives in the tooltip. Red dot + message when a poll fails.
     min-width 0 + ellipsis let the error message shrink before it pushes the
     account controls off a phone screen. */
  .updated { display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; min-width: 0; margin-left: 4px; }
  .updated .lt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Phones: dot only — there's no room for a label next to the get-app pill
     and avatar (an error message truncated to two letters helps nobody; the
     red dot alone signals trouble and the poll retries on its own). */
  @media (max-width: 640px) {
    .updated .lt { display: none; }
    .get-app .ga-label { white-space: nowrap; }
  }
  .updated .ld { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: none;
    animation: live-pulse 2.4s ease-in-out infinite; }
  .updated.err { color: var(--err); }
  .updated.err .ld { background: var(--err); animation: none; }
  @keyframes live-pulse {
    0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 45%, transparent); }
    70%, 100% { box-shadow: 0 0 0 5px transparent; }
  }
  @media (prefers-reduced-motion: reduce) { .updated .ld { animation: none; } }
  .layout { display: flex; gap: 20px; align-items: flex-start; }
  .rail { display: flex; flex-direction: column; gap: 4px; width: 150px; flex: none; position: sticky; top: 60px; }
  .tab { display: flex; align-items: center; gap: 9px; padding: 9px 12px; border-radius: 9px;
    font-family: inherit; font-weight: 700; font-size: 13px; color: var(--muted);
    background: transparent; border: 1px solid transparent; cursor: pointer; text-align: left; text-decoration: none; }
  .tab:hover { color: var(--text); background: var(--card2); }
  .tab.on { color: var(--accent); background: var(--accent-tint); }
  .tab svg { width: 17px; height: 17px; flex: none; }
  .tab .ext { margin-left: auto; opacity: .5; }
  .tab .ext svg { width: 13px; height: 13px; }
  /* Admin rail: a way out (the header logo leaves the app), then the sections. */
  .tab.back { font-size: 12px; }
  .tab.back:hover { color: var(--accent); background: transparent; }
  .rail-sep { height: 1px; background: var(--line); margin: 6px 10px; flex: none; }
  .view { flex: 1; min-width: 0; }

  /* transfers toolbar: status chips + device select */
  .filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
  /* Desktop: filters pin right under the stuck topbar (44px), alongside the
     rail. Bottom padding + trimmed margin keep the static spacing at 14px
     while giving the stuck bar a solid strip cards can slide under. */
  @media (min-width: 641px) {
    .filters { position: sticky; top: 44px; z-index: 10; background: var(--bg);
      padding: 4px 0 10px; margin-bottom: 4px; }
  }
  .chip { font-family: inherit; font-size: 12px; font-weight: 700; color: var(--muted);
    background: var(--card2); border: 1px solid var(--line); border-radius: 20px; padding: 5px 12px; cursor: pointer; }
  .chip:hover { color: var(--text); border-color: var(--line2); }
  .chip.on { color: var(--accent); border-color: var(--accent); background: var(--accent-tint); }
  /* Device filter. A listbox rather than a <select> so each row can carry its
     OS badge — native <option> can't hold an SVG. */
  .devsel { position: relative; }
  .ds-btn { display: flex; align-items: center; gap: 7px; max-width: 220px;
    background: var(--card2); border: 1px solid var(--line); border-radius: 9px;
    color: var(--text); padding: 6px 10px; font-family: inherit; font-size: 12px; font-weight: 700; cursor: pointer; }
  .ds-btn:focus-visible, .devsel.open .ds-btn { outline: none; border-color: var(--accent); }
  .ds-btn:disabled { opacity: .55; cursor: default; }
  .ds-btn .ds-cur { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ds-btn .ds-caret { margin-left: auto; flex: none; width: 12px; height: 12px; color: var(--muted); transition: transform .15s; }
  .devsel.open .ds-caret { transform: rotate(180deg); }
  .ds-pop { position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; min-width: 100%; width: max-content; max-width: 260px;
    background: var(--card); border: 1px solid var(--line2); border-radius: 10px; padding: 4px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, .28); display: flex; flex-direction: column; gap: 2px; }
  .ds-pop[hidden] { display: none; }
  .ds-opt { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
    background: none; border: 0; border-radius: 7px; color: var(--text);
    padding: 7px 9px; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 700; }
  .ds-opt:hover, .ds-opt:focus-visible { outline: none; background: var(--card2); }
  .ds-opt[aria-selected="true"] { color: var(--accent); }
  .ds-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ds-ico { display: flex; flex: none; color: var(--accent); }
  .ds-ico svg { width: 14px; height: 14px; }
  .filters .spacer { flex: 1; }

  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--faint); display: inline-block; margin-right: 5px; vertical-align: middle; }
  .status-dot.on { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); }

  /* devices tab: one row per machine */
  .device { display: flex; align-items: center; gap: 12px; background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; }
  .device.off { opacity: 0.72; }
  .device .micon { width: 38px; height: 38px; border-radius: 10px; background: var(--accent-tint); display: flex; align-items: center; justify-content: center; color: var(--accent); flex: none; }
  .device .micon svg { width: 20px; height: 20px; }
  .device .dinfo { flex: 1; min-width: 0; }
  .device .dname { font-weight: 700; font-size: 15px; }
  .device .dsub { color: var(--muted); font-size: 12px; }

  /* stats tab: usage meter + pride stats, ported from the standalone /stats page.
     We meter ONLY relayed bytes; direct is free. */
  .scard { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 18px; margin-bottom: 12px; }
  .plan-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
  .plan-badge { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
    padding: 4px 10px; border-radius: 6px; background: var(--card2); color: var(--muted); border: 1px solid var(--line); }
  .plan-badge.pro { background: var(--accent); color: #fff; border-color: var(--accent); }
  .eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; }
  .meter-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
  .meter-used { font-size: 30px; font-weight: 800; letter-spacing: -.02em; line-height: 1; font-variant-numeric: tabular-nums; }
  .meter-used .u { font-size: 15px; color: var(--muted); font-weight: 600; margin-left: 3px; }
  .meter-cap { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .meter { background: var(--card2); border: 1px solid var(--line); border-radius: 6px; height: 12px; overflow: hidden; }
  .meter > div { height: 100%; background: var(--accent); border-radius: 6px; transition: width .4s ease, background .3s; }
  .meter.over > div { background: var(--err); }
  .meter-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 9px; font-size: 12.5px; color: var(--muted); }
  .meter-foot b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
  .warn-line { display: none; align-items: center; gap: 8px; margin-top: 12px; padding: 9px 11px; border-radius: 8px;
    background: var(--accent-tint); color: var(--link); font-size: 12.5px; font-weight: 600; }
  .warn-line.show { display: flex; }
  .warn-line .wdot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
  /* Purchased-balance line under the monthly meter */
  .topup-line { display: none; margin-top: 9px; font-size: 12.5px; color: var(--muted); }
  .topup-line.show { display: block; }
  .topup-line b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
  /* Buy-relay card */
  .buy-sub { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }
  .buy-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
  .buy-pack { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 12px 10px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--card2); cursor: pointer;
    font: inherit; color: var(--text); transition: border-color .15s, background .15s; }
  .buy-pack:hover { border-color: var(--accent); background: var(--accent-tint); }
  .buy-pack:disabled { opacity: .55; cursor: default; }
  .buy-pack b { font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
  .buy-pack span { font-size: 12.5px; color: var(--muted); }
  /* Referral CTA card (mock) */
  .referral { border-color: var(--accent); }
  .ref-head { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
  .ref-icon { width: 38px; height: 38px; border-radius: 10px; background: var(--accent-tint);
    display: flex; align-items: center; justify-content: center; font-size: 19px; flex: none; }
  .ref-title { font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
  .ref-sub { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }
  .ref-link { display: flex; gap: 8px; }
  .ref-link input { flex: 1; min-width: 0; background: var(--card2); border: 1px solid var(--line);
    border-radius: 9px; padding: 9px 12px; color: var(--text); font-family: inherit;
    font-size: 13px; font-variant-numeric: tabular-nums; }
  .ref-link button { white-space: nowrap; }
  .ref-foot { display: flex; gap: 22px; margin-top: 12px; font-size: 12.5px; color: var(--muted); }
  .ref-foot b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
  .sgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .sgrid .scard { margin: 0; }
  .stat-big { font-size: 26px; font-weight: 800; letter-spacing: -.02em; line-height: 1; font-variant-numeric: tabular-nums; }
  .stat-sub { font-size: 12px; color: var(--muted); margin-top: 6px; }
  /* ---- admin tab (folded-in /admin dashboard). Table/stat/pill styles are
     scoped to #viewAdmin so they can't collide with the dashboard's own. ---- */
  .adm-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
  .adm-bar .grow { flex: 1; min-width: 0; color: var(--muted); font-size: 12px; }
  #viewAdmin .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
  #viewAdmin .stat { background: var(--card2); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
  #viewAdmin .stat .n { font-size: 24px; font-weight: 800; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
  #viewAdmin .stat .l { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
  #viewAdmin table { width: 100%; border-collapse: collapse; font-size: 13px; }
  #viewAdmin th { text-align: left; color: var(--muted); font-weight: 600; padding: 6px 10px; border-bottom: 1px solid var(--line); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; white-space: nowrap; }
  #viewAdmin td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
  #viewAdmin tr:last-child td { border-bottom: 0; }
  #viewAdmin .empty { padding: 6px 10px; }
  /* break-word, not anywhere: anywhere shrinks the cell's min-content width to
     one character, so table auto-layout crushes the ID column into a vertical
     letter-stack when Name/Email hold long unbreakable strings (Apple
     private-relay emails). The pane scrolls horizontally instead. */
  #viewAdmin .mono { overflow-wrap: break-word; }
  /* Users table identity stack: name headline, email + id as muted sub-lines. */
  #viewAdmin .au-name { font-weight: 600; }
  #viewAdmin .au-sub { color: var(--muted); margin-top: 2px; }
  #viewAdmin .au-id { font-size: 11px; }
  #viewAdmin .pill.active, #viewAdmin .pill.on { color: var(--ok); border-color: var(--ok); background: transparent; }
  #viewAdmin .pill.used, #viewAdmin .pill.off { color: var(--muted); border-color: var(--line); background: var(--card2); }
  #viewAdmin .pill.expired { color: var(--err); border-color: var(--err); background: transparent; }
  #admRooms, #admLinks, #admUsers, #admBugs, #admConnPairs, #admConnRecent { overflow-x: auto; }
  /* A version and an OS name are atomic — "1.0.0-beta.54" broken across two
     lines at the hyphen reads as two different builds, which is the one thing
     this column exists to tell you. The section already scrolls sideways. */
  #admBugs .bug-build { white-space: nowrap; }
  .split { display: flex; height: 8px; border-radius: 5px; overflow: hidden; margin-top: 12px; border: 1px solid var(--line); }
  .split .d { background: var(--ok); transition: width .4s ease; }
  .split .r { background: var(--accent); transition: width .4s ease; }
  .legend { display: flex; gap: 14px; margin-top: 9px; font-size: 11px; color: var(--muted); }
  .legend > span { display: flex; align-items: center; gap: 5px; }
  .legend i { width: 8px; height: 8px; border-radius: 2px; flex: none; }
  /* Send-to-device chooser + early-beta download warning. */
  dialog#sendModal, dialog#betaModal { background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 12px; padding: 18px; width: 360px; margin: auto; }
  dialog#sendModal::backdrop, dialog#betaModal::backdrop { background: rgba(0,0,0,.5); }
  .sm-title { font-weight: 600; font-size: 14px; }
  .sm-sub { color: var(--muted); font-size: 12px; margin: 6px 0 12px; line-height: 1.4; }
  .sm-targets { display: flex; flex-direction: column; gap: 6px; max-height: 260px; overflow: auto; }
  .sm-target { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
    background: var(--card2); border: 1px solid var(--line); border-radius: 8px; color: var(--text);
    padding: 9px 11px; cursor: pointer; font-size: 13px; }
  .sm-target:hover { border-color: var(--accent); }
  .sm-target .st-name { font-weight: 600; }
  .sm-target .st-icon { display: flex; color: var(--accent); }
  .sm-target .st-icon svg { width: 15px; height: 15px; }
  .sm-target .st-meta { margin-left: auto; color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 6px; }
  .sm-empty { color: var(--muted); font-size: 13px; text-align: center; padding: 16px 8px; }
  .sm-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
  #betaModal .sm-title { display: flex; align-items: center; gap: 8px; }
  #betaModal .sm-title svg { width: 16px; height: 16px; color: var(--accent); flex: none; }

  /* transfers tab: one card per transfer, each tagged with its device.
     Every card is the SAME height by construction, whatever its state: a
     three-row grid — name, meta, then a bottom row shared by the fixed slot
     (bar or error) and the action buttons. The actions live on that bottom
     row, not in a side column, so their count widens nothing and the meta
     line keeps the full card width; and since every state has at least one
     action, the bottom row is button-tall on every card. Nothing inside a
     card is allowed to wrap — text ellipsizes and the meta row trims its
     least important pieces instead of growing a second line. */
  .xfer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 14px;
    grid-template-areas: "fn fn" "meta meta" "slot acts"; align-items: center;
    background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; }
  /* ---- team cards wear the team hue ----
     The desktop app tints the whole WINDOW by scope (renderer.css, .in-team),
     because there you sit inside one bucket at a time. The dashboard has no
     scope — it is one flat list of everything on every machine — so the same
     rule lands per CARD instead: a transfer that went to a team re-points
     --accent at --team for its own subtree, and its progress bar, status pill
     and device chip come out indigo while a 1:1 card beside it stays amber.
     Same token, same two hues as the app; only the element it is declared on
     differs.
     --ok and --err are untouched here too, so a finished team transfer's bar
     is green and a failed one is red, exactly like a 1:1 one. */
  .xfer.team { --accent: var(--team); --accent-tint: var(--team-tint); }
  /* The chip that says WHICH team. This is not decoration — it is what makes
     the colour legible. In the app the rail teaches the rule (you clicked a
     team to get here); a dashboard card has no such context, so indigo on its
     own would be a mystery. The chip names the team and carries the hue, and
     the two explain each other. Same shape as .devtag, one tone up. */
  .teamtag { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 20px;
    background: var(--team-tint); border: 1px solid color-mix(in srgb, var(--team) 45%, transparent);
    color: var(--team); font-weight: 700; max-width: 100%; }
  .teamtag svg { width: 13px; height: 13px; color: var(--team); flex: none; }
  .teamtag .tt-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .xfer .fn { grid-area: fn; font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .xfer .meta { grid-area: meta; color: var(--muted); font-size: 12px; display: flex; gap: 8px; flex-wrap: nowrap; align-items: center; overflow: hidden; }
  /* One line, no exceptions: chips and figures never wrap. When the row runs
     out of room, things give way in a fixed order and always by visible
     ellipsis, never a mid-glyph clip (a truncated byte figure would misread
     as a smaller number): the timestamp collapses first, then the transport
     mode, then the byte figures — the shrink factors stage that order. The
     device chip never shrinks with the row; it caps at 40% of it and
     ellipsizes inside itself, so the machine's name outlives the trimmings. */
  .xfer .meta > * { flex: none; white-space: nowrap; }
  .xfer .meta .devtag { max-width: 40%; }
  /* Capped like the device chip and for the same reason: a long team name must
     ellipsize inside its own pill rather than push the row into a second line
     (which .meta forbids) or squeeze the machine name out. Tighter than 40% —
     between the two chips the machine is the one you navigate by. */
  .xfer .meta .teamtag { max-width: 30%; }
  .xfer .meta .when { flex: 0 999 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .xfer .meta .mmode { flex: 0 24 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .xfer .meta .msz { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .devtag { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 20px; background: var(--card2); border: 1px solid var(--line); color: var(--strong); font-weight: 700; max-width: 100%; }
  .devtag svg { width: 13px; height: 13px; color: var(--accent); flex: none; }
  .devtag .dt-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Both ends of a transfer between two of the account's own machines (see
     pairUp): sender chip, arrow, receiver chip, in place of the single chip and
     the send/receive tag — the arrow says the direction the tag used to. The
     pair gets a bigger slice of the row than one chip would, and each end
     ellipsizes inside its own half, so one long hostname can't starve the other. */
  .xfer .meta .route { display: inline-flex; align-items: center; gap: 5px; min-width: 0; max-width: 62%; flex: 0 1 auto; }
  .xfer .meta .route .devtag { flex: 0 1 auto; min-width: 0; max-width: none; }
  .route .arr { flex: none; display: flex; color: var(--faint); }
  .route .arr svg { width: 13px; height: 13px; }
  .xfer .meta .when { margin-left: auto; font-variant-numeric: tabular-nums; opacity: .8; }
  /* Row three, fixed height: the progress bar or the failure reason, never
     both, never neither. An error used to claim a full extra line (errors are
     sentences); now it takes the bar's slot — the pill already says "failed",
     so the bar's frozen fill said nothing the sentence doesn't — and the full
     text lives in the title for whatever the ellipsis eats. */
  .xslot { grid-area: slot; min-height: 18px; display: flex; align-items: center; min-width: 0; }
  .xslot .prog { flex: 1; margin-top: 0; }
  .xslot .xerr { flex: 1; min-width: 0; color: var(--err); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Transfer bar. Thinner than the desktop app's (the rows are dense), but the
     colours and the phase behaviour are deliberately identical — see the .bar
     rules in app/renderer.html. Keep the two in step. */
  .prog { height: 5px; border-radius: 4px; background: var(--card2); overflow: hidden; margin-top: 8px; }
  .prog > i { display: block; height: 100%; background: var(--accent); border-radius: 4px; transition: width .3s, background-color .35s ease; }
  .prog.done > i { background: var(--ok); }
  /* A transfer that stopped for good freezes its fill where it died: red for a
     failure, muted for a deliberate stop. Amber never survives into a terminal
     state — it reads as "still moving". */
  .prog.failed > i { background: var(--err); }
  .prog.stopped > i { background: var(--muted); }
  /* Indeterminate: nothing to measure yet (link out but unclaimed, or a peer
     mid-handshake) — a chunk of fill sweeps rather than sitting at a lying 0. */
  .prog.indet > i { width: 30%; animation: barSweep 1.3s ease-in-out infinite; }
  @keyframes barSweep { from { margin-left: -30%; } to { margin-left: 100%; } }
  /* Actions ride one horizontal row beside the bar, not a column: stacked
     buttons made the card's height depend on how many ops a state offers
     (three for a live link share, two for a finished row). */
  .xfer .acts { grid-area: acts; display: flex; align-items: center; gap: 6px; }

  /* The standing "not in the cloud" notice between the toolbar and the list.
     This dashboard is the one peeza surface that LOOKS like a cloud drive — a
     website listing your files — so the fact the whole product stands on
     (files live on machines; nothing is stored in any cloud) is said in the
     view itself, not hidden in a tooltip someone has to find. Quiet colours
     on purpose: wallpaper after the first read, but everyone gets that read. */
  .nocloud { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 12px;
    padding: 9px 13px; border: 1px solid var(--line); border-radius: 10px;
    background: var(--card2); color: var(--muted); font-size: 12.5px; line-height: 1.5; }
  .nocloud svg { width: 16px; height: 16px; flex: none; color: var(--accent); margin-top: 1px; }
  .nocloud b { color: var(--strong); font-weight: 700; }

  /* ---- browser send: the horizontal drop zone above the list ----
     Sending from this tab, as opposed to commanding a machine to send. It sits
     above the list rather than in it because it is not the same kind of thing:
     every row below is daemon-backed and survives a closed tab, and this one
     dies with the page. Same card grammar (radius, border, bar colours) so it
     doesn't read as a foreign object; dashed border while idle so it reads as a
     target rather than a record.
     One send at a time — the zone IS the send while one is live. */
  .dropbar { border: 1.5px dashed var(--line2); border-radius: 12px; background: var(--card);
    padding: 0 16px; margin-bottom: 12px; transition: border-color .18s, background-color .18s; }
  .dropbar[data-sstate="idle"] { cursor: pointer; }
  .dropbar[data-sstate="idle"]:hover, .dropbar.over { border-color: var(--accent); background: var(--accent-tint); }
  /* Live states are records, not targets: solid border, no pointer affordance. */
  .dropbar:not([data-sstate="idle"]) { border-style: solid; border-color: var(--line); }
  .dropbar .db-row { display: flex; align-items: center; gap: 14px; min-height: 58px; padding: 12px 0; }
  .dropbar .db-icon { width: 34px; height: 34px; border-radius: 9px; flex: none;
    background: var(--accent-tint); color: var(--accent); display: flex; align-items: center; justify-content: center; }
  .dropbar .db-icon svg { width: 18px; height: 18px; }
  .dropbar .db-info { flex: 1; min-width: 0; }
  .dropbar .db-title { font-weight: 700; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dropbar .db-sub { color: var(--muted); font-size: 12px; margin-top: 3px;
    display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  .dropbar .db-acts { display: flex; gap: 6px; flex: none; }
  /* The tab-bound warning is the one thing that must not read as decoration —
     it's the whole difference between this and every row below it. */
  .dropbar .db-warn { color: var(--accent); font-weight: 700; }
  .dropbar .db-err { color: var(--err); flex: 1 1 100%; overflow-wrap: anywhere; }
  .dropbar .prog { margin: 0 0 12px; }
  .dropbar[data-sstate="idle"] .prog { display: none; }
  @media (max-width: 640px) {
    .dropbar .db-row { flex-wrap: wrap; }
    .dropbar .db-acts { width: 100%; }
    .dropbar .db-acts button { flex: 1; }
  }

  /* mobile: rail becomes a horizontal top tab bar */
  @media (max-width: 640px) {
    .wrap { padding: 18px 12px 48px; }
    /* Stretch beats the desktop flex-start here: in column direction
       flex-start makes .view fit-content, so one long filename widens the
       whole dashboard past the viewport instead of ellipsizing. */
    .layout { flex-direction: column; gap: 14px; align-items: stretch; }
    /* Tabs pin right under the stuck topbar; solid bg so cards slide beneath. */
    .rail { flex-direction: row; width: 100%; overflow-x: auto;
      position: sticky; top: 44px; z-index: 20; background: var(--bg); padding-bottom: 8px; }
    .tab { flex: 1; justify-content: center; white-space: nowrap; }
    .tab .ext { display: none; }
    /* The rail is a row here, so a 1px-tall rule would just be a sliver. */
    .rail-sep { display: none; }
    /* Back is an affordance, not a section — don't let it eat an equal share. */
    .tab.back { flex: none; }
    /* Top nav: tighten spacing on phones (the name/email text is already gone
       by here — see the 760px rule above). */
    .topbar { gap: 8px; margin-bottom: 18px; }
    /* Keep the send chooser inside the viewport. */
    dialog#sendModal, dialog#betaModal { width: min(360px, calc(100vw - 24px)); }
  }

  /* Narrow screens: stack each transfer card so the actions get a full-width
     touch row instead of squeezing the filename. Kicks in well above the
     phone breakpoint because the 641-860px band (landscape phones, the rail
     still beside the list) suffers the same squeeze. */
  @media (max-width: 860px) {
    /* The bottom row splits back in two — bar above, buttons below — because
       full-width touch targets beat a bar squeezed to a stub. Still the same
       rows on every card, so still one uniform height. */
    .xfer { grid-template-columns: minmax(0, 1fr); gap: 8px 0;
      grid-template-areas: "fn" "meta" "slot" "acts"; }
    /* Real touch targets: ONE row of equal-width buttons, never two — a
       wrapped second row would make a three-op card taller than a two-op one,
       and the uniform height is the whole contract. Labels ellipsize before
       the row grows. Ghosts get their border back so they read as buttons
       rather than stray labels. */
    .xfer .acts button { flex: 1 1 0; min-width: 0; min-height: 40px;
      padding-left: 8px; padding-right: 8px;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .xfer .acts button.ghost { border-color: var(--line); }
  }

  .pill { font-size: 10px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; padding: 2px 8px; border-radius: 20px; background: var(--card2); border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }
  .pill.active, .pill.connecting { color: var(--accent); border-color: var(--accent); background: var(--accent-tint); }
  .pill.completed { color: var(--ok); border-color: var(--ok); background: transparent; }
  .pill.failed { color: var(--err); border-color: var(--err); background: transparent; }
  .role { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--faint); }

  .empty { color: var(--muted); font-size: 13px; padding: 16px 18px; }

  /* Machines online but nothing moving: the desktop app's ASCII mailman, in the
     pose he wears while a transfer waits for a peer — stopped on the route, cap
     swinging as he looks around. Same figure, same quiet ink, but this is a
     whole empty pane rather than a card's spare 37px, so he's drawn at hero
     size on the dotted horizon instead of the 10px the row allows.
     He's decoration on top of copy that already says it: aria-hidden, no hover,
     no clicks. */
  .empty-scene { text-align: center; padding: 44px 18px 40px; color: var(--muted); }
  .empty-scene .route { position: relative; width: 220px; height: 92px; margin: 0 auto 6px; }
  .empty-scene .ground { position: absolute; left: 0; right: 0; bottom: 8px; border-bottom: 2px dotted var(--line2); }
  /* The two poses are stacked, not swapped: both frames are in the DOM and the
     animation cross-cuts their opacity, so nothing reflows on the beat and the
     figure can't jitter a subpixel between frames. Both are absolutely placed
     from the same origin, and every frame line is padded to one width, so the
     glyphs that DON'T move (head, torso, legs) land on the same pixel. */
  .empty-scene .mm { position: absolute; left: 0; right: 0; bottom: 12px; height: 78px;
    font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
    font-size: 24px; line-height: 26px; font-weight: 600; letter-spacing: 0;
    white-space: pre; color: var(--faint); user-select: none; pointer-events: none; }
  .empty-scene .mm i { position: absolute; left: 0; right: 0; top: 0; font-style: normal;
    animation: mm-frame 1.6s linear infinite; }
  /* NEGATIVE delay: the second frame starts half a cycle in, already running.
     A positive one would leave it un-animated — and so at its base opacity of
     1 — for the first 800ms, showing both poses stacked on load. */
  .empty-scene .mm i + i { animation-delay: -.8s; }
  /* 800ms a frame — the card's search cadence. Slow enough to read as looking
     around rather than a flicker. The near-duplicate stops make the switch a
     0.16ms ramp, i.e. a hard cut; a step timing function is deliberately NOT
     used, since it holds the FROM value across the swap and both frames land
     on screen together on the exact half-cycle. */
  @keyframes mm-frame { 0%, 49.99% { opacity: 1; } 50%, 100% { opacity: 0; } }
  /* Reduced motion holds the first frame, exactly as the card's ticker does. */
  @media (prefers-reduced-motion: reduce) {
    .empty-scene .mm i { animation: none; }
    .empty-scene .mm i + i { display: none; }
  }
  .empty-scene .et { color: var(--text); font-size: 15px; font-weight: 700; margin-bottom: 4px; }
  .empty-scene .es { font-size: 13px; color: var(--muted); max-width: 340px; margin: 0 auto; }
  .toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(20px);
    background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 10px;
    /* Matches the desktop toast. The cap matters more here than in the app:
       with no max-width a long notice ran as one line the full width of a
       desktop viewport, and on a phone it filled the screen edge to edge. The
       percentage keeps it inset on narrow screens, the ceiling keeps the line
       length readable on wide ones, and 1.5 leading makes a wrapped message
       legible. */
    padding: 12px 18px; font-size: 13.5px; line-height: 1.5; max-width: min(92%, 420px);
    text-align: center; text-wrap: pretty;
    box-shadow: 0 12px 30px rgba(0,0,0,.4); opacity: 0; pointer-events: none;
    transition: opacity .2s, transform .2s; }
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
