        :root {
            /* spacing — 4px scale */
            --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
            --s5: 24px; --s6: 32px; --s7: 48px;

            /* type — six steps, replacing fifteen ad-hoc sizes */
            --t-xs: 11px; --t-sm: 12px; --t-md: 13px;
            --t-lg: 15px; --t-xl: 18px; --t-2xl: 24px;

            --r-sm: 6px; --r-md: 10px; --r-lg: 14px;

            /* neutral ramp */
            --n0: #ffffff; --n1: #f7f8fa; --n2: #eef1f5; --n3: #e6e9ee;
            --n4: #c3cad4; --n5: #646e7b; --n6: #5a6676; --n7: #35455a; --n8: #0f1a28;

            /* semantic */
            /* --brand is the identity colour and is fill-only: white on it is 2.53:1, below
               AA, so text-bearing surfaces use --brand-dark (5.01:1) and hover goes darker. */
            --brand: #17b890; --brand-dark: #0e7e66; --brand-darker: #0b6553;
            --brand-wash: #e8f7f2;
            /* Label colour on the brand-dark button fill. White suits the green and every dark
               theme pick; the theme partial flips it to black for a light custom colour. */
            --brand-ink: #fff;
            --ok: #177c4e;    --ok-wash: #e3f5ec;
            --warn: #916524;  --warn-wash: #fdf2dd;
            --danger: #c0392b; --danger-wash: #fbe9e7;

            --e1: 0 1px 2px rgba(16,26,40,.05);
            --e2: 0 4px 16px rgba(16,26,40,.08);

            --sidebar-w: 220px; --sidebar-collapsed: 56px; --topbar-h: 56px;

            /* The old variable names, kept at their ORIGINAL literal values rather than
               re-pointed at the ramp above. Re-pointing them silently restyles every
               existing rule at once, which is exactly what the foundation must not do:
               this plan migrates views one at a time, and that only works if the base
               moves nothing. (Measured: --muted re-pointed at #8a939f drops secondary
               text from 4.60:1 to 3.11:1 on white — below WCAG AA, and no test can see
               it.) Each alias is deleted once its last consuming view has migrated. */
            --bg: #f4f6f9; --card: #ffffff; --line: #e8ebef;
            --ink: #101a28; --muted: #646e7b;
            --amber: #e08a1e; --grey: #9aa3af; --red: #e0524a;
            --radius: 14px;
            --shadow-sm: 0 1px 2px rgba(16,26,40,.04), 0 1px 3px rgba(16,26,40,.06);
        }
        * { box-sizing: border-box; }
        /* A class selector with an explicit `display` beats the UA's [hidden] rule, so `hidden`
           silently stops working. Every `hidden` in these views relies on this. */
        [hidden] { display: none !important; }
        body { margin: 0; color: var(--ink); -webkit-font-smoothing: antialiased;
               background: linear-gradient(180deg, #eef2f6 0%, var(--bg) 240px);
               font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; }

        .topbar { position: sticky; top: 0; z-index: 30;
                  background: rgba(255,255,255,.82); backdrop-filter: saturate(180%) blur(12px);
                  border-bottom: 1px solid var(--line); padding: 0 24px;
                  height: 60px; display: flex; align-items: center; justify-content: space-between; }
        .brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; }
        main { max-width: 1520px; margin: 0 auto; padding: 20px 24px 60px; }

        .toolbar { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; margin-bottom: var(--s4); }
        .search { display: flex; align-items: center; background: var(--n0); border: 1px solid var(--n3);
                  border-radius: var(--r-sm); padding: 0 var(--s3); height: 36px; width: 320px; max-width: 100%; box-shadow: var(--e1);
                  transition: border-color .15s, box-shadow .15s; }
        .search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent); }
        .search input { border: 0; outline: 0; font: inherit; padding: 0 8px; flex: 1; background: transparent; }
        /* A flex child keeps its intrinsic width unless told otherwise; without this the
           search input spills out of its own box and the field becomes untypeable. */
        .search input { min-width: 0; width: 100%; }
        .search svg { color: var(--muted); flex: none; }
        .search .searchicon { display: inline-flex; color: var(--muted); }
        .search .searchicon svg { width: 18px; height: 18px; }
        select { height: 40px; border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
                 font: inherit; padding: 0 10px; color: var(--ink); }
        .legend { display: flex; gap: 18px; margin-left: auto; font-size: 12px; letter-spacing: .04em;
                  color: var(--muted); text-transform: uppercase; }
        .legend span { display: flex; align-items: center; gap: 6px; }



        .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(430px, 1fr)); gap: var(--s4); }
        .card { background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-md);
                overflow: hidden; box-shadow: var(--e1); transition: box-shadow .18s, transform .18s; }
        .card:hover { box-shadow: var(--e2); transform: translateY(-2px); }
        .cardhead { display: flex; gap: var(--s4); padding: var(--s4) var(--s4) var(--s3); }
        /* NOT a grid. `place-items: center` made the row auto-size to the image, so the img's
           `max-height: 100%` resolved against the ROW (109.6px for a 91x172 render) instead of
           this box's 80px, and any photo taller than 4:3 escaped — pushing one card's header
           30px out of line with its neighbours. An explicit box plus object-fit cannot overflow. */
        .devthumb { width: 58px; height: 80px; flex: none; }
        .devthumb img { width: 100%; height: 100%; object-fit: contain; display: block;
                        filter: drop-shadow(0 3px 7px rgba(16,26,40,.18)); }
        .cardmain { flex: 1; min-width: 0; }
        .cardtop { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); }
        .cardname { font-size: var(--t-lg); font-weight: 700; color: var(--n8); text-decoration: none;
                    letter-spacing: -.2px; }
        .cardname:hover { color: var(--brand-dark); }
        .cardmeta { color: var(--n5); font-size: var(--t-sm); margin-top: var(--s1); }
        .cardflags { display: flex; align-items: center; gap: var(--s2) var(--s3); margin-top: var(--s3); flex-wrap: wrap; }
        .flag { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: #566175; font-weight: 500; }
        /* A device fingerprint is one 32-character unbreakable token, so flex-wrap cannot help
           it: at 320px it ran 17px past the card's edge and the last character was clipped.
           min-width:0 lets the flex item shrink below its content; anywhere does the wrapping. */
        .idflag { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; color: var(--muted);
                  background: #f4f6f8; padding: 2px 8px; border-radius: 6px;
                  min-width: 0; overflow-wrap: anywhere; }

        /* A status BADGE, not a button — so it never reads as an action next to the Connect link. */
        .status { border: 0; border-radius: 20px; padding: 4px 11px 4px 9px; font: inherit;
                  font-weight: 600; font-size: 12px; cursor: default; white-space: nowrap;
                  display: inline-flex; align-items: center; gap: 6px; }
        .status::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
        .status.available { background: var(--ok-wash); color: var(--ok); }
        .status.diag { background: var(--warn-wash); color: var(--warn); }
        .status.off { background: var(--n2); color: var(--n6); }

        .chip { background: #f1f3f6; border-radius: 5px; padding: 3px 8px; font-size: 11.5px; color: #4a5566; }

        .actions { border-top: 1px solid var(--n2); padding: var(--s3) var(--s4); display: flex; gap: var(--s2); flex-wrap: wrap; }
        .cardfoot { border-top: 1px solid var(--n2); padding: var(--s3) var(--s4); }
        .cardfoot .btn-primary { display: block; text-align: center; }

        .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; }
        .dot.on { background: var(--brand); } .dot.warn { background: var(--amber); } .dot.off { background: var(--grey); }

        .diag { padding: 0 16px 12px; font-size: 12px; color: var(--muted); }
        .diag strong { color: #2b3a4d; }
        .diag em { color: var(--warn); font-style: normal; font-weight: 600; }
        /* #77818f was 3.51:1 on the page. It escaped the rendered-contrast sweep because the
           span is empty until JS fills it, so there was no text to measure. */
        .meta { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
                color: var(--n5); margin-top: 3px; }

        .out { padding: 0 16px 12px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: #374151; }
        .side .out { padding: 0; min-height: 16px; }
        .out img { max-width: 100%; border: 1px solid var(--line); border-radius: 8px; display: block; margin-top: 8px; }
        /* Colour only, deliberately. A review claimed onboarding's two COMPANION_AGENT_*
           warnings had lost their size and margin in the stylesheet move; they had not —
           both sit inside `.wizsteps` (onboarding.blade.php:37-180), so `.wizsteps .err`
           covers them. Adding size and margin here instead shrank the page-level error
           banners on /users and /pending from 14px to 12px, which is the only thing it did. */
        .err { color: var(--danger); }

        .live { padding: 0 16px 14px; }
        .live img { width: 100%; max-width: 280px; border: 1px solid var(--line); border-radius: 8px;
                    display: block; background: #0f1115; }
        .livebar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 6; margin: 0;
            display: flex; align-items: center; gap: 12px; padding: 26px 14px 12px;
            background: linear-gradient(180deg, rgba(8,10,14,0), rgba(8,10,14,.78)); }
        .livecount { font-family: ui-monospace, Menlo, monospace; font-size: 12px; font-weight: 700;
            color: #eef2f6; min-width: 2ch; text-align: right; text-shadow: 0 1px 2px rgba(0,0,0,.55); }
        .livebar input[type=range] { flex: 1; accent-color: var(--brand); }
        .livejump { flex: none; border: 1px solid var(--line); background: #fff; color: var(--muted);
            font: inherit; font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
            border-radius: 999px; padding: 3px 10px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
        .livejump::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--grey); }
        .livejump.atlive { color: var(--brand-dark); border-color: var(--brand); background: #eafaf4; }
        .livejump.atlive::before { background: var(--red); box-shadow: 0 0 0 3px rgba(224,82,74,.18); }

        /* Named .notice, not .warn: `.dot.warn` also exists, and a bare `.warn` rule's padding
           clamped those 9px dots to 30x26 under box-sizing: border-box. */
        .notice { background: #fff8ed; border: 1px solid #fcd9a4; color: #92500e; padding: 12px 14px;
                  border-radius: var(--radius); margin-bottom: 16px; }
        .empty { background: #fff; border: 1px dashed var(--line); border-radius: var(--radius);
                 padding: 40px; text-align: center; color: var(--muted); }
        .emptystate { background: var(--card); border: 1px solid var(--line); border-radius: 18px;
            box-shadow: var(--shadow-sm); padding: 44px 28px; text-align: center; max-width: 460px; margin: 24px auto; }
        .emptystate img { width: 64px; height: 64px; margin: 0 auto 14px; display: block;
            filter: drop-shadow(0 6px 16px rgba(89,70,64,.22)); }
        .emptystate h2 { margin: 0 0 6px; font-size: 18px; color: var(--ink); }
        .emptystate p { margin: 0 0 20px; color: var(--muted); font-size: 13.5px; line-height: 1.6; }

        .stage { display: grid; grid-template-columns: minmax(280px, 420px) 64px 1fr; gap: 18px; align-items: start; }
        /* Messages & calls: a full-width card below the device stage, not a .stage grid child. */
        .msgcalls { background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-md);
                    padding: var(--s4); margin-top: var(--s4); }
        .msgcalls h3 { margin: 0 0 var(--s3); font-size: var(--t-lg); color: var(--n8); }
        .msgbtns { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s3); }
        .keepcopy { display: inline-flex; align-items: center; gap: var(--s2); font-size: var(--t-sm); color: var(--n6); }
        .listresult { margin-top: var(--s4); overflow-x: auto; }

        .screen { background: #0e1013; border: 1px solid #1d2229; border-radius: 14px; min-height: 620px;
                  display: grid; place-items: center; overflow: hidden; }
        .screen img { max-width: 100%; max-height: 700px; display: block; }
        .screen .placeholder { color: #939cab; font-size: 13px; }
        .screen { position: relative; }
        .screen.scanning::after { content: ''; position: absolute; left: 0; right: 0; height: 45%;
            background: linear-gradient(180deg, rgba(31,199,156,0), rgba(31,199,156,.18) 78%, rgba(31,199,156,.55));
            box-shadow: 0 2px 10px rgba(31,199,156,.5); animation: scandown 1.15s cubic-bezier(.4,0,.2,1) infinite;
            pointer-events: none; z-index: 4; top: -45%; }
        @keyframes scandown { 0% { top: -45%; } 100% { top: 100%; } }
        .scanicon { position: absolute; inset: 0; display: none; place-items: center; z-index: 5;
            color: #37e3b4; pointer-events: none; }
        .screen.scanning .scanicon { display: grid; }
        .scanicon img { width: 208px; height: 208px; filter: drop-shadow(0 2px 10px rgba(64,232,187,.55)); }

        .rail { display: flex; flex-direction: column; gap: 12px; align-items: center;
                background: transparent; border: 0; padding: 4px 0; }
        button.icon { width: 42px; height: 42px; border-radius: 50%; border: 1px solid var(--line);
                      background: #fff; color: #35455a; display: grid; place-items: center; cursor: pointer;
                      padding: 0; box-shadow: var(--shadow-sm); transition: background .15s, box-shadow .15s, transform .1s; }
        button.icon:active { transform: scale(.94); }
        button.icon svg { width: 19px; height: 19px; }
        button.icon:hover { background: #eef1f5; }
        button.icon.on { background: var(--brand-dark); border-color: var(--brand-dark); color: var(--brand-ink); }
        button.icon.rec { background: #c0392b; border-color: #c0392b; color: #fff; }
        button.icon.busy { pointer-events: none; }
        button.icon.busy svg { display: none; }
        button.icon.busy::after { content: ''; width: 16px; height: 16px; border-radius: 50%;
            border: 2px solid rgba(53,69,90,.25); border-top-color: var(--brand); animation: spin .6s linear infinite; }
        @keyframes spin { to { transform: rotate(360deg); } }

        .facts { margin-top: 0; border: 1px solid var(--line); border-radius: var(--radius); background: #fff; }
        .facts div { display: flex; justify-content: space-between; gap: 12px; padding: 9px 14px;
                     border-bottom: 1px solid #f1f3f6; font-size: 13px; }
        .facts div:last-child { border-bottom: 0; }
        .facts span { color: var(--muted); }


        .screen video { max-width: 100%; max-height: 700px; display: block; background: #000; }
        .player { margin-bottom: 16px; }
        .player video { width: 100%; max-height: 420px; display: block; background: #000;
            border-radius: 8px; border: 1px solid #1d2229; }
        .playerbar { display: flex; justify-content: space-between; align-items: center; gap: 12px;
            padding: 8px 2px 0; }
        .playerbar strong { font-size: 13px; color: #1f2d3d; }
        .playerbar button { border: 1px solid var(--line); background: #fff; border-radius: 6px;
            padding: 4px 10px; font-size: 12px; cursor: pointer; color: var(--muted); }
        .playerbar button:hover { background: #eef1f5; }
        .vidlist { display: grid; gap: 12px; }
        .vid { display: flex; gap: 12px; cursor: pointer; align-items: stretch; }
        .vidbody { flex: 1 1 auto; min-width: 0; display: flex; gap: 12px; align-items: center; }
        .viddl { flex: 0 0 30%; display: flex; flex-direction: column; align-items: center; justify-content: center;
            gap: 5px; text-decoration: none; border: 1px solid var(--line); border-radius: 10px;
            background: #f0faf5; color: var(--brand-dark); font-size: 11px; font-weight: 700;
            text-transform: uppercase; letter-spacing: .03em; }
        .viddl:hover { background: #d7f3e8; border-color: var(--brand); }
        .viddl svg { width: 20px; height: 20px; }
        .vid:hover .thumb { border-color: var(--brand); }
        .thumb { position: relative; width: 116px; height: 70px; flex: none; border: 1px solid var(--line);
                 border-radius: 8px; overflow: hidden; background: #0e1013; }
        .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
        .playbadge { position: absolute; inset: 0; display: grid; place-items: center; color: #fff;
                     font-size: 20px; text-shadow: 0 1px 4px rgba(0,0,0,.6); }
        .dur { position: absolute; right: 4px; bottom: 4px; background: rgba(0,0,0,.78); color: #fff;
               font-size: 10.5px; padding: 1px 5px; border-radius: 3px;
               font-family: ui-monospace, Menlo, monospace; }
        .vidmeta { display: flex; flex-direction: column; gap: 2px; }
        .vidmeta strong { font-size: 13.5px; color: #1f2d3d; }
        .vidmeta span { font-size: 12px; color: var(--muted); }

        .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 14px; }
        .tab { border: 0; background: none; font: inherit; font-size: 13px; font-weight: 600; color: var(--muted); white-space: nowrap;
               padding: 9px 12px; cursor: pointer; border-bottom: 2px solid transparent; }
        .tab.on { color: var(--brand-darker); border-bottom-color: var(--brand); }
        .tab .count { background: #eef1f5; border-radius: 10px; padding: 1px 7px; font-size: 11px; color: #5b6676; }
        .tab.on .count { background: #e3f5f0; color: var(--brand-dark); }
        .tabpane { display: block; }
        .tabpane[data-pane=screenshots] { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
        .shot { margin: 0; }
        .shot img { width: 100%; border: 1px solid var(--line); border-radius: 8px; display: block; background: #0e1013; }
        .shot figcaption { font-size: 11px; color: var(--muted); margin-top: 4px; display: flex;
                           justify-content: space-between; align-items: center; font-family: ui-monospace, Menlo, monospace; }
        .dl { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 7px;
              color: var(--brand-dark); background: #eafaf4; text-decoration: none; flex: none; }
        .dl:hover { background: #d7f3e8; }
        .dl svg { width: 15px; height: 15px; }
        .row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 2px;
               border-bottom: 1px solid #f1f3f6; font-size: 13px; }
        .row span { color: var(--muted); font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
        .row a { color: var(--brand-darker); text-decoration: none; font-weight: 600; }
        .none { color: var(--muted); font-size: 13px; padding: 14px 2px; }
        .snaprow { display: flex; justify-content: space-between; gap: 12px; padding: 9px 2px;
                   border-bottom: 1px solid #f1f3f6; font-size: 13px; text-decoration: none;
                   color: var(--brand-darker); font-weight: 600; }
        .snaprow span:first-child { color: var(--muted); font-family: ui-monospace, Menlo, monospace; font-size: 12px; font-weight: 400; }
        .snaprow .sub { color: var(--brand-darker); }

        table.list { width: 100%; border-collapse: collapse; background: var(--n0); border: 1px solid var(--line);
                     border-radius: var(--r-lg); overflow: hidden; }
        table.list th, table.list td { text-align: left; padding: var(--s3) var(--s4); border-bottom: 1px solid var(--n2); }
        /* A long unbroken value — an email address, typically — otherwise forces the table
           wider than the viewport and drags every other cell off-screen. Applies at all
           widths: the phone card layout and the real table at tablet widths both need it. */
        table.list td { overflow-wrap: anywhere; }
        table.list th { font-size: var(--t-sm); text-transform: uppercase; letter-spacing: .04em; color: var(--n6);
                        font-weight: 600; background: var(--n1); }
        .acts { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; }
        .acts label { display: flex; align-items: center; gap: 5px; font-size: 12.5px; color: #35455a; }
        table.list .sub { font-size: var(--t-sm); color: var(--n6); }
        table.list tbody.userblock { border-bottom: 2px solid var(--n3); }
        /* Presence. The element is always in the markup and always this size, so the
           addresses line up whether or not anybody is signed in; only the filled one is
           drawn. The halo is what makes an 8px dot read as "live" rather than as a
           bullet, and it is drawn with a shadow so it costs the row no width. */
        .uwho { display: flex; align-items: flex-start; gap: 8px; }
        /* Aligned to the FIRST line rather than centred: a wrapped address is two lines
           tall and a dot floating halfway down it reads as belonging to neither. */
        .udot { flex: none; width: 8px; height: 8px; border-radius: 50%;
                margin-top: 6px; background: transparent; }
        .uemail { min-width: 0; overflow-wrap: anywhere; }
        .udot-on { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 20%, transparent); }
        .quickdates { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
        .quickdates .btn { padding: 6px 10px; font-size: 12px; }
        .status.unlimited { background: var(--n2); color: var(--n6); }
        .status.trial { background: var(--warn-wash); color: var(--warn); }
        /* An account whose trial has not begun: it owns no device yet, so there is nothing
           counting down. Neutral like 'unlimited' rather than warning like 'trial' — nobody
           needs to act on it. */
        .status.trial_pending { background: var(--n2); color: var(--n6); }
        .status.subscribed { background: var(--ok-wash); color: var(--ok); }
        .status.expired { background: var(--danger-wash); color: var(--danger); }

        .panelform { background: var(--n0); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--s5); }
        /* Add-user form sits below the users table in normal flow, capped so it does not stretch. */
        main > aside { margin-top: var(--s5); max-width: 420px; }
        .panelform h3 { margin: 0 0 var(--s3); font-size: var(--t-lg); }
        .panelform label { display: block; font-size: var(--t-sm); color: var(--n6); margin-top: var(--s3); }
        .panelform input { width: 100%; padding: var(--s2) var(--s3); border: 1px solid var(--n4); border-radius: var(--r-sm);
                           font: inherit; margin-top: 4px; }
        .panelform button { margin-top: 16px; width: 100%; justify-content: center; }

        .loginwrap { min-height: 88vh; display: flex; flex-direction: column; align-items: center;
                     justify-content: center; gap: 18px; padding: 40px 16px; }
        .logincard { background: var(--n0); width: 100%; max-width: 400px; border: 1px solid var(--n3);
                     border-radius: var(--r-lg); padding: 36px 32px; box-shadow: var(--e2); }
        /* /subscribe stacks its blocks and wants full-width actions, so the view needs no
           inline style attributes of its own. */
        .subcard > * + * { margin-top: 18px; }
        .subcard .btn { display: block; width: 100%; text-align: center; justify-content: center; }
        /* Sign out is an icon here, not a full-width action — after the rule above so it wins. */
        .subcard .signoutform { display: flex; justify-content: center; }
        .subcard .signouticon { display: inline-flex; width: 44px; height: 44px; min-height: 44px;
                                padding: 0; border-radius: 50%; align-items: center;
                                justify-content: center; color: var(--n6); }
        .subcard .signouticon svg { width: 20px; height: 20px; }
        .submsg { white-space: pre-line; }
        .loginbrand { text-align: center; margin-bottom: 24px; }
        .loginlogo { width: 72px; height: 72px; margin: 0 auto 12px; display: block;
                     filter: drop-shadow(0 6px 16px rgba(16,26,40,.2)); }
        .loginbrand h1 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -.5px; color: var(--n8); }
        .loginsub { margin: 6px 0 0; color: var(--n5); font-size: 13.5px; }
        .logincard label { display: block; font-size: 12.5px; font-weight: 600; color: var(--n7);
                           margin: 14px 0 6px; }
        .logincard input { width: 100%; padding: 12px 14px; border: 1px solid var(--n3);
                           border-radius: 11px; font: inherit; background: var(--n1); transition: border-color .15s, box-shadow .15s; }
        .logincard input:focus { outline: 0; border-color: var(--brand); background: var(--n0);
                                 box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent); }
        /* The gradient this replaces ran #1fc79c to #12a483: white on those is 2.16:1 and
           3.15:1, both under AA. It was the last white-on-light-teal surface left after
           .btn-primary moved to --brand-dark, and it sits on the sign-up page. */
        .logincard button { width: 100%; margin-top: 22px; height: 46px; border: 0; border-radius: 12px;
                            background: linear-gradient(135deg, var(--brand-dark), var(--brand-darker)); color: var(--brand-ink);
                            font: inherit; font-weight: 700; font-size: 14.5px; cursor: pointer;
                            box-shadow: 0 6px 18px color-mix(in srgb, var(--brand) 35%, transparent); transition: transform .1s, box-shadow .15s; }
        .logincard button:hover { transform: translateY(-1px); box-shadow: 0 8px 22px color-mix(in srgb, var(--brand) 45%, transparent); }
        .logincard .err { color: var(--danger); font-size: 12.5px; margin: 0 0 8px; }
        .loginfoot { color: var(--n5); font-size: 12px; letter-spacing: .02em; }
        /* The other auth page's action, as an equal-size button rather than a text link:
           same 100% width / 46px height / 12px radius as the primary submit, outlined
           instead of filled (Google/Facebook pattern — both buttons one size). */
        .loginor { display: flex; align-items: center; gap: 12px; margin-top: 18px; color: var(--n5); font-size: 12px; }
        .loginor::before, .loginor::after { content: ""; flex: 1; height: 1px; background: var(--n3); }
        .altbtn { display: flex; align-items: center; justify-content: center; width: 100%; height: 46px; margin-top: 14px;
                  border: 1.5px solid var(--n4); border-radius: 12px; background: var(--n0); color: var(--brand-darker);
                  font: inherit; font-weight: 700; font-size: 14.5px; text-decoration: none;
                  transition: border-color .15s, background .15s; }
        .altbtn:hover { border-color: var(--brand); background: var(--n1); }
        .permwarn { border: 0; background: #fbe9e7; color: #c0392b; border-radius: 6px; width: 26px;
            height: 26px; font-size: 15px; cursor: pointer; margin-left: 6px; line-height: 1; }
        .permwarn { display: inline-grid; place-items: center; }
        .permwarn svg { width: 15px; height: 15px; }
        .permwarn:hover { background: #f5c6bd; }
        .permmodal { position: fixed; inset: 0; background: rgba(15,20,25,.55); display: flex;
            align-items: center; justify-content: center; z-index: 50; }
        .permbox { background: #fff; border-radius: 12px; padding: 24px; max-width: 460px; width: 90%;
            box-shadow: 0 20px 60px rgba(0,0,0,.3); }
        .permbox h3 { margin: 0 0 8px; font-size: 17px; color: #c0392b; }
        .permlead { color: var(--muted); font-size: 13px; margin: 0 0 14px; }
        .permbox ul { list-style: none; padding: 0; margin: 0 0 18px; display: grid; gap: 12px; }
        .permbox li { border-left: 3px solid #f0a999; padding: 8px 12px; background: #fdf3f1; border-radius: 6px; }
        .permbox li strong { color: #1f2d3d; font-size: 13.5px; }
        .permbox li span { color: var(--muted); font-size: 12.5px; }

        .pagehead { display: flex; align-items: flex-end; gap: var(--s4);
                    margin-bottom: var(--s4); flex-wrap: wrap; }
        .pagehead h1 { margin: 0; font-size: var(--t-xl); font-weight: 700;
                       letter-spacing: -.3px; color: var(--n8); }
        .pagehead p { margin: var(--s1) 0 0; font-size: var(--t-sm); color: var(--n5); }
        .pagehead-actions { margin-left: auto; display: flex; gap: var(--s2); flex-wrap: wrap; }
        @media (max-width: 720px) {
            .pagehead-actions { margin-left: 0; width: 100%; }
        }

        /* ---- Tablets and landscape phones ---- */
        /* This band had no rules at all, so a ~840px landscape phone was being served the
           full desktop stage: a 280-420px screen, a 64px rail and a 1fr sidebar. */
        @media (max-width: 1024px) {
            main { padding: 18px 16px 52px; }
            .stage { grid-template-columns: minmax(280px, 520px) auto; justify-content: start;
                     gap: 14px; }
            .stage .side { grid-column: 1 / -1; }
            .grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
        }

        /* ---- Mobile / small screens ---- */
        @media (max-width: 720px) {
            .topbar { gap: 6px 10px; }
            .brand { font-size: 16px; }
            main { padding: 14px 12px 48px; }

            .toolbar { gap: 10px; }
            /* search + sort share one row; the legend drops to its own line below */
            .search { min-width: 0; flex: 1 1 140px; width: auto; }
            #sort { flex: 0 1 auto; max-width: 45%; min-width: 0; }
            .legend { margin-left: 0; width: 100%; order: 3; flex-wrap: wrap; gap: 10px 16px; }

            /* device cards: one per row instead of a 430px-min grid that overflows */
            .grid { grid-template-columns: 1fr; }
            .cardhead { gap: 12px; }
            .devthumb { width: 54px; height: 70px; }

            /* connect page: keep the screen + the vertical icon rail side by side (same column of
               actions as the web view); drop the tabs/info to full width on the next row. */
            /* A floor-free screen column: the tablet tier's 280px minimum is a hard
               minimum, and on a 320px phone it overflows once the rail and gap are added. */
            .stage { grid-template-columns: minmax(0, 1fr) auto; gap: 12px; }
            .screen { min-height: 300px; }
            .tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; }
            .live img, .livebar { max-width: 100%; }

            /* users page: stack the table + side form; let the table scroll rather than overflow the page */
            /* One row becomes one labelled card. Replaces a sideways scroll that held a
               280px form inside a nowrap container — unusable with a thumb. */
            table.list { border: 0; background: transparent; }
            table.list thead { display: none; }
            table.list tbody { display: block; background: #fff; border: 1px solid var(--line);
                               border-radius: var(--radius); margin-bottom: 10px;
                               box-shadow: var(--shadow-sm); overflow: hidden; }
            table.list tr { display: block; }
            table.list td { display: flex; justify-content: space-between; align-items: center;
                            gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f3f6;
                            flex-wrap: wrap; }
            table.list td::before { content: attr(data-label); color: var(--muted);
                                    font-size: 12px; font-weight: 600; flex: none; }
            /* A cell's secondary line drops to its own full-width row rather than competing
               with the value for horizontal space. */
            table.list td .sub { flex-basis: 100%; text-align: right; }
            table.list tbody.userblock { border-bottom: 1px solid var(--line); }

            /* Thumb-sized: 26px squares and 150px-capped inputs are fine with a mouse
               and fiddly with a finger. */
            .permwarn, .dl { width: 40px; height: 40px; }
            .permwarn svg, .dl svg { width: 18px; height: 18px; }
            .livejump { padding: 7px 14px; }
            button.icon { width: 44px; height: 44px; }
            .rail { gap: 8px; }

            /* onboarding: stack the step card, shot first; stack the add-device form */
            .stepcard { grid-template-columns: 1fr; padding: var(--s4); }
            .shotslot { min-height: 0; order: -1; }
            .wizprog span:not(.wizdot) { display: none; }
            .addform { flex-direction: column; }
            .addform input, .codein, .addform button { width: 100%; }
        }

        @media (max-width: 400px) {
        }

/* ---- Shell ---- */
.app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
.sidebar { background: var(--n0); border-right: 1px solid var(--n3);
           padding: var(--s3) 0; display: flex; flex-direction: column; gap: 2px;
           position: sticky; top: 0; height: 100vh; overflow-y: auto; }
.sidebar .brand { display: flex; align-items: center; gap: var(--s2);
                  padding: var(--s1) var(--s4) var(--s4); text-decoration: none;
                  color: var(--n8); font-weight: 800; font-size: var(--t-md); }
.sidebar .brand img { width: 28px; height: 28px; }
.navgroup { font-size: 10px; text-transform: uppercase; letter-spacing: .09em;
            color: var(--n5); font-weight: 700; padding: var(--s4) var(--s4) var(--s1); }
.navitem { display: flex; align-items: center; gap: var(--s2); margin: 0 var(--s2);
           padding: var(--s2) var(--s3); border-radius: var(--r-sm);
           color: var(--n6); text-decoration: none; font-size: var(--t-md);
           font-weight: 500; min-height: 40px; }
.navitem svg { width: 17px; height: 17px; flex: none; opacity: .8; }
.navitem:hover { background: var(--n1); color: var(--n8); }
.navitem.on { background: var(--brand-wash); color: var(--brand-dark); font-weight: 650; }
.navitem.on svg { opacity: 1; }

.appmain { min-width: 0; display: flex; flex-direction: column; }
.topbar { height: var(--topbar-h); display: flex; align-items: center; gap: var(--s3);
          padding: 0 var(--s5); background: var(--n0); border-bottom: 1px solid var(--n3);
          position: sticky; top: 0; z-index: 20; }
.topslot { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s2); }
.topslot select { height: 36px; }  /* match .search, the other control in the bar */
.account { display: flex; align-items: center; gap: var(--s2); }
.account .email { font-size: var(--t-sm); color: var(--n5); }
.hamburger { display: none; flex-direction: column; gap: 4px; cursor: pointer;
             width: 40px; height: 40px; justify-content: center; align-items: center; }
.hamburger span { width: 18px; height: 2px; background: var(--n7); border-radius: 2px; }
.navtoggle, .navscrim { display: none; }
main { padding: var(--s5); max-width: 1520px; width: 100%; }

/* ---- Buttons ---- */
/* NOT element-qualified: .btn is worn by both <button> and <a>, and `button.btn` silently
   leaves every link unstyled. */
.btn { font: inherit; font-size: var(--t-sm); font-weight: 600; min-height: 36px;
       padding: var(--s2) var(--s3); border: 1px solid var(--n3); background: var(--n0);
       border-radius: var(--r-sm); cursor: pointer; color: var(--n7);
       display: inline-flex; align-items: center; gap: var(--s1); text-decoration: none; }
.btn:hover { background: var(--n1); }
/* Every .btn variant needs this: a disabled button that still looks enabled is a worse bug
   than an ugly one, and the rule it replaces was lost with .act. */
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-primary { background: var(--brand-dark); border-color: var(--brand-dark); color: var(--brand-ink); }
.btn-primary:hover { background: var(--brand-darker); border-color: var(--brand-darker); color: var(--brand-ink); }
.btn-danger { color: var(--danger); border-color: #f0c4c0; }
.btn-danger:hover { background: var(--danger-wash); }
.btn-ghost { border-color: transparent; background: transparent; }
.btn-ghost:hover { background: var(--n1); }
.btn svg { width: 17px; height: 17px; }

@media (max-width: 900px) {
    .app { grid-template-columns: 1fr; }
    .sidebar { position: fixed; top: 0; left: 0; bottom: 0; width: var(--sidebar-w);
               z-index: 40; transform: translateX(-100%); transition: transform .18s ease;
               box-shadow: var(--e2); height: 100dvh; visibility: hidden; }
    .navtoggle:checked ~ .app .sidebar { transform: none; visibility: visible; }
    .navtoggle:checked ~ .app .navscrim { display: block; position: fixed; inset: 0;
                                     background: rgba(15,26,40,.45); z-index: 35; }
    .hamburger { display: flex; }
    main { padding: var(--s4); }
    .topbar { padding: 0 var(--s4); }
    .account .email { display: none; }
}

/* ---- Device console ---- */
.devmap { height: 260px; border-radius: var(--r-sm); border: 1px solid var(--n3);
          margin-bottom: var(--s3); background: var(--n1); }
.mapnote { color: var(--n5); font-size: var(--t-sm); margin: 0 0 var(--s3); }
.mapnote.err { color: var(--warn); }

.crumb { display: flex; align-items: center; gap: var(--s1);
         font-size: var(--t-sm); color: var(--n5); }
.crumb a { color: var(--n6); text-decoration: none; font-weight: 600; }
.crumb a:hover { color: var(--brand-dark); }
.crumb svg { width: 14px; height: 14px; opacity: .6; }

@media (min-width: 901px) {
    .app.focused { grid-template-columns: var(--sidebar-collapsed) 1fr; }
    .app.focused .sidebar { overflow: visible; z-index: 30; }
    .app.focused .navitem span,
    .app.focused .brand span { display: none; }
    /* visibility, not display: toggling display makes the headings appear on hover and
       shoves every nav item down ~35px, so the target moves out from under the pointer. */
    .app.focused .navgroup { visibility: hidden; }
    .app.focused .navitem { justify-content: center; padding: var(--s2); }
    .app.focused .brand { justify-content: center; padding-left: 0; padding-right: 0; }
    /* Hover/focus widens the rail OVER the content. The negative margin keeps its grid
       footprint at 56px, so nothing reflows. (position:absolute would pull it out of
       the grid and the content would slide under it.) */
    .app.focused .sidebar:hover,
    .app.focused .sidebar:focus-within { width: var(--sidebar-w); box-shadow: var(--e2);
        margin-right: calc(var(--sidebar-collapsed) - var(--sidebar-w)); }
    .app.focused .sidebar:hover .navitem span,
    .app.focused .sidebar:focus-within .navitem span,
    .app.focused .sidebar:hover .brand span,
    .app.focused .sidebar:focus-within .brand span { display: inline; }
    .app.focused .sidebar:hover .navgroup,
    .app.focused .sidebar:focus-within .navgroup { visibility: visible; }
    .app.focused .sidebar:hover .navitem,
    .app.focused .sidebar:focus-within .navitem { justify-content: flex-start;
                                                  padding: var(--s2) var(--s3); }
    .app.focused .sidebar:hover .brand,
    .app.focused .sidebar:focus-within .brand { justify-content: flex-start;
                                                padding: var(--s1) var(--s4) var(--s4); }
}

/* ---- Form fields ---- */
.field { display: block; margin-top: var(--s3); }
.field > label { display: block; font-size: var(--t-sm); font-weight: 600;
                               color: var(--n7); margin-bottom: var(--s1); }
.input { display: block; width: 100%; font: inherit; font-size: var(--t-md); min-height: 36px;
         padding: var(--s2) var(--s3); border: 1px solid var(--n4); border-radius: var(--r-sm);
         background: var(--n0); color: var(--n8); }
.input:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); }
/* Buttons and nav items had a hover affordance but no focus one, so driving the panel by
   keyboard left you guessing where you were. :focus-visible, not :focus, or a mouse click
   leaves a ring behind on every button. */
.btn:focus-visible, .navitem:focus-visible, .hamburger:focus-visible, .tab:focus-visible,
.wizbtn:focus-visible, .apkbtn:focus-visible, button.icon:focus-visible, .row a:focus-visible,
.cardname:focus-visible, .crumb a:focus-visible {
    outline: 2px solid var(--brand-dark); outline-offset: 2px; }
.formcard { background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-lg);
            padding: var(--s5); max-width: 560px; }
.formcard h2 { margin: 0 0 var(--s2); font-size: var(--t-lg); color: var(--n8); }
/* A second section in one card ran straight into the field above it. Only later headings get
   the rule, or the card would open with a divider under nothing. */
.formcard h2 ~ h2 { margin-top: var(--s5); padding-top: var(--s5); border-top: 1px solid var(--n3); }
.formcard > p { margin: 0 0 var(--s2); font-size: var(--t-sm); color: var(--n6); }
.formcard .fielderr { color: var(--danger); font-size: var(--t-sm); margin: var(--s1) 0 0; }
.formcard .notice { margin-top: var(--s3); }
.formcard .btn { margin-top: var(--s4); }
/* A tick box and its words on one line. `.field > label` is the block, bold caption a text
   input wants above it, which reads as a heading when the control is a checkbox. */
.formcard .check { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s2);
                   font-size: var(--t-md); font-weight: 400; color: var(--n8); }
.formcard .check input { width: auto; min-height: 0; }
/* Secondary copy under a control: the same muted size as the card's own intro paragraph,
   which the contrast sweep already covers for --n6 on --n0. */
.formcard .hint { font-size: var(--t-sm); color: var(--n6); margin: var(--s1) 0 0; }
.formcard input[type=file].input { padding: var(--s2); line-height: 1.4; }

/* Settings: the default-actions grid dims while its "choose myself" toggle is off. */
.acts.is-off { opacity: .5; }

/* Settings: the logo upload — a preview beside its controls. */
.logorow { display: flex; gap: var(--s4); align-items: flex-start; }
.logofields { flex: 1; min-width: 0; }
.logofields > label { display: block; font-size: var(--t-sm); font-weight: 600;
                      color: var(--n7); margin-bottom: var(--s1); }
.logopreview { width: 64px; height: 64px; flex: none; object-fit: contain;
               border: 1px solid var(--n3); border-radius: var(--r-sm); background: var(--n1); padding: 6px; }

/* Settings: the Subscribe-button customiser and the free-trial pair. */
/* A group caption that reads like a .field label but sits over a control that isn't one input. */
.fieldcaption { display: block; font-size: var(--t-sm); font-weight: 600;
                color: var(--n7); margin-bottom: var(--s1); }
/* The icon chooser: radio tiles. The real radios are visually hidden; the label is the target. */
.iconpick { display: flex; flex-wrap: wrap; gap: var(--s2); }
.iconpick input[type=radio] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.iconpick-tile { display: inline-flex; align-items: center; justify-content: center;
                 min-width: 44px; min-height: 44px; padding: 0 var(--s2); border: 1px solid var(--n4);
                 border-radius: var(--r-sm); background: var(--n0); color: var(--n7); cursor: pointer; }
.iconpick-tile svg { width: 22px; height: 22px; }
.iconpick-none { font-size: var(--t-sm); font-weight: 600; }
.iconpick-tile.is-on { border-color: var(--brand-dark); background: var(--brand-wash); color: var(--brand-dark); }
/* The real control still gets the focus; mirror its ring onto the tile a sighted user sees. */
.iconpick input[type=radio]:focus-visible + svg,
.iconpick input[type=radio]:focus-visible + .iconpick-none { outline: 2px solid var(--brand-dark); outline-offset: 4px; }
/* Two controls that belong on one line: a swatch beside its hex, a day count beside its date. */
.colorrow { display: flex; gap: var(--s2); align-items: stretch; }
.colorrow .input { flex: 1; }
.colorrow input[type=color] { width: 44px; min-height: 36px; padding: 2px; border: 1px solid var(--n4);
                              border-radius: var(--r-sm); background: var(--n0); cursor: pointer; flex: none; }
.colorrow .btn { margin-top: 0; flex: none; }
.colorrow input[type=date] { flex: 1; }

/* Tap targets. Must sit AFTER the base .btn/.input rules: a media query adds no specificity,
   so earlier in the file the 36px base would win at every width. */
@media (max-width: 720px) {
    .btn, .input { min-height: 44px; }
}

/* ---- Onboarding wizard ---- */
.wizbar { display: flex; align-items: center; gap: var(--s2); max-width: 920px; margin: var(--s3) 0 var(--s5); }
.wizprog { display: inline-flex; align-items: center; gap: var(--s2); color: var(--n5); font-size: var(--t-md);
           font-weight: 600; white-space: nowrap; }
.wizprog .wizdot { width: 28px; height: 28px; border-radius: 50%; display: inline-grid; place-items: center;
                   border: 2px solid var(--n3); background: var(--n0); font-size: var(--t-md); transition: all .15s; }
.wizprog.active { color: var(--n8); }
.wizprog.active .wizdot { border-color: var(--brand); color: var(--brand-dark); }
.wizprog.done { color: var(--n8); }
.wizprog.done .wizdot { background: var(--brand-dark); border-color: var(--brand-dark); color: var(--brand-ink); }
.wizline { flex: 1; height: 2px; background: var(--n3); min-width: 14px; }
.wizline.done { background: var(--brand); }
.wizsteps { max-width: 920px; }
.stepcard { display: grid; grid-template-columns: 1fr 260px; gap: 26px; align-items: center;
            background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-lg);
            box-shadow: var(--e1); padding: var(--s5) 26px; }
.stepcard.solo { grid-template-columns: 1fr; }
.stepcard h3 { margin: 0 0 var(--s2); font-size: var(--t-lg); color: var(--n8); display: flex; align-items: center; gap: 10px; }
.stepcard .num { flex: none; width: 30px; height: 30px; border-radius: 50%; background: var(--brand-dark);
                 color: var(--brand-ink); display: inline-grid; place-items: center; font-size: var(--t-md); font-weight: 700; }
.stepcard p { margin: 0 0 var(--s3); color: var(--n6); font-size: var(--t-md); line-height: 1.6; }
.stepcard code { font-family: ui-monospace, Menlo, monospace; font-size: var(--t-sm); background: var(--n2);
                 padding: 1px 5px; border-radius: 5px; word-break: break-all; }
.wizsteps .err { color: var(--danger); font-size: var(--t-sm); margin: 6px 0 0; }
/* Separates the second APK block from the first. Was an inline style on the shared
   wizard markup, which Harbor could not space consistently around. */
.apkstep { margin-top: 16px; }

.platforms { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: var(--s1) 0 var(--s4); }
.platform { display: flex; flex-direction: column; align-items: center; gap: var(--s2); text-align: center;
            background: var(--n0); border: 2px solid var(--n3); border-radius: var(--r-lg); padding: 22px var(--s4);
            cursor: pointer; font: inherit; transition: border-color .15s, box-shadow .15s; }
.platform:hover { border-color: var(--n4); }
.platform.selected { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); }
.oslogo { width: 46px; height: 46px; display: grid; place-items: center; }
.oslogo svg { width: 40px; height: 40px; }
.oslogo.android { color: #3ddc84; }
.oslogo.ios { color: var(--n8); }
.osname { font-size: var(--t-lg); font-weight: 700; color: var(--n8); }
.ossub { font-size: var(--t-sm); color: var(--n5); line-height: 1.4; }

.devcard { display: flex; align-items: center; gap: var(--s4); background: var(--ok-wash); border: 1px solid #b7e3ca;
           border-radius: var(--r-lg); padding: 18px 20px; margin: var(--s1) 0 var(--s4); }
.devicon { flex: none; width: 52px; height: 52px; border-radius: 12px; background: var(--n0);
           border: 1px solid #cfe9db; display: grid; place-items: center; color: var(--ok); }
.devicon svg { width: 28px; height: 28px; }
.devcard strong { font-size: var(--t-lg); color: var(--n8); display: block; }
.devmeta { font-size: var(--t-sm); color: var(--n6); margin-top: 3px; }
.devnone { color: var(--n6); font-size: var(--t-md); }

.wiznav { display: flex; justify-content: space-between; align-items: center; gap: var(--s3); margin-top: 14px; }
.wizbtn { border: 0; border-radius: 9px; padding: 10px 18px; font-size: var(--t-md); font-weight: 600;
          background: var(--brand-dark); color: var(--brand-ink); cursor: pointer; }
.wizbtn:hover { background: var(--brand-darker); }
.wizbtn.ghost { background: var(--n2); color: var(--n8); }
.wizbtn.ghost:hover { background: var(--n3); }

.shotslot { margin: 0; border-radius: var(--r-lg); border: 1px solid var(--n3); background: var(--n1);
            display: grid; place-items: center; padding: var(--s4); min-height: 240px; }
.shotslot.phone { background: var(--n8); border-color: var(--n8); }
.shotslot img, .shotslot video { max-width: 100%; max-height: 320px; height: auto; display: block; border-radius: var(--r-md); }
.shotph { text-align: center; color: var(--n6); font-size: var(--t-sm); line-height: 1.5; padding: 14px; }
.shotph svg { width: 30px; height: 30px; opacity: .45; display: block; margin: 0 auto var(--s2); }
.shotph code { font-size: var(--t-xs); background: var(--n3); padding: 1px 5px; border-radius: 5px; }

/* The intro step keeps heading → video → Next order on a phone. The shared mobile rule lifts every
   .shotslot to the top (order:-1); the intro's video opts out so its Next button stays below it. */
@media (max-width: 720px) {
    .stepcard.introstep .shotslot { order: 0; }
}

.apkbtn { display: inline-flex; align-items: center; gap: var(--s2); background: var(--brand-dark); color: var(--brand-ink);
          border: 0; border-radius: 9px; padding: 11px 18px; font-size: var(--t-md); font-weight: 600; text-decoration: none; }
.apkbtn:hover { background: var(--brand-darker); }
.apkbtn svg { width: 17px; height: 17px; }
.apkbtn.off { background: var(--n3); color: var(--n6); pointer-events: none; }

.cmd { background: var(--n8); color: var(--n3); border-radius: 9px; padding: var(--s3) 14px; font-size: var(--t-md);
       font-family: ui-monospace, Menlo, monospace; overflow-x: auto; white-space: pre; margin: 0 0 10px; }

.addform { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start; }
.addform input { border: 1px solid var(--n4); border-radius: var(--r-md); padding: 10px var(--s3); font-size: var(--t-md); }
.addform input:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); }
.addform button { border: 0; border-radius: var(--r-md); padding: 10px 18px; font-size: var(--t-md); font-weight: 600;
                  background: var(--n8); color: #fff; cursor: pointer; }
.addform button:hover { background: #000; }
.codein { font-family: ui-monospace, Menlo, monospace; font-size: 20px; letter-spacing: 4px;
          text-transform: uppercase; width: 190px; text-align: center; }

.keybox, .ok, .found { border-radius: var(--r-md); padding: var(--s4); margin: 0 0 var(--s4);
                       background: var(--ok-wash); border: 1px solid #b7e3ca; }
.keybox code { display: block; font-size: 14px; font-weight: 700; color: var(--ok); word-break: break-all; margin: 6px 0; }
.ok { font-size: var(--t-md); color: var(--ok); }
.found dl { display: grid; grid-template-columns: 110px 1fr; gap: var(--s1) var(--s3); margin: 10px 0 14px; font-size: var(--t-md); }
.found dt { color: var(--n6); }
.found dd { margin: 0; font-weight: 600; color: var(--n8); }

.recenth { font-size: var(--t-md); font-weight: 700; margin: 26px 0 10px; color: var(--n8); }
.recent { max-width: 920px; margin-top: var(--s2); }
.recent .row { display: flex; justify-content: space-between; align-items: center;
               padding: 11px 2px; border-bottom: 1px solid var(--n3); }
.recent .row a { color: var(--brand-darker); text-decoration: none; font-weight: 600; }

.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; }

/* These two were lost to the same ordering bug as the tap targets: they live in the
   legacy media blocks, which sit before the base rules that override them. */
@media (max-width: 720px) {
    main { padding-bottom: var(--s7); }
    .found dl { grid-template-columns: 90px 1fr; }

    /* The enrollment wizard's step card. The identical rules exist in the legacy 720 block
       above, where the base `.stepcard { grid-template-columns: 1fr 260px }` and
       `.shotslot { min-height: 240px }` out-rank them on source order -- so steps 2 to 4 kept
       their two-column desktop layout on a phone and the document scrolled sideways: 363px
       of content in a 320px viewport. The 260px column is fixed, so the first column was
       squeezed to 34px and the overflow happened whether or not the screenshot had loaded.
       Step 1 is `.stepcard.solo`, already one column, which is why a sweep that only ever
       saw the first step found nothing. */
    .stepcard { grid-template-columns: 1fr; padding: var(--s4); }
    .shotslot { min-height: 0; order: -1; }
}

/* The smallest tier, and it must live here rather than in the 400px block further up, which
   sits before .topbar and .topslot and so could never beat them.

   At 320px a hamburger, a search box, a sort menu and the account menu share one 56px row,
   which leaves the search about 46px of typable width -- its placeholder renders as "Se".
   The page's own controls get their own row instead of being squeezed into that one. */
@media (max-width: 400px) {
    .topbar { height: auto; flex-wrap: wrap; padding-top: var(--s2); padding-bottom: var(--s2); }
    .topslot { order: 3; flex-basis: 100%; }
    .topslot select { flex: none; }
}

/* Pager. Sits after the base rules deliberately: see the four ordering bugs above. */
.pager { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap;
         margin-top: var(--s4); font-size: var(--t-sm); color: var(--n6); }
.pager .pagesize { display: flex; align-items: center; gap: var(--s2); margin: 0; }
.pager .pagesize .input { width: auto; min-width: 72px; }
.pager .pagecount { color: var(--n6); }
.pagenav { display: flex; align-items: center; gap: var(--s2); margin-left: auto; }
.pagenav .pagenow { color: var(--n6); white-space: nowrap; }
.pagenav .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

/* A hold is the only status an administrator causes, so it is the only filled badge —
   it must not be mistaken for the amber of an expiring trial. 9.77:1. */
.status.held { background: var(--n7); color: var(--n0); }
.rowactions { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.rowactions form { margin: 0; }

/* .iconbtn was deleted as dead in the redesign; it returns here with a real user. */
.cardicons { display: inline-flex; gap: var(--s1); align-items: center; }
.cardicons form { margin: 0; }
.iconbtn { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0;
           border: 1px solid var(--n3); border-radius: var(--r-sm); background: var(--n0);
           color: var(--n6); cursor: pointer; text-decoration: none; }
.iconbtn:hover { background: var(--n1); color: var(--n8); }
.iconbtn.danger:hover { background: var(--danger-wash); color: var(--danger); }
.iconbtn svg { width: 16px; height: 16px; }
.iconbtn:focus-visible { outline: 2px solid var(--brand-dark); outline-offset: 2px; }
@media (max-width: 720px) { .iconbtn { width: 44px; height: 44px; } }

/* The card's title row holds three things since the edit/delete icons arrived: the name, the
   icons, and the status badge. At 320px that is 271px of content in 221px of row, and because
   the row is nowrap the badge was pushed clean off the right edge — measured, not guessed.
   Letting it wrap puts the name on its own line and the icons beside the badge below it. */
@media (max-width: 400px) {
    .cardtop { flex-wrap: wrap; }
}

/* The row-action cell was display:flex, which takes a <td> out of table layout: it rendered
   62px tall against its row's 69px, so its bottom border sat 7px high and the row separator
   visibly stepped. Scoped to the desktop tier — below 720px the card rules turn every td into
   a block on purpose, and this must not fight them. */
@media (min-width: 721px) {
    td.rowactions { display: table-cell; white-space: nowrap; vertical-align: middle; }
    td.rowactions > * { display: inline-flex; vertical-align: middle; }
    td.rowactions form { margin: 0; }
    td.rowactions > * + * { margin-left: var(--s2); }
}

/* The manage page nests .panelform inside .formcard. the old grid wrapper's `.panelform` rule used to reset
   the inner frame, and that rule died correctly with the disclosure it belonged to — but the
   reset it provided is still needed, or the page draws an identical card inside a card with
   48px of stacked padding. Measured: both were 1px/14px/white/24px. */
.formcard .panelform { border: 0; padding: 0; background: transparent; }

/* `.panelform button { width: 100% }` exists for the add-user form's single submit button.
   The date-bump buttons are a wrapped flex row, so inheriting it gave three full-width rows
   where three small buttons belong. The primary actions below them keep their full width. */
.quickdates .btn { width: auto; }

/* ── Floating support pair ──────────────────────────────────────────────────────
   Help above Telegram, bottom right. Nothing else in this stylesheet claims that
   corner: position:fixed appears only for .permmodal, .sidebar and .navscrim.

   z-index 25 sits deliberately low on this stylesheet's small scale: above page content,
   but UNDER the topbar (30), the mobile drawer's scrim (35), the drawer itself (40) and
   the permission modal (50). Each of those is something the user is being asked to deal
   with, and a button floating over them would be both wrong and unreachable. */
.supfabs {
    position: fixed;
    right: 20px;
    bottom: 20px;
    z-index: 25;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.supfab {
    position: relative;
    width: 46px;
    height: 46px;
    border-radius: 23px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 16px rgba(15, 20, 25, .28);
    transition: transform .14s ease, box-shadow .14s ease;
}

/* The dark brand fill, not --brand: white on --brand is 2.53:1, under the 3:1 a graphical
   object needs, and this file already records that trap beside --brand-dark above. */
.supfab-help { background: var(--brand-dark); color: var(--brand-ink, #fff); }

/* Telegram's own blue, not the administrator's theme colour: a Telegram button that is
   not Telegram blue reads as something else. */
.supfab-tg { background: #229ED9; color: #fff; }

.supfab svg { width: 22px; height: 22px; }

.supfab:hover { transform: translateY(-2px); box-shadow: 0 7px 20px rgba(15, 20, 25, .34); }

.supfab:focus-visible { outline: 3px solid var(--brand-dark); outline-offset: 3px; }

.supfab-dot {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 11px;
    height: 11px;
    border-radius: 999px;
    background: #e5484d;
    border: 2px solid #fff;
}

@media (max-width: 720px) {
    .supfabs { right: 14px; bottom: 14px; gap: 8px; }
    .supfab { width: 42px; height: 42px; border-radius: 21px; }
    .supfab svg { width: 21px; height: 21px; }
}

@media (prefers-reduced-motion: reduce) {
    .supfab { transition: none; }
    .supfab:hover { transform: none; }
}

/* ── Support inbox and conversation ──────────────────────────────────────────── */
.navbadge {
    margin-left: auto;
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    text-align: center;
    background: var(--brand-dark);
    color: var(--brand-ink, #fff);
}

.sup-waiting { font-weight: 700; }

/* The top bar's alert bell. Square and the same 36px as .btn beside it, so the row of
   controls reads as a row rather than as a bell that wandered in. */
.bellbtn {
    position: relative;
    flex: none;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--n3);
    border-radius: var(--r-sm);
    background: var(--n0);
    color: var(--n5);
    cursor: pointer;
}

.bellbtn:hover { background: var(--n1); color: var(--n7); }
.bellbtn svg { width: 18px; height: 18px; }

/* On and blocked are both states the person chose or their browser imposed, so both are
   worth colouring. Off is the quiet default and stays the neutral of its neighbours. */
.bellbtn[data-state="on"] {
    color: var(--brand-dark);
    border-color: var(--brand);
    background: var(--brand-wash);
}

.bellbtn[data-state="blocked"] { color: var(--danger); border-color: var(--n3); }

/* Overlapping the corner rather than sitting beside it: the bar is tight at phone widths
   and this must not widen the control it belongs to. */
.bellbtn-count {
    position: absolute;
    top: -7px;
    right: -7px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--danger);
    color: #fff;
    font-size: var(--t-xs);
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}

.sup-panel { display: flex; flex-direction: column; gap: 14px; max-width: 760px; }

/* 2px between messages, with the gap between PEOPLE put back by the rule further down.
   A run from one person is one block, the way a chat app draws it. */
.sup-thread {
    display: flex; flex-direction: column; gap: 2px;
    max-height: 60vh; overflow-y: auto; padding: 4px;
}

.sup-msg { display: flex; flex-direction: column; max-width: 76%; }
.sup-me { align-self: flex-end; align-items: flex-end; }
.sup-them { align-self: flex-start; align-items: flex-start; }

.sup-msg:not(.sup-grouped) { margin-top: 10px; }

/* A short conversation sits at the bottom of the thread, against the composer, rather
   than at the top with empty space under it. margin-top:auto computes to zero once the
   content overflows, so unlike justify-content: flex-end it cannot put the top of a long
   thread out of reach. The specificity is deliberate: it has to beat the run spacing
   above, which would otherwise win on the first message. */
.sup-thread > .sup-msg:first-child,
.sup-thread > .sup-day:first-child { margin-top: auto; }

/* Drawn by support/_timeline.blade.php from the reader's clock — app.timezone is UTC and
   the server cannot know when their day starts. */
.sup-day { display: flex; justify-content: center; margin: 10px 0 6px; }

.sup-day span {
    padding: 3px 12px; border-radius: 999px;
    background: var(--n2); color: var(--n6);
    font-size: 11px; font-weight: 700; letter-spacing: .02em;
}

.sup-bubble {
    display: flex; flex-direction: column; gap: 8px;
    padding: 7px 11px; border-radius: 14px;
    background: var(--card); border: 1px solid var(--line);
}

/* Squared on the speaker's own side. Most of what makes a box read as a bubble, and what
   stops a two-word message rendering as a circle. */
.sup-me .sup-bubble { border-bottom-right-radius: 5px; }
.sup-them .sup-bubble { border-bottom-left-radius: 5px; }

.sup-me .sup-bubble {
    background: var(--brand-wash);
    border-color: color-mix(in srgb, var(--brand), white 70%);
}

/* The text and its time share one block so the float lands on the last line. The clearfix
   is load-bearing: in an attachment-only bubble the float is the only content, and a
   float gives its parent no height. */
.sup-text::after { content: ""; display: block; clear: both; }

.sup-body { font-size: 15px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }

/* Inside the bubble and floated to the end of the last line, as every messaging app draws
   it. On its own row underneath it cost a line per message and made a one-word message
   mostly timestamp. The text is rewritten to the reader's local clock by
   support/_timeline.blade.php; what the server renders is UTC. */
.sup-at {
    float: right;
    margin: 2px 0 0 10px;
    font-size: 11px;
    color: var(--muted);
    white-space: nowrap;
}

.sup-shot img { display: block; max-width: 100%; max-height: 300px; border-radius: 10px; }
.sup-media { width: 100%; max-width: 320px; border-radius: 10px; }
.sup-file { font-size: 14px; font-weight: 600; }
.sup-size { font-weight: 400; font-size: 12px; color: var(--muted); }
.sup-expired { margin: 0; font-size: 13px; color: var(--muted); font-style: italic; }

/* ── the composer ────────────────────────────────────────────────────────────
   The same pill the user's side wears. It replaced a bare <input type="file">, whose
   "Choose files / No file chosen" was the ugliest thing on the page and, because a file
   input will not shrink below the width of its own button, the reason this wrapped to
   three rows on a phone. */
.sup-composer { display: flex; align-items: flex-end; gap: 8px; }

.sup-field {
    flex: 1 1 320px; min-width: 0;
    display: flex; align-items: flex-end; gap: 2px;
    padding: 2px 4px 2px 6px;
    border: 1px solid var(--n4); border-radius: 21px; background: var(--n0);
}

.sup-input {
    flex: 1; min-width: 0;
    resize: none; font: inherit; font-size: 15px; line-height: 1.4;
    padding: 9px 4px; border: 0; background: none; color: inherit;
    overflow-y: hidden;
}

.sup-input:focus { outline: none; }

.sup-field:focus-within { outline: 2px solid var(--brand-dark); outline-offset: 1px; }

.sup-attach {
    flex: none; display: flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 19px; cursor: pointer;
    border: 0; background: none; color: var(--n6);
}

.sup-attach:focus-within { outline: 2px solid var(--brand-dark); outline-offset: -2px; }

.sup-attach svg { width: 20px; height: 20px; }

/* --brand-dark, not --brand: white on --brand is 2.53:1, and this file records that trap
   beside the token. */
.sup-send {
    flex: none; display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; padding: 0; border: 0; border-radius: 22px;
    cursor: pointer;
    background: var(--brand-dark); color: var(--brand-ink, #fff);
}

.sup-send:hover { background: var(--brand-darker); }

.sup-send svg { width: 21px; height: 21px; }

.sup-send:focus-visible { outline: 2px solid var(--brand-darker); outline-offset: 2px; }

/* The picked-file chips, drawn by support/_picker.blade.php. The same shape as Harbor's
   copy of them, in this file's palette — the administrator's composer gained the picker
   when the size check was shared, and with it the only pre-submit warning either side
   gets before PHP discards an oversized post. */
.sup-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }

.sup-chip { display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 4px 3px 10px; border-radius: 999px; background: var(--n2); }

/* The remove control. A plain glyph button: the chip is already the label. */
.sup-chipx { border: 0; background: none; cursor: pointer; font: inherit; font-size: 15px;
    line-height: 1; padding: 0 5px; border-radius: 999px; color: inherit; }

.sup-chipx:hover { background: var(--n3); }
.sup-chipx:focus-visible { outline: 2px solid var(--brand-dark); outline-offset: 1px; }

.sup-err { color: var(--danger); }

/* ── The administrator's conversation on a phone ──────────────────────────────────
   At 720px and below the reply view stops being a page you scroll and becomes a chat
   screen: the thread takes whatever the header and the composer leave, and scrolls by
   itself. Measured before this block, the composer sat below the fold at every phone
   size: at 320x568 the page had to be scrolled 212px to reach it, so replying meant
   scrolling away from the message being replied to. Harbor's copy of this chat already
   works the other way round (the "Support chat" block in harbor.css); the two sides
   now agree.

   Scoped to .app.supchat, which support/thread.blade.php sets through the layout's
   `shell` section — the same extension point the device console uses for `focused` — so
   no other panel page inherits a viewport-height ceiling. Above 720px nothing here
   applies and the desktop shape is exactly as it was: a 760px column, a 60vh thread.

   The height clause is a phone held sideways: 844x390 is 844px WIDE, so a width-only
   query leaves the landscape chat on the desktop shape, where the 60vh thread plus the
   page header overflow a 390px viewport and the composer needs 93px of page scrolling
   to reach. It catches a short desktop window too, which has the same problem. */
@media (max-width: 720px), (max-height: 520px) {
    /* height, not min-height: a flex child cannot shrink into a ceiling its ancestors do
       not have, and .app's min-height: 100vh is not one. dvh so a mobile browser's
       collapsing address bar does not leave the composer under it. */
    .app.supchat { height: 100dvh; min-height: 0; }

    /* .appmain is already a flex column; this is the min-height: 0 that lets it shrink
       below its content, which is what makes the overflow below engage at all. */
    .app.supchat .appmain { min-height: 0; }

    /* The page gives up its scrollbar so the thread can have one. main keeps overflow-y
       as the safety valve .hb-main plays in Harbor: on a viewport too short for the page
       header and the composer alone, the page scrolls rather than leaving a thread
       squeezed to nothing with no way to reach it. The s7 bottom padding every other page
       reserves for the floating buttons would be dead space under a pinned composer. */
    .app.supchat main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
                        padding-bottom: var(--s4); overflow-y: auto; }

    .app.supchat .sup-panel { flex: 1 1 auto; min-height: 0; }
    .app.supchat .sup-panel > * { flex: none; }

    /* max-height: none, not a smaller vh: the thread's height is now whatever is left,
       and any vh figure alongside a header and a composer either overshoots the viewport
       or leaves a gap below the composer. */
    .app.supchat .sup-thread { flex: 1 1 auto; min-height: 0; max-height: none; }

    /* Inert while the thread is the scroller, which is the normal case. In the safety-valve
       case above it keeps the composer at the bottom of the viewport instead of letting it
       ride away with the thread. */
    .app.supchat .sup-composer { position: sticky; bottom: 0; background: var(--bg);
                                 padding: var(--s2) 0 0; }
}

/* Width-only refinements. A short but wide window keeps the desktop bubble width; only a
   narrow one needs this. 76% of a 288px column is a 219px bubble against a 69px gutter,
   and the gutter is there to show which side a message is on — 14% does that. */
@media (max-width: 720px) {
    .app.supchat .sup-msg { max-width: 86%; }
}

/* A short viewport spends its height on the conversation, not the furniture. Measured at
   390x420 — a tall phone with a big keyboard open, which the viewport meta in
   layout.blade.php now makes a real height rather than an overlay — the thread was down to
   34px: one line of the newest message. */
@media (max-height: 520px) {
    .app.supchat .pagehead { margin-bottom: var(--s2); }
    .app.supchat .pagehead p { display: none; }
    .app.supchat .sup-panel { gap: var(--s2); }
    .app.supchat .sup-day { margin: 6px 0 4px; }
    .app.supchat .sup-msg:not(.sup-grouped) { margin-top: 8px; }
}

/* On a phone the page header repeats what the breadcrumb above it already says — the same
   email address, twice, in the first 150px of a 568px screen. It is hidden from the eye
   and kept for everything else: this page's only <h1> still has to exist for a screen
   reader and for the document outline, so it is clipped rather than display:none. */
@media (max-width: 720px) {
    .app.supchat .pagehead > div:first-child {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
    }

    .app.supchat .pagehead { margin-bottom: var(--s2); }
}

/* ── User journey ────────────────────────────────────────────────────────────── */
/* A reading screen, not a worklist: the shapes here are chosen so an administrator can
   scan a column of times and spot the red line without reading any of it. */
/* 148px, not the 180 this started at: at 180 a 390px phone fits one tile per row, and
   four stacked tiles put 349px of summary between the top of the page and the first
   thing anybody came here to read. Measured, 148 halves that and still holds the
   longest value ("16 Oct 2026, 20:04") on one line. */
.jstats { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
          gap: var(--s3); margin-bottom: var(--s5); }
.jstat { background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-lg);
         padding: var(--s4); display: flex; flex-direction: column; gap: var(--s1); }
.jstat-n { font-size: var(--t-xl); font-weight: 700; color: var(--n8); line-height: 1.2; }
.jstat-l { font-size: var(--t-sm); color: var(--n5); display: flex; align-items: center;
           gap: var(--s2); flex-wrap: wrap; }
/* Only the problems tile changes colour, and only when there are some: a red surface
   that is always there stops meaning anything within a day. */
.jstat-bad { border-color: #f0c4c0; background: var(--danger-wash); }
.jstat-bad .jstat-n { color: var(--danger); }
.jnow { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-weight: 600; }
.jnow .udot { margin-top: 0; }

.jscreens { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.jscreens li { display: flex; justify-content: space-between; gap: var(--s4);
               padding: var(--s2) 0; border-bottom: 1px solid var(--n2); font-size: var(--t-md); }
.jscreens li:last-child { border-bottom: 0; }
.jscreens b { color: var(--n6); }

.jfilter { display: flex; gap: var(--s2); margin-bottom: var(--s4); flex-wrap: wrap; }

.jvisit { background: var(--n0); border: 1px solid var(--n3); border-radius: var(--r-lg);
          margin-bottom: var(--s4); overflow: hidden; }
.jvisit-head { display: flex; justify-content: space-between; gap: var(--s3);
               padding: var(--s3) var(--s4); background: var(--n1);
               border-bottom: 1px solid var(--n3); font-size: var(--t-sm);
               font-weight: 600; color: var(--n6); }
.jlist { list-style: none; margin: 0; padding: 0; }
/* A grid, not a table: the time and the status are fixed columns so they line up down
   the whole visit, and the two middle cells take whatever is left. */
.jrow { display: grid; grid-template-columns: 56px minmax(0, 1fr) minmax(0, 1.2fr) 48px;
        gap: var(--s3); align-items: baseline; padding: var(--s2) var(--s4);
        border-bottom: 1px solid var(--n2); font-size: var(--t-md); }
.jrow:last-child { border-bottom: 0; }
.jat { color: var(--n5); font-variant-numeric: tabular-nums; font-size: var(--t-sm); }
.jlabel { color: var(--n8); font-weight: 600; }
.jpath { color: var(--n5); font-size: var(--t-sm); overflow-wrap: anywhere; }
.jstatus { text-align: right; color: var(--n5); font-size: var(--t-sm);
           font-variant-numeric: tabular-nums; }
.jstatus-bad { color: var(--danger); font-weight: 700; }
/* The whole row, not just the number: a problem is the thing you are scanning for. */
.jrow-bad { background: var(--danger-wash); }
.jrow-bad .jlabel { color: var(--danger); }
.jtag { display: inline-block; margin-left: var(--s2); padding: 1px 7px; border-radius: 999px;
        background: var(--n2); color: var(--n6); font-size: var(--t-xs); font-weight: 600; }
.jrow-bad .jtag { background: #f6d5d1; color: #8f2a1f; }

/* On a phone the four columns become two lines: when and what, then where and what
   happened. Four columns at 360px gives the path about 60px, which is no path at all. */
@media (max-width: 720px) {
    .jrow { grid-template-columns: 56px minmax(0, 1fr) 48px; row-gap: 2px; }
    .jpath { grid-column: 2 / 4; }
}
